上一节的灯只有"亮"和"灭"。这一节让它"半亮"。 但这里有个硬事实:树莓派根本没有能力输出真正的模拟电压——它的针只有 0V 和 3.3V 两种状态。所以本节讲的是一个"骗过眼睛"的巧妙办法:PWM(脉冲宽度调制)。
7-1 说"一根针只能表达 0 或 1",7-3 用这件事点灯和读按钮。本节要处理这个限制带来的第一个矛盾:想要连续的量(亮度),手上却只有开关。
6-4 的 for 循环在本节的渐变 Demo 里第一次用于硬件:代码跑得快、灯变得慢,这个节奏差就是"程序控制现实世界"的核心手感。
一、矛盾:只有开关,怎么表达"半亮"
回到 7-1 的那个结论:一根 GPIO 只能输出两个电压,3.3V(高)或 0V(低)。没有中间值。
而"亮度"是连续的:0%、10%、37%、100%……理论上无穷多种。
用两个值怎么表达无穷多种?有个很巧的办法。
二、PWM:用"极快的开关"骗过眼睛
人眼有个特性叫"视觉暂留"——光消失后,眼睛里的影像还会保留约 1/16 秒。所以如果一个灯每秒开关几百次,你看不到它在闪,只会看到一个"平均亮度"。
这就是 PWM 的全部原理:
- 固定一个周期(比如 1 毫秒),在这个周期里让灯"开一段时间、关一段时间";
- "开"占的比例越大,看起来越亮。
"开"的比例有个正式名字:占空比(duty cycle)。
| 占空比 | 一个周期里 | 平均电压 | 看起来的亮度 |
|---|---|---|---|
| 0% | 全程关 | 0V | 不亮 |
| 25% | 开 1/4、关 3/4 | 0.82V | 微亮 |
| 50% | 一半开一半关 | 1.65V | 半亮 |
| 75% | 开 3/4、关 1/4 | 2.47V | 较亮 |
| 100% | 全程开 | 3.3V | 最亮 |
(这几个数就是下面 Demo 里会打印出来的——它们都是"3.3V 乘占空比"算出来的。)
核心公式只有一条:
平均电压 = 3.3V × 占空比
对 LED 这种"反应很快"的元件来说:它其实一秒被开了又关了 1000 次,但因为它完全没有惯性(不像灯泡需要预热),它输出的光强几乎就正比于占空比。 而人的眼睛又把这个闪烁平均掉了。两头都"模糊"一下,中间就出现了"连续"的错觉。 所以 PWM 并不是"真的输出了 1.65V"——针上的电压每一刻都还是 0V 或 3.3V,只是"平均下来"像 1.65V。
两个理由: 1. 它是一串脉冲,不是平稳的电压。用它给某些电路供电(比如放大器、某些传感器)会出问题——那些东西需要"真的平稳"。 2. 电流上限没变:还是那根针的几毫安。50% 占空比不等于"能用一半的电流去干重活"。 真要输出平稳可调的电压 → 得外接 DAC 芯片(见下一节表格)。
三、树莓派上做"模拟输出"的四条路
先钉一个事实:树莓派没有内置 DAC(数模转换器)——就像它没有内置 ADC(7-5 要讲)一样。原因相同:它的主芯片是"应用处理器",设计目标是跑操作系统,不是精密控制电信号。
所以在这台机器上,"模拟输出"永远是"近似"。常见四条路:
| 路 | 怎么做 | 优点 | 缺点 | 什么时候用 |
|---|---|---|---|---|
| ① 软件 PWM | gpiozero 的 PWMLED |
任意一根针都行、一行代码 | 占 CPU 一点、频率不高、精度一般 | 绝大多数场景(本节用这条) |
| ② 硬件 PWM | 用芯片自带的 PWM 通道 | 极稳定、不占 CPU、频率可很高 | 只有 4 个通道、要额外配置 | 需要精确控制(舵机、音频) |
| ③ 外接 DAC 芯片 | 如 MCP4725(走 I2C) | 输出真正的连续电压 | 多一个元件、要接线 | 音频、精密模拟量输出 |
| ④ RC 低通滤波 | 在 PWM 输出后接电阻 + 电容 | 便宜,能把 PWM 抹平 | 有延迟、带负载能力差 | 教学演示/要求不高的场合 |
关于"硬件 PWM 只有 4 个通道":它们固定在 BCM 12、13、18、19 这几根针上(本课给 LED 挑的就是 BCM 17,不是这几根之一——所以本节用的是软件 PWM,这完全够用)。硬件 PWM 的配置在新系统上和老板子不一样,本章不展开,你只要记住"有这么一条更精确的路,等真的需要精确时再去查"。
四、PWMLED:会调亮度的灯
用法和 LED 几乎一样,区别只有一处:value 是个 0 到 1 之间的小数。
from gpiozero import PWMLED
led = PWMLED(17)
led.value = 0.5 # 半亮
led.value = 1.0 # 最亮(等同 led.on())
led.value = 0.0 # 全灭(等同 led.off())
另外两个现成的动作:
led.pulse() # 呼吸灯:亮度来回渐变,默认在后台一直跑
led.blink(n=3) # 闪烁(和 7-3 的 LED 一样能用)
还可以调频率(周期长短):
led = PWMLED(17, frequency=1000) # 每秒 1000 个周期
要不要调?默认值(100Hz)对 LED 调光就够。把频率调高会让软件 PWM 更吃 CPU、也更容易抖动——除非你有具体理由(比如要消掉可见的闪烁、或驱动某些元件),否则保持默认。
软件 PWM 是靠 Python 定时去开关针的。如果这时候系统忙(比如另一个程序在抢 CPU、或者你在读传感器),开关的时间点会不准,现象是灯偶尔轻微一跳。
这是它和硬件 PWM 的本质区别:硬件 PWM 由芯片自己产生波形,CPU 完全不管,所以稳如钟表。
对调光、呼吸灯这类应用,软件的抖动完全看不出来,用 PWMLED 就够了。
五、RGBLED:三个 PWM 合成一个颜色
三色 LED 里面其实就是三颗灯(红、绿、蓝),共用一根正极或负极。所以控制三色 = 同时控制三条 PWM。
from gpiozero import RGBLED
led = RGBLED(red=17, green=27, blue=22)
led.color = (1, 0, 0) # 纯红
led.color = (1, 1, 0) # 红 + 绿 = 黄
led.color = (1, 0.5, 0) # 橙色(红满、绿一半)
led.color = (1, 1, 1) # 三个全开 = 白
规矩一句话:三个数各表示"那一色有多亮"(0~1),加起来就是颜色。这正是显示器混色的方式("加法混色":红绿蓝叠起来变白)。
三颗灯必须共用一个电极:共用负极的叫"共阴极",共用正极的叫"共阳极"。
共阳的那种是"反的"——你得用 RGBLED(red=17, green=27, blue=22, active_high=False),否则会出现"要亮反而灭"的现象。
买的时候看模块说明;接上后颜色全反,就是这个原因。
六、动手 Demo:调光三步(接线不用改)
注意:本节不用动接线。上一节那颗 LED(BCM 17 经 220Ω 到 GND)原样就行——这正是 PWM 的好处:它提高的是软件能力,不要求你多加任何元件。
第 1 步 · 定几个亮度看一看
nano pwm_level.py
from gpiozero import PWMLED
import time
led = PWMLED(17)
for level in [0.0, 0.25, 0.5, 0.75, 1.0]:
led.value = level
print(f"亮度 {level:.2f} → 平均电压约 {3.3 * level:.2f}V")
time.sleep(1.5)
led.off()
led.close()
跑:python3 pwm_level.py
看到什么算成功:灯依次呈现"全灭 → 微亮 → 半亮 → 较亮 → 最亮",每档停 1.5 秒,屏幕同步打印。
亮度 0.00 → 平均电压约 0.00V
亮度 0.25 → 平均电压约 0.82V
亮度 0.50 → 平均电压约 1.65V
亮度 0.75 → 平均电压约 2.47V
亮度 1.00 → 平均电压约 3.30V
第 2 步 · 渐变(真正的"渐变")
nano pwm_fade.py
from gpiozero import PWMLED
import time
led = PWMLED(17)
print("渐亮…")
for i in range(0, 101, 5): # 0, 5, 10, … 100
led.value = i / 100
time.sleep(0.05)
print("渐暗…")
for i in range(100, -1, -5): # 100, 95, … 0
led.value = i / 100
time.sleep(0.05)
led.off()
led.close()
print("完成")
看到什么算成功:灯用约 1 秒平滑地亮起来,再用约 1 秒暗下去。
读结果:注意这个"节奏差"——range(0,101,5) 有 21 步,每步等 0.05 秒,总共才 1 秒左右。代码"跑得很快",但灯"变得很慢",全靠 sleep 把时间拉开。
这是程序控制现实世界最核心的一件事:现实世界的节奏由 sleep 决定,代码的节奏由 CPU 决定。(range(100, -1, -5) 这个"倒着数"的写法见 6-4。)
第 3 步 · 呼吸灯(一行搞定)
nano pwm_breathe.py
from gpiozero import PWMLED
from signal import pause
led = PWMLED(17)
print("呼吸灯已启动(后台运行)。Ctrl+C 退出。")
led.pulse(fade_in_time=1, fade_out_time=1) # 1 秒亮起、1 秒暗下,不断循环
pause()
跑:python3 pwm_breathe.py,按 Ctrl + C 结束。
看到什么算成功:灯像呼吸一样平滑地亮暗循环。
读结果:pulse() 一行就是刚才那段渐变代码的"包装版"——它内部就是循环改 value,只是 gpiozero 帮你写好了、还放在后台跑(所以 pause() 不必等它)。这就是库的价值:你描述"我想要什么效果",而不是"怎么一步步做"。
七、30 秒实验:把"占空比"和"平均电压"一个个对出来
python3 -c "
for i in range(0, 11):
v = i / 10
print(f'占空比 {v*100:5.1f}% → 平均电压 {3.3*v:.2f}V')
"
你会看到什么:
占空比 0.0% → 平均电压 0.00V
占空比 10.0% → 平均电压 0.33V
占空比 20.0% → 平均电压 0.66V
占空比 30.0% → 平均电压 0.99V
占空比 40.0% → 平均电压 1.32V
占空比 50.0% → 平均电压 1.65V
占空比 60.0% → 平均电压 1.98V
占空比 70.0% → 平均电压 2.31V
占空比 80.0% → 平均电压 2.64V
占空比 90.0% → 平均电压 2.97V
占空比 100.0% → 平均电压 3.30V
读结果:这就是 PWMLED 里 value 背后的账——value = 0.5 其实就是"占空比 50%",换算成平均电压是 1.65V(3.3 的一半)。
再想一步:如果想让 LED 的平均电压正好是 2.0V,value 该写多少?——2.0 ÷ 3.3 ≈ 0.61。这条除法就是"模拟输出"的全部数学。
八、常见误区
- "树莓派能输出 0~3.3V 之间的任意电压" —— 不能。它只有 0V 和 3.3V 两种输出(没有内置 DAC)。你看到的"中间亮度"是 PWM 的平均效果,不是真的输出了中间电压。
- "PWM 输出 50% 就等于一个 1.65V 的电源" —— 不等于。它输出的是一串脉冲,只有"平均"是 1.65V;而且电流能力还是那根针的几毫安。要真平稳的电压就得用 DAC 芯片。
- "调光必须换一种 LED" —— 不用。上一节那颗普通 LED 原样就能调光,接线一根都不用动。这是 PWM 最漂亮的地方。
- "软件 PWM 和硬件 PWM 差不多" —— 平时差不多,但软件 PWM 会抖、占 CPU,而且通道数不限;硬件 PWM 只有 4 个通道(BCM 12/13/18/19)但稳如钟表。要求精确(舵机角度、音频)时必须用硬件 PWM。
- "
pulse()会挡住程序" —— 不会。它默认在后台跑(所以后面要跟pause()让它一直待着)。同理blink()默认也是后台的——想让它"闪完才继续",得写background=False。 - "三色 LED 直接按红绿蓝接线就行" —— 要看它是共阴还是共阳。共阳的那种颜色是反的,必须加
active_high=False。 - "占空比越高越省电" —— 对 LED 来说,占空比越低确实平均功耗越低;但这不是一个"节能手段",它只是调光方式。别为了省电把灯调暗到看不见。
自测三问(点开对答案)
1. 树莓派能不能输出 1.65V 这样一个"中间"的电压?为什么? 2. `PWMLED(17).value = 0.25` 时,这根针上的电压实际是什么?"平均电压"是多少? 3. 为什么调光**不需要改接线**,而 7-5 读模拟信号却**必须加芯片**? **参考答案** 1. **不能直接输出。**因为树莓派的 GPIO 是**纯数字的,它没有内置 DAC(数模转换器)**,一根针只能给出 0V 或 3.3V。 **但可以让它"看起来像 1.65V"**:用 **PWM**——让针以很快的速度开关,**50% 的时间开、50% 的时间关**,平均下来就是 1.65V。**这是"平均",不是"真的输出"。** 2. **针上的电压每一刻都还是 0V 或 3.3V 两者之一**(它在飞快地开关,25% 的时间是 3.3V)。**"平均电压"是 3.3 × 0.25 ≈ 0.82V。**(这个"平均"对 LED 的亮度是有效的,但**不能拿它当电源给别的电路供电**。) 3. 因为**调光靠的是"时间"而不是"电压"**——LED 还是那颗 LED,针还是那根针,**只是开关的节奏变了**,所以一根线都不用动。 而 **7-5 要读的是"外面那个电压到底是多少"**,这需要**测量**——树莓派**没有内置 ADC(模数转换器)**,所以**必须外接一颗 ADC 芯片**才能把模拟电压变成数字送给它。**输出可以"骗",输入没法"骗"。**- ☐ 下一节(7-5 模拟输入) —— 反过来的难题:怎么让树莓派"读"一个连续变化的电压,以及为什么这一步必须买芯片
- ☑ 本章第四节已读 —— 理解了 PWM 与占空比,能让灯"半亮"了
相关笔记
- 数字输出与输入 —— 上一节:LED 与按钮的接线,本节沿用那套接法
- 认识树莓派的 GPIO 引脚 —— "一根针只有两种状态"这个前提在那里的第一节
- Python 流程控制 —— 渐变 Demo 里的
for、range与"倒着数"的写法 - Python 函数与模块 ——
pulse()/blink()为什么"不挡住程序":后台线程的概念 - 🍓 树莓派 · 索引 —— 全课路线图