前两节都在纸上谈兵,这一节通电。 输出=你决定一根针是高还是低(点亮一颗 LED);输入=你让一根针去读别人是高还是低(读一个按钮)。这两件事加起来,就是所有数字电路实验的原型。
7-1 给了你针脚地图,7-2 给了你工具(gpiozero)——本节是它们第一次真的碰上电。
本节还第一次出现"要算"的东西:限流电阻到底用多大(这是 1-5 那三条电子铁律的第一次实际应用)。
6-4 的 while 与 6-5 的"函数作参数"在这里都用得上——button.when_pressed = 某个函数 这句话,就是"把函数当值传递"。
一、"输出"到底是什么意思
输出:你写一行代码,那根针上的电压就变。
led.on() # BCM 17 变成 3.3V(高电平)
led.off() # BCM 17 变成 0V(低电平)
它表达的只有"两种情况"。所以在数字输出这件事上,你能做的事只有:把某个东西通电/断电。听起来很少,但配上下面这些,就什么都能干了:
- LED:亮 / 灭
- 蜂鸣器:响 / 不响
- 继电器:接通 / 断开(间接控制大电流设备)
- 另一个芯片的某个脚:给它一个"开始"信号
二、⚠️ 做任何输出实验前,先记住这一条:必须串限流电阻
这是本章唯一会让你的元件当场冒烟的地方。
2.1 为什么必须串电阻
LED 不是"灯泡",是"二极管"。它的特点是:只要电压超过它的导通门槛(约 2~3V),它就几乎变成一根导线——内阻趋近于 0。
而 3.3V 是明显超过门槛的。所以不串电阻直接接,会发生:
- 电流不受限制地冲过去(理论上电流只受电源能力限制);
- 电流远超 LED 自己的承受能力 → LED 烧掉;
- 同时也远超 GPIO 针的承受能力 → 针也可能烧掉。
电阻的作用就是"把电流掐到安全值"。它串在电路里,承受掉多余的电压。
2.2 算一次(这就是本节唯一的"要会算")
用欧姆定律:电阻 = 多余的电压 ÷ 想要的电流
R = (3.3V − LED 的导通电压) ÷ 想要的电流
以最常见的红色 LED 为例:
| 项 | 数值 | 说明 |
|---|---|---|
| 电源电压 | 3.3V | GPIO 拉高时的电压 |
| LED 导通电压(Vf) | 约 2.0V | 红色 LED 的典型值 |
| 多余电压 | 3.3 − 2.0 = 1.3V | 这部分要由电阻承担 |
| 想要的电流 | 约 6mA(0.006A) | 够亮,又很安全 |
| 算出的电阻 | 1.3 ÷ 0.006 = 217Ω | 取最近的标准值 220Ω |
反过来验算:用 220Ω 时,实际电流 = 1.3V ÷ 220Ω ≈ 5.9mA。
这个数安全吗?GPIO 单针的上限大约 16mA,而长期使用建议只取几毫安——5.9mA 正好落在"够亮又稳当"的区间。这就是为什么 220Ω 是全世界最通用的 LED 限流电阻。
| 颜色 | 导通电压 | 用 220Ω 时的电流 | 效果 |
|---|---|---|---|
| 红、黄、绿 | 约 2.0V | 约 5.9mA | 正好 |
| 蓝、白 | 约 3.0V | 约 1.4mA | 偏暗(因为门槛吃掉太多电压) |
| 所以:手头没别的电阻时,就用 220Ω 配红/黄/绿 LED,效果最标准。 | |||
| 想更保守(比如担心接错)就用 1kΩ:电流只有约 1.3mA,灯明显暗,但几乎不可能烧任何东西。 |
电阻没有方向(正反接都一样);LED 有方向(见下)。
2.3 认清 LED 的两个脚
- 长脚 = 正极(阳极) → 接 GPIO 那一侧
- 短脚 = 负极(阴极) → 接 GND 那一侧
如果两个脚被剪得一样长:看里面——LED 内部有两个金属支架,大块的是负极,小块(像个小杯子)的是正极。
LED 是二极管,反向基本不导电(只会漏一点点)。所以接反的现象是:程序跑得好好的、读回也是 1,但灯就是不亮。 排查经验:灯不亮时,先量(或换)极性,再怀疑代码。这是"现象和原因不对应"的典型——代码这边一切正常。
三、接线:第一个电路
3.1 (附)面包板三条常识
如果你第一次用面包板,只需要记住三件事:
- 中间那条凹槽把上下两半隔开——凹槽左边的孔和右边的孔不连通。
- 同一列的 5 个孔是连通的(以凹槽为界,上半区一列 5 个孔互通,下半区一列 5 个孔互通)。所以同一列上的元件互相当导线连着。
- 上下两条长排(通常标着红线和蓝线)是"电源轨"——整排贯通,一般用来铺 3.3V 和 GND。
最简单的用法:元件跨着凹槽插(两条脚分别落在上下半区),这样它的两条脚就不连通;需要连通时,就用导线把它们插进同一列。
3.2 LED 接线表(本课统一约定)
| 从 | 经过 | 到 |
|---|---|---|
| BCM 17(物理针 11) | 一根跳线 | 面包板某列 |
| 那一列 | 220Ω 电阻 | 另一列 |
| 那一列 | LED 长脚(正极) | — |
| LED 短脚(负极) | 一根跳线 | GND(物理针 9 或 6) |
四个要点:
- 接线前拔掉电源。接完、用眼睛检查一遍,再上电。
- 电阻和 LED 谁在前面谁在后面都行(串联电路里电流处处相等),只要它们串在一条路上。
- LED 的长脚朝向 GPIO 方向(电流从 GPIO 流向 GND)。
- GND 用哪个都行——排上 8 个 GND 都通,挑离你最近的(这就是 7-1 讲"8 根 GND 是为方便"的意思)。
四、让 Python 点亮它
写 led_blink.py:
from gpiozero import LED
import time
led = LED(17)
try:
print("亮 3 秒")
led.on()
time.sleep(3)
print("灭 3 秒")
led.off()
time.sleep(3)
print("后台闪烁 6 次(每次间隔 0.5 秒)")
led.blink(on_time=0.5, off_time=0.5, n=6, background=False)
finally:
led.off()
led.close()
print("结束,灯已熄灭")
跑:
python3 led_blink.py
看到什么算成功:LED 先亮 3 秒 → 灭 3 秒 → 快速闪 6 次 → 熄灭,屏幕上依次打印对应的提示。
读结果 · 三种"让灯动"的方式要分清:
| 写法 | 行为 | 什么时候用 |
|---|---|---|
led.on() / led.off() |
立刻改变,然后立刻交回控制权 | 你要自己控制时间节奏 |
led.blink(on_time=, off_time=, n=, background=False) |
挡住程序,闪完才继续(上面 Demo 用的这种) | 简单的闪烁,不关心别的 |
led.blink(on_time=1, off_time=1)(不写 background=False) |
不挡程序,在后台一直闪 | 你还要同时干别的事(比如读按钮) |
blink() 默认 background=True——它开一个小线程在后台闪,主程序立刻继续往下走。
所以如果你想让"灯一直闪"的同时"程序还能响应按钮",就该用默认的后台方式;如果写了 background=False,程序会停在那一行直到闪完。
这是本课第一次遇到"同时做两件事",7-3 后面的按钮控制就会用到它。
如果没有 finally,你在灯亮着的时候按 Ctrl + C,程序退出但灯可能留在亮的状态(针没被释放)。
用 finally 保证"不管怎么结束,都把灯关掉"——这是硬件实验的好习惯,和第 6 章的代码风格一样重要。
(gpiozero 在程序正常退出时也会自动清理,但 Ctrl + C 属于"非正常",写明 finally 更稳妥。)
五、数字输入:按钮,以及"浮空"这个坑
输入:让一根针去读外面的电平。
接一个按钮到 BCM 27(物理针 13),你以为只要"接上就能读"。但这里有一个必须先解决的物理问题。
5.1 浮空的针会读到随机值
如果一根输入针什么都没接(或者只接了按钮的一端、按钮另一端悬空),它上面的电压是不确定的。
为什么?因为它像一根小天线:周围环境的电磁场(你的手、灯、电源线)都会在它上面感应出电压。结果是——读到的值忽 1 忽 0。
这叫"浮空"(floating)。它的现象很迷惑人:按钮明明没按,程序却一直在触发。
5.2 解法:给它一个"默认值"
办法是给这根针配一个电阻,把它"轻轻拉"到某一端:
| 做法 | 效果 | 平时读到 | 按下时读到 |
|---|---|---|---|
| 上拉(拉到 3.3V) | 平时高,按下(接地)变低 | 1 | 0 |
| 下拉(拉到 GND) | 平时低,按下(接 3.3V)变高 | 0 | 1 |
好消息是:树莓派的每根 GPIO 都有内置的上拉/下拉电阻,可以软件开关,不用你外接。这是它比老式单片机方便的地方之一。
5.3 gpiozero 的选择:默认上拉,所以按钮接 GND
Button(27) 默认 pull_up=True(内部上拉)。这决定了接线方式:
| 按钮的一端 | 按钮的另一端 |
|---|---|
| BCM 27(物理针 13) | GND(任意一个 GND 针) |
按下时读到 0,松开读到 1。——注意这是反的:"按下"对应"低电平"。
很多人凭直觉把按钮的另一端接到 3.3V。结果是什么呢——按钮完全不起作用,程序永远认为"没按下"。
原因:内部上拉已经把针平时就拉到 3.3V(读 1 = 松开);你的按钮又把它接到 3.3V,按不按都是 3.3V,读数永远不变,程序自然永远读不到"按下"这个动作。
规矩:gpiozero 的 Button 默认上拉,所以按钮接 GND,不是接 3.3V。
想接 3.3V 就得写 Button(27, pull_up=False)(改成内部下拉)——接线方式和软件设置必须配套,这是同一个决定的两半。
5.4 去抖:为什么"按一下"会触发好几次
按钮内部的金属片在接触的瞬间会"弹跳"——在这几毫秒里,它一会儿通一会儿断,像一颗小石子在地上弹。
结果:程序以为你按了 3 次。
gpiozero 已经帮你处理了:Button 默认 bounce_time=0.01(10 毫秒)——它会忽略这 10 毫秒内的抖动。所以正常情况下你不用管它。
什么时候要调它:如果你用一个质量差的按钮、或者长长的导线(导线本身也有噪声),可以把 bounce_time=0.05 调大一点。调大代价是"反应变慢"——所以别随便调到 0.5 秒。
六、按钮的三种读法(从简单到好用)
版本 A:轮询(不停地问"按了吗")
while True:
if button.is_pressed:
print("按下")
缺点:CPU 空转(它一秒问几十万次),而且这个循环里干不了别的。
版本 B:阻塞等待(停在那里等)
button.wait_for_press()
print("被按了一下")
优点:几乎不占 CPU。缺点:就停在那儿了,后面还要干别的事就得再想办法。
版本 C:事件回调(告诉它"按下时该做什么",然后放心去干别的)
from gpiozero import Button
from signal import pause
button = Button(27)
button.when_pressed = lambda: print("按下了!")
pause() # 挂起,专门等事件
这是最推荐的写法。因为:
- 不占 CPU(
pause()时进程睡着); - 可以同时挂好几件事(多个
when_pressed、后台闪烁的灯都能一起跑); - 代码结构清楚:每个事件对应一小段逻辑,不用写成一堆嵌套的
if。
七、动手 Demo:按钮控制灯(三步递进)
接线(先断电):
| 元件 | 一端 | 另一端 |
|---|---|---|
| LED + 220Ω | BCM 17(物理针 11) | GND |
| 按钮 | BCM 27(物理针 13) | GND |
(两件东西可以同时在面包板上,各占一个 GND 针,互不影响。)
第 1 步 · 按住亮,松开灭(最短的版本)
nano btn_hold.py
from gpiozero import LED, Button
from signal import pause
led = LED(17)
button = Button(27)
button.when_pressed = led.on # 注意:这里没有括号!
button.when_released = led.off
print("按住按钮灯亮,松开灯灭。Ctrl+C 退出。")
pause()
跑:python3 btn_hold.py
看到什么算成功:按住按钮灯亮,松手灯灭。
- 写
led.on(不加括号)= "把这个动作交给它"——以后每次按下时,它自己去调用。 - 写
led.on()(加括号)= "现在就执行一次"——那么语句执行完、灯就亮了,跟按钮再没关系。 这是 6-5 讲过的"函数当值传递"。接错了不会报错,只是行为不对(灯会在脚本一启动就亮起来、按钮按了没反应)。
第 2 步 · 按一下切换亮灭(引入"状态")
nano btn_toggle.py
from gpiozero import LED, Button
from signal import pause
led = LED(17)
button = Button(27)
count = 0
lit = False
def on_press():
global count, lit
count += 1
lit = not lit # 取反:亮变灭、灭变亮
led.value = lit
print(f"第 {count} 次按下 → 灯现在{'亮' if lit else '灭'}")
button.when_pressed = on_press
print("每按一次切换一次。Ctrl+C 退出。")
pause()
跑:python3 btn_toggle.py
看到什么算成功:
每按一次切换一次。Ctrl+C 退出。
第 1 次按下 → 灯现在亮
第 2 次按下 → 灯现在灭
第 3 次按下 → 灯现在亮
读结果:这里引入了一个新概念——"状态"(state)。按钮本身只有"按下/松开"两种瞬时动作,而"灯现在是亮的还是灭的"必须由你的程序自己记住(那个 lit 变量)。
这也是"程序"这个词的真正含义:它不只响应眼前这一刻,它还带着一个"过去"。(global 的用法见 6-5:函数里要给外面的变量赋值,必须声明。)
第 3 步 · 加一个短按/长按的区分(可选,玩一下)
gpiozero 自带 held_time 和 when_held:
button = Button(27, hold_time=1.0)
button.when_held = lambda: print("长按超过 1 秒!")
不用改接线,只是改代码里的判断条件。这说明"更多功能"往往只是"代码里多写一行",而不是"多接一根线"。
八、30 秒实验:不用按钮,用一根线当按钮
这个实验特别好用——手边没按钮、或者怀疑按钮坏了的时候,它能把问题定位到"是按钮的锅还是代码的锅"。
做法:插一根跳线在一根 GND 针上,另一头空着。然后跑下面这个脚本:
nano wire_test.py
from gpiozero import Button
from signal import pause
button = Button(27)
button.when_pressed = lambda: print("针 13 被接到地上了(等同于按下)")
print("用那根接在 GND 上的线去碰物理针 13,每碰一次会打印一行。")
pause()
你会看到什么:每次用那根线碰一下针 13,就打印一行。
读结果:这根线做的事,和按钮被按下时做的事在电学上完全一样(都是"把这个针连到 0V")。
用 GND 线碰是安全的(这就是"按下")。如果拿一根接在 3.3V 或 5V 上的线去碰,那是在给针强灌高电压——5V 那根尤其危险。 做这个实验前,把手上多余的线收走,桌上只留那一根接地线。
九、常见误区
- "LED 直接接针就能亮,为什么还要电阻" —— 会亮,但电流没被限制,可能在慢慢损伤 LED 或那根针。220Ω 是底线,不是讲究。
- "灯不亮就是代码错了" —— 先查极性(LED 长脚有没有朝 GPIO)、再查 GND 有没有接上。LED 接反时程序一切正常、灯完全不亮——这是最常见的"假故障"。
- "按钮接 3.3V 还是接 GND 都行" —— 不行。gpiozero 的
Button默认内部上拉,必须接 GND。接 3.3V 会出现"按不按都一样"的现象(想接 3.3V 就得写pull_up=False)。 - "输入针直接接按钮就行,不用管上拉下拉" —— 不管它,针会浮空,读数随机,现象是"没按也在触发"。上拉或下拉必须有一个(用 gpiozero 就是选
pull_up=True/False,它帮你开内部电阻)。 - "按一下计数加 3,是我的代码有问题" —— 大概率是触点抖动。gpiozero 默认已经去抖 10 毫秒;按钮质量差时可把
bounce_time调到 0.05。 - "
when_pressed = led.on()和= led.on差不多" —— 差远了。加了括号是"现在就执行",不加才是"交给它以后执行"(见第七节那个警告框)。 - "轮询(
while True+is_pressed)是最好的写法" —— 它空转 CPU,而且干不了别的事。事件回调 +pause()才是推荐写法。 - "
Ctrl+C退出后灯还亮着,是 gpiozero 的 bug" —— 是没做清理。用try/finally把led.off()包进去。
自测三问(点开对答案)
1. 红色 LED 接在 3.3V 的 GPIO 上,为什么用 220Ω 电阻?把这个数算出来。 2. 按钮一端接 BCM 27、另一端接 GND,为什么"按下"时读到的是 **0** 而不是 1? 3. 你写 `button.when_pressed = led.on()`,脚本一跑灯就亮了、按按钮却没反应。为什么? **参考答案** 1. 因为 **LED 是二极管,导通后内阻趋近于 0,必须靠外部电阻限制电流**。算法:**R =(3.3V − LED 导通电压 2.0V)÷ 想要的电流 0.006A = 1.3 ÷ 0.006 ≈ 217Ω**,取标准值 **220Ω**。反验:1.3V ÷ 220Ω ≈ **5.9mA**,落在"够亮又安全"的区间(GPIO 单针上限约 16mA,长期使用建议几毫安)。 2. 因为 gpiozero 的 `Button` **默认开启内部上拉电阻**——**平时这根针被"轻轻拉"到 3.3V(读到 1)**;按下时,按钮把针**直接接到了 GND**(0V),电阻那点力气拉不过一根导线,于是读到 **0**。**所以"按下"等于"接地"等于低电平**——这就是"低电平有效"。 3. 因为 **`led.on()` 加了括号=立刻执行一次**。这句话在**脚本启动的那一刻**就把灯点亮了,然后**把 `None`(`on()` 的返回值)赋给了 `when_pressed`**——等于告诉它"按下时什么都不用做"。正确写法是 **`button.when_pressed = led.on`(不加括号)**,意思是"把这个动作交给它,以后按下时调它"。- ☐ 下一节(7-4 模拟输出) —— 让灯不只"亮/灭",而是能"半亮";讲清 PWM 这个把数字变成近似模拟的办法
- ☑ 本章第三节已读 —— 亲手点亮了第一颗 LED,也让第一颗按钮有了反应
相关笔记
- Python 的 GPIO 模块 —— 上一节:
LED(17)与Button(27)的来处,以及报错时该往哪查 - 认识树莓派的 GPIO 引脚 —— BCM 17(物理针 11)、BCM 27(物理针 13)的对照表在那里
- 你需要知道的背景知识 —— 电压/电流/电阻的三条新手铁律,本节第一次真的用上
- 树莓派的硬件规格 —— 3.3V 铁律与 GPIO 针的电流上限
- Python 函数与模块 ——
when_pressed = 某函数为什么不能加括号,答案在"函数也是值"那里 - 🍓 树莓派 · 索引 —— 全课路线图