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

上一节看清了那排针,这一节要学会指挥它们。 Python 控制 GPIO 有好几套库,而且换代了——老教程里那句人人都在抄的 import RPi.GPIO,在新板子上会直接报错。本节把"该用哪个、为什么、报错了怎么办"一次讲清。

📝 本节和前面几节的关系

6-5 你学过"模块就是一个 .py 文件"、学过 import 的几种写法,还学过装库的两条正路(apt 与 venv);本节是那些知识第一次用在硬件上。 上一节讲的"BCM 编号"从这里开始变成代码里的数字:LED(17) 里的这个 17 就是 BCM 号。 4-3 讲的"用户与组"会在本节末尾回来——GPIO 设备文件不是谁都能碰的。

一、三代库:为什么会有"该用哪个"的问题

Python 控制 GPIO 这件事,历史上出现过三代做法,现在三代都还活着:

代 名字 出现时间 它怎么工作 现在还用吗
第一代 RPi.GPIO 2012 直接读写主芯片的内存寄存器 ⚠️ 在新板子上失效(见第二节)
第二代 gpiozero 2015 把一根针包装成"设备对象",底层外包 ✅ 官方推荐,新项目首选
底层 lgpio / libgpiod 近年 走内核提供的通用 GPIO 接口 ✅ 高级用法/底层工具
💡 打个比方就懂了

RPi.GPIO 像给你一把螺丝刀:告诉你"一号针的寄存器第 3 位是方向位,写 1 是输出"——你得自己拧。 gpiozero 像给你一盏已经装好的灯:你只要说 led.on(),灯亮;led.off(),灯灭。它自己知道该怎么拧那根螺丝。 这也解释了名字:zero 就是"零基础"——这个库是专门为初学者(和树莓派 Zero 那种教学场景)设计的。

二、⚠️ 本节最大的坑:RPi.GPIO 在新板子上用不了了

网上绝大多数中文树莓派教程的开头都是这两行:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

在老板子(Pi 4 及以前)上没问题。但在这台新机器上,它会报错:

RuntimeError: Cannot determine SOC peripheral base address

为什么?因为 Pi 5 换了新的 I/O 芯片(RP1)。

以前 GPIO 的寄存器长在主芯片自己身上,RPi.GPIO 直接去读那块内存就能操作针脚。Pi 5 把 GPIO 交给了一颗专门的外围芯片 RP1 管——主芯片不再直接连针脚了,RPi.GPIO 按老地址去找,扑了个空,于是报"找不到外设基地址"。

⚠️ 三条出路,按情况选
  • 写新代码 → 用 gpiozero(本节和后面五节全部用它)。这是官方推荐,也是本课统一的选择。
  • 手上有一段老的 RPi.GPIO 代码,不想改 → 装一个叫 rpi-lgpio 的包(sudo apt install python3-rpi-lgpio)。它顶上 RPi.GPIO 的名字,让老代码原封不动跑起来(import RPi.GPIO 照写),但内部走的是新接口。
  • 要最大控制力/要性能 → 直接用 lgpio 或 libgpiod(代码会长很多,新手不必)。

记住这个判断原则:凡是你看到教程里出现 GPIO.setmode、GPIO.setup、GPIO.cleanup,就知道这是老一代写法。它在老板子上能用,在新板子上要么换成 gpiozero,要么先装 rpi-lgpio。

三、gpiozero 好在哪:从"五步"到"一行"

同一件事——让 BCM 17 这根针变高,两代写法对比:

老一代(RPi.GPIO)要五步:

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)      # ① 声明用哪套编号
GPIO.setup(17, GPIO.OUT)    # ② 把这根针设成输出
GPIO.output(17, GPIO.HIGH)  # ③ 拉高
GPIO.output(17, GPIO.LOW)   # ④ 拉低
GPIO.cleanup()              # ⑤ 收拾现场(不写会留一堆针在占用状态)

gpiozero 只要一行:

from gpiozero import LED
led = LED(17)               # 建立对象=设成输出并在用完后自动收拾
led.on()                    # 拉高(LED 亮)
led.off()                   # 拉低(LED 灭)

省掉的不是"打字量",而是三类出错机会:

  1. 不用声明编号体系——LED(17) 里的 17 一律是 BCM。
  2. 不用记得 cleanup()——对象不用了会自动释放针(也可以显式 led.close())。
  3. 不用自己管"这根针是什么模式"——你说"这是 LED",它就知道该设输出。

3.1 本章后面要用的"设备类"

gpiozero 的用法就是"先想清楚你接的是什么设备,再选对应的类":

类名 它是干什么的 本章/本课哪里用
LED 一颗灯:亮 / 灭 7-3
PWMLED 一颗灯:能调亮度 7-4
RGBLED 三色灯:能调颜色 7-4
Buzzer 蜂鸣器:响 / 不响 练习用
Button 按钮:按下 / 松开 7-3
MotionSensor 人体红外感应模块 练习用
LightSensor 光敏元件 7-5
MCP3008 / MCP3208 ADC 芯片:把模拟信号读成数字 7-5

