🍓 第七章 GPIO 硬件接口
📄 一句话概括

前两节都在纸上谈兵,这一节通电。 输出=你决定一根针是高还是低(点亮一颗 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(低电平)

它表达的只有"两种情况"。所以在数字输出这件事上,你能做的事只有:把某个东西通电/断电。听起来很少,但配上下面这些,就什么都能干了:

二、⚠️ 做任何输出实验前,先记住这一条:必须串限流电阻

这是本章唯一会让你的元件当场冒烟的地方。

2.1 为什么必须串电阻

LED 不是"灯泡",是"二极管"。它的特点是:只要电压超过它的导通门槛(约 2~3V),它就几乎变成一根导线——内阻趋近于 0。

而 3.3V 是明显超过门槛的。所以不串电阻直接接,会发生:

  1. 电流不受限制地冲过去(理论上电流只受电源能力限制);
  2. 电流远超 LED 自己的承受能力 → LED 烧掉;
  3. 同时也远超 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 限流电阻。

💡 不同颜色的 LED 差别很大
颜色 导通电压 用 220Ω 时的电流 效果
红、黄、绿 约 2.0V 约 5.9mA 正好
蓝、白 约 3.0V 约 1.4mA 偏暗(因为门槛吃掉太多电压)
所以:手头没别的电阻时,就用 220Ω 配红/黄/绿 LED,效果最标准。
想更保守(比如担心接错)就用 1kΩ:电流只有约 1.3mA,灯明显暗,但几乎不可能烧任何东西。

电阻没有方向(正反接都一样);LED 有方向(见下)。

2.3 认清 LED 的两个脚

如果两个脚被剪得一样长:看里面——LED 内部有两个金属支架,大块的是负极,小块(像个小杯子)的是正极。

⚠️ 接反了会怎样?不一定烧,但一定不亮

LED 是二极管,反向基本不导电(只会漏一点点)。所以接反的现象是:程序跑得好好的、读回也是 1,但灯就是不亮。 排查经验:灯不亮时,先量(或换)极性,再怀疑代码。这是"现象和原因不对应"的典型——代码这边一切正常。

三、接线:第一个电路

3.1 (附)面包板三条常识

如果你第一次用面包板,只需要记住三件事:

  1. 中间那条凹槽把上下两半隔开——凹槽左边的孔和右边的孔不连通。
  2. 同一列的 5 个孔是连通的(以凹槽为界,上半区一列 5 个孔互通,下半区一列 5 个孔互通)。所以同一列上的元件互相当导线连着。
  3. 上下两条长排(通常标着红线和蓝线)是"电源轨"——整排贯通,一般用来铺 3.3V 和 GND。

最简单的用法:元件跨着凹槽插(两条脚分别落在上下半区),这样它的两条脚就不连通;需要连通时,就用导线把它们插进同一列。

3.2 LED 接线表(本课统一约定)

从 经过 到
BCM 17(物理针 11) 一根跳线 面包板某列
那一列 220Ω 电阻 另一列
那一列 LED 长脚(正极) —
LED 短脚(负极) 一根跳线 GND(物理针 9 或 6)

四个要点:

四、让 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) 不挡程序,在后台一直闪 你还要同时干别的事(比如读按钮)
💡 `background` 这个参数是理解"并发"的入口

blink() 默认 background=True——它开一个小线程在后台闪,主程序立刻继续往下走。 所以如果你想让"灯一直闪"的同时"程序还能响应按钮",就该用默认的后台方式;如果写了 background=False,程序会停在那一行直到闪完。 这是本课第一次遇到"同时做两件事",7-3 后面的按钮控制就会用到它。

⚠️ `try` / `finally` 为什么值得写

如果没有 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()      # 挂起,专门等事件

这是最推荐的写法。因为:

  1. 不占 CPU(pause() 时进程睡着);
  2. 可以同时挂好几件事(多个 when_pressed、后台闪烁的灯都能一起跑);
  3. 代码结构清楚:每个事件对应一小段逻辑,不用写成一堆嵌套的 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()` 的区别,是本节最值得记住的一行代码
  • 写 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

用 GND 线碰是安全的(这就是"按下")。如果拿一根接在 3.3V 或 5V 上的线去碰,那是在给针强灌高电压——5V 那根尤其危险。 做这个实验前,把手上多余的线收走,桌上只留那一根接地线。

九、常见误区

  1. "LED 直接接针就能亮,为什么还要电阻" —— 会亮,但电流没被限制,可能在慢慢损伤 LED 或那根针。220Ω 是底线,不是讲究。
  2. "灯不亮就是代码错了" —— 先查极性(LED 长脚有没有朝 GPIO)、再查 GND 有没有接上。LED 接反时程序一切正常、灯完全不亮——这是最常见的"假故障"。
  3. "按钮接 3.3V 还是接 GND 都行" —— 不行。gpiozero 的 Button 默认内部上拉,必须接 GND。接 3.3V 会出现"按不按都一样"的现象(想接 3.3V 就得写 pull_up=False)。
  4. "输入针直接接按钮就行,不用管上拉下拉" —— 不管它,针会浮空,读数随机,现象是"没按也在触发"。上拉或下拉必须有一个(用 gpiozero 就是选 pull_up=True/False,它帮你开内部电阻)。
  5. "按一下计数加 3,是我的代码有问题" —— 大概率是触点抖动。gpiozero 默认已经去抖 10 毫秒;按钮质量差时可把 bounce_time 调到 0.05。
  6. "when_pressed = led.on() 和 = led.on 差不多" —— 差远了。加了括号是"现在就执行",不加才是"交给它以后执行"(见第七节那个警告框)。
  7. "轮询(while True + is_pressed)是最好的写法" —— 它空转 CPU,而且干不了别的事。事件回调 + pause() 才是推荐写法。
  8. "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`(不加括号)**,意思是"把这个动作交给它,以后按下时调它"。

相关笔记