看这张表就能明白 gpiozero 的设计哲学:它按"你手上有件什么设备"来组织 API,而不是按"你要操作哪个寄存器"。这才是零基础能上手的原因。

3.2 引脚还能写名字

LED(17) 这种写法里的整数一律按 BCM 解释。除此之外还能写字符串:

led = LED("BOARD11")    # 明确按物理针号
led = LED("GPIO17")     # 明确按 BCM(和写 17 等价)

什么时候用:你照着一张只在针脚上标"11"的接线图接线时,写 "BOARD11" 比把它翻译成 17 更不容易错。

什么时候别用:本章统一写整数 BCM,因为这是社区通用写法,看别人的代码不用二次翻译。

四、pin factory:gpiozero 是怎么"隔空"指挥硬件的

这是 gpiozero 最聪明的一处设计,值得单独看。

gpiozero 自己一点硬件都不碰。它把"真正去操作针脚"这件事,外包给一个叫 pin factory(引脚工厂)的部件。

这带来两个好处:

  1. 它能跨所有树莓派型号(从 Pi 1 到 Pi 5),因为它不写死任何寄存器地址——它只跟"工厂"打交道。
  2. 它能在没有硬件的地方测试(用假工厂),也能换底层实现而不改你的代码。

在你这台机器上,它会自动选对工厂,你不用配置。想确认它选了哪个,一条命令:

python3 -c "import gpiozero; print(gpiozero.Device.pin_factory)"

正常输出:<gpiozero.pins.lgpio.LGPIOFactory object at 0x7f...>

读结果:看到 LGPIOFactory 就对了——这说明它认出了新硬件,正在用新的 lgpio 后端。如果看到 MockFactory,说明它没找到真实后端(那时你的代码跑得起来,但针一点反应都没有——这是最阴的一类问题)。

想手动指定?不用改代码,加个环境变量就行:

GPIOZERO_PIN_FACTORY=lgpio python3 my_script.py

五、权限:你会遇到的第一个报错

第一次跑 GPIO 程序,很可能撞上这个:

PermissionError: [Errno 13] Permission denied: '/dev/gpiochip0'

原因:控制针脚的设备文件(/dev/gpiochip0,上一节 gpiodetect 见过它)属于 root 用户和一个叫 gpio 的组。4-3 讲过 Linux 的权限模型——你的普通用户不在这个组里,就打不开它。

解法(两步):

sudo usermod -aG gpio $USER
sudo reboot

注意两件事:

⚠️ 不要图省事用 `sudo python3 my.py` 绕过去

一是危险:你的脚本一旦有机会写错文件路径(比如那句 rm),root 权限下就没有"删不掉"这一说; 二是会留下麻烦:脚本生成的日志、写出的文件都变成 root 属主,以后普通用户改不动; 三是掩盖问题:真正的原因是"你的用户不在 gpio 组",修它才是正路。 正确习惯:系统配置用 sudo,跑你自己的程序用普通用户。

六、一根针同一时间只能有一个"主人"

这是新接口相比老 RPi.GPIO 的一处重要变化(而且是个好变化):

所以你可能会看到:上一个测试脚本没关掉、还挂在后台,新脚本一跑就报"针被占用"。

排错两招:

ps aux | grep python3     # 看看是不是还有别的 python 脚本在跑

或者重启一次(最省事)。养成习惯:测试脚本用 Ctrl + C 结束,别留着。

七、动手 Demo:库与后端的体检(不接线)

这一步不接任何线,只验证"软件这条路是通的"。(点亮 LED 留在 7-3,因为那一步开始就必须接线了。)

第 1 步 · 确认库在不在,什么版本

dpkg -s python3-gpiozero | grep -E "Package|Version"

你会看到什么:类似 Package: python3-gpiozero + Version: 2.x.x-...。

读结果:有 Version 就说明预装了(这套查法是 4-5 学的 dpkg 用法)。这条命令也顺便告诉你:为什么不用 pip——gpiozero 是系统自带的包。

第 2 步 · 看它选了哪个引脚工厂

python3 -c "import gpiozero; print(gpiozero.Device.pin_factory)"

你会看到什么:<gpiozero.pins.lgpio.LGPIOFactory object at 0x...>(内存地址会不同)。

第 3 步 · 写第一个 GPIO 脚本

nano gpio_hello.py

内容:

from gpiozero import Device, LED

print("引脚工厂:", Device.pin_factory)

led = LED(17)          # BCM 17 = 物理针 11
print("对象已建立:BCM 17(物理针 11)")

led.on()
print("已拉高,读回:", led.value)

led.off()
print("已拉低,读回:", led.value)

led.close()
print("针已释放")

跑:

python3 gpio_hello.py

你会看到什么算成功:

引脚工厂: <gpiozero.pins.lgpio.LGPIOFactory object at 0x7f8a...>
对象已建立:BCM 17(物理针 11)
已拉高,读回: 1.0
已拉低,读回: 0.0
针已释放
💡 `read back` 读到的 `1.0` 和 `0.0` 是什么

它读的是这根针的驱动状态("我现在把它推到高还是低"),不是"外接电路上的电压"。 所以这两个数字能证明你的代码确实指挥到了这根针——但它不能告诉你"灯亮了没有"。灯亮不亮还要看接线对不对、电阻有没有串上、LED 极性有没有反。 要眼见为实,就得接一颗 LED 上去看。下一节就干这件事。

⚠️ 没接线时,别拿 `LED(17)` 去"测试硬件"

光跑脚本不报错,只能说明软件这条路通了。针上到底有没有电压、有没有电流回路,必须靠外接的元件来确认。 新手常犯的一个错:脚本跑通就以为"硬件没问题",然后接线接错了半天找不到原因。软件通过 ≠ 硬件通过,这两件事要分开验。

第 4 步 · 顺手做个"针被占用"实验

再写一个小脚本,让它占住这根针不放:

nano gpio_hold.py

内容:

from gpiozero import LED
import time

led = LED(17)
print("已占住 BCM 17,60 秒内别的程序抢不到")
time.sleep(60)

先让它跑到后台占住针,然后立刻跑上一个脚本:

python3 gpio_hold.py &
python3 gpio_hello.py

你会看到什么:gpio_hello.py 大概率报错,错误里带 PinInUse 或 GPIO busy 字样(不同版本的措辞略有不同)。

读结果:这就是第六节讲的现象——一根针只能有一个主人。看到这个报错,说明你理解了;然后把那个后台脚本收掉:

kill %1

(%1 是"最近那个后台任务"的编号,4-1 讲过作业控制。也可以用 ps aux | grep gpio_hold 找到进程号再 kill。)

八、30 秒实验:一条命令看清三层

python3 -c "
import gpiozero, platform
print('gpiozero 版本:', getattr(gpiozero, '__version__', '未知'))
print('引脚工厂:', gpiozero.Device.pin_factory)
print('机器:', platform.machine())
"

你会看到什么:三行信息——库的版本、工厂类名、CPU 架构。

读结果:这里的"三层"要分清——最上面是你的代码(from gpiozero import LED),中间是gpiozero 库,最下面是工厂 + 内核接口。本节讲的所有"换代",换的其实都是中间和下面这两层,你的代码只写最上面那一层。这就是为什么换个库比换个板子容易得多。

九、常见误区

  1. "网上教程都用 RPi.GPIO,我也照抄" —— 在老板子上可以。在 Pi 5 上会直接报 Cannot determine SOC peripheral base address;要么改用 gpiozero,要么先装 python3-rpi-lgpio 顶上去。
  2. "gpiozero 只是 RPi.GPIO 的新名字" —— 不是。它是完全不同的一层:RPi.GPIO 是"操作针",gpiozero 是"操作设备"。从 GPIO.output(17, 1) 变成 led.on() 不是换了写法,是换了思路。
  3. "跑 GPIO 程序必须 sudo" —— 如果必须 sudo 才能跑,说明你的用户没在 gpio 组里。正确做法是加组 + 重启,不是一直用 root 跑自己的脚本。
  4. "led.value 读回 0 就是针上真的 0V" —— 没有接线的针是浮空的,读回来的值不可靠。要验证输出,就接一颗 LED 上去看。
  5. "装了库就完事了" —— 库只是中间层。针不动的时候,往下要查工厂选对没有(是不是 LGPIOFactory)、用户权限(在不在 gpio 组)、针是否被别的程序占着。按"代码 → 库 → 工厂 → 内核 → 硬件"这个顺序往下查,比乱改代码快得多。
自测三问(点开对答案) 1. 为什么 `RPi.GPIO` 在 Pi 5 上会失效,而在 Pi 4 上好好的? 2. `led = LED(17)` 里的 17,指的是物理针 17 吗? 3. 你的程序报 `PermissionError: [Errno 13] Permission denied: '/dev/gpiochip0'`,最正确的处理是什么? **参考答案** 1. 因为 **Pi 5 换了新的 I/O 芯片(RP1)**:GPIO 不再挂在主芯片上,而 `RPi.GPIO` 是**直接去读主芯片的寄存器地址**的,地址变了它自然找不到(报 `Cannot determine SOC peripheral base address`)。**根本原因是它用了"直接访问内存"这种跟硬件绑得太紧的方式**——这也是官方推荐新代码用 gpiozero 的原因:gpiozero 通过工厂层间接访问,不写死任何地址。 2. **不是。**`17` 是 **BCM 号**,对应的物理针是 **11**。想按物理针号写,得明确写成字符串 `LED("BOARD11")`。(这就是 7-1 讲"三套编号"的原因——**写代码的号和插线的位置,是两个概念。**) 3. **把自己的用户加进 `gpio` 组然后重启**:`sudo usermod -aG gpio $USER` + `sudo reboot`。**不要用 `sudo python3` 绕过去**——那会让脚本带着最高权限跑,还可能留下 root 属主的文件,而且把真正的问题(权限没配好)藏起来了。

相关笔记