一 本章简介
本章介绍如何使用 MicroPython 的定时器和 PWM 功能,在筑基学习板的 LED(PB8)上实现呼吸灯效果。呼吸灯是 PWM 最经典的入门实验——通过不断改变 PWM 占空比来平滑调节 LED 亮度,让 LED 像呼吸一样渐亮渐灭。
1.1 学习目标
| 序号 | 学习目标 | 重要程度 |
|---|---|---|
| 1 | 理解 PWM 的基本概念(频率、占空比) | ⭐⭐⭐⭐⭐ |
| 2 | 掌握 MicroPython 中 pyb.Timer 配置 PWM 输出的方法 | ⭐⭐⭐⭐⭐ |
| 3 | 理解低电平点亮 LED 时占空比与亮度的反向关系 | ⭐⭐⭐⭐⭐ |
| 4 | 能够实现线性呼吸灯和非线性(视觉均匀)呼吸灯效果 | ⭐⭐⭐⭐ |
1.2 重点提示
- 筑基学习板 LED 连接在 PB8,低电平点亮。因此 PWM 占空比越低,LED 越亮;占空比越高,LED 越暗。这与高电平点亮的 LED 逻辑相反,代码中需要注意。
- PB8 支持 TIM10_CH1(AF3)的 PWM 输出,本章使用该定时器通道。选择 TIM10 而非 TIM4 是因为 TIM4 在后续编码器章节中会被用于编码器模式,两者不能同时使用。
- PWM 频率建议设置在 500Hz~2kHz 之间。频率太低(如 50Hz 以下)人眼会看到明显闪烁;频率太高则对 LED 亮度调节没有额外好处。
- 人眼对亮度的感知是非线性的(韦伯-费希纳定律),线性改变占空比时,低亮度区域变化不明显,高亮度区域变化过快。进阶部分会介绍 Gamma 校正方案。
1.3 基础概念与术语
- PWM(Pulse Width Modulation,脉冲宽度调制):通过快速切换高低电平,并改变高电平所占的时间比例(占空比),来等效地控制输出的平均电压。
- 占空比(Duty Cycle):一个 PWM 周期内,高电平持续时间占整个周期的百分比。0% = 全低电平,100% = 全高电平。
- 频率(Frequency):PWM 信号每秒切换的次数。频率越高,LED 闪烁越快,人眼越感觉不到闪烁。
- 定时器通道(Timer Channel):STM32 的每个定时器有多个通道,每个通道可以独立输出 PWM 信号,但共享同一个频率(由定时器的 ARR 和 PSC 决定)。
二 PWM 工作原理
2.1 什么是 PWM

PWM(Pulse Width Modulation,脉宽调制)是一种在嵌入式系统中常用的技术,它可以用来模拟信号,控制设备的功率输出或者实现对设备的精确控制。PWM信号是一种类似于方波的信号,具有固定的频率,但脉冲宽度(占空比)可以调整。在一定频率下,我们可以通过调整这个占空比来改变他的有效电压,在一定程度上可以实现D/A转换,
- 频率(Frequency):指PWM信号在一秒内循环的次数。频率是周期的倒数,单位是赫兹(Hz)。
- 周期(Period):指一个完整的PWM信号的时间长度,与频率成反比。单位是秒(s)。
- 脉宽(Pulse Width):指PWM信号中高电平(通常为1)的时间长度。单位是秒(s)或毫秒(ms)。
- 占空比(Duty Ratio):表示在一个完整的PWM信号周期内,高电平(通常为1)所占的时间比例。占空比 = (脉宽 / 周期)x 100%。
- 上升沿(Rising Edge):PWM信号从低电平跳变到高电平的瞬间,通常用来作为触发事件。
- 下降沿(Falling Edge):PWM信号从高电平跳变到低电平的瞬间,也常被用作触发事件。
- 正脉冲宽度(Positive Pulse Width):PWM信号中高电平的持续时间,一般情况下的脉宽指的就是这个。
- 负脉冲宽度(Negative Pulse Width):PWM信号中低电平的持续时间。
PWM 的核心思想很简单:用数字信号(只有高和低两种电平)来模拟模拟信号(连续变化的电压)。
假设 PWM 频率为 1kHz(周期 1ms),占空比为 30%:
- 每个周期内,前 0.3ms 输出高电平(3.3V),后 0.7ms 输出低电平(0V)
- 平均电压 = 3.3V × 30% = 0.99V
- LED 感受到的等效电压约为 1V,亮度约为最大亮度的 30%
2.2 PB8 低电平点亮的特殊性
PB8 的 LED 是低电平点亮(灌电流接法),所以:
| 占空比 | 高电平时间 | 低电平时间 | LED 状态 |
|---|---|---|---|
| 0% | 无 | 全部 | 最亮 |
| 50% | 一半 | 一半 | 中等亮度 |
| 100% | 全部 | 无 | 全灭 |
WARNING
PB8 低电平点亮,占空比与亮度是反向关系。写代码时要特别注意:想让 LED 变亮,应该降低占空比;想让 LED 变暗,应该升高占空比。
三 软件设计
3.1 基础部分
3.1.1 固定亮度输出
先用 PWM 输出一个固定占空比,验证硬件和配置是否正确,注意这里是占空比越大,LED灯反而越暗:
# PWM 固定亮度输出
from pyb import Pin, Timer
# 配置 PB8 为 TIM10_CH1 的 PWM 输出
pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim = Timer(10, freq=1000) # PWM 频率 1kHz
ch = tim.channel(1, Timer.PWM, pin=pin)
# 低电平点亮:占空比 0% = 最亮,100% = 全灭
ch.pulse_width_percent(80) # 较暗
# ch.pulse_width_percent(20) # 较亮
# ch.pulse_width_percent(0) # 最亮
# ch.pulse_width_percent(100) # 全灭2
3
4
5
6
7
8
9
10
11
12
13
3.1.2 线性呼吸灯
最基本的呼吸灯——线性改变占空比:
# 线性呼吸灯(PB8,低电平点亮)
from pyb import Pin, Timer
import time
pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim = Timer(10, freq=1000)
ch = tim.channel(1, Timer.PWM, pin=pin)
print("呼吸灯启动")
try:
while True:
# 渐亮:占空比从 100%(灭)降到 0%(最亮)
for duty in range(100, -1, -1):
ch.pulse_width_percent(duty)
time.sleep_ms(15)
# 渐灭:占空比从 0%(最亮)升到 100%(灭)
for duty in range(0, 101):
ch.pulse_width_percent(duty)
time.sleep_ms(15)
except KeyboardInterrupt:
ch.pulse_width_percent(100) # 熄灭
tim.deinit()
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
3.1.3 调节呼吸速度
通过改变每步的延时时间来控制呼吸速度:
# 可调速呼吸灯
from pyb import Pin, Timer
import time
pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim = Timer(10, freq=1000)
ch = tim.channel(1, Timer.PWM, pin=pin)
STEP_MS = 10 # 每步延时(ms),越小呼吸越快
STEP = 2 # 每步占空比变化量,越大呼吸越快
try:
while True:
for duty in range(100, -1, -STEP):
ch.pulse_width_percent(duty)
time.sleep_ms(STEP_MS)
for duty in range(0, 101, STEP):
ch.pulse_width_percent(duty)
time.sleep_ms(STEP_MS)
except KeyboardInterrupt:
ch.pulse_width_percent(100)
tim.deinit()
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
3.2 进阶部分
3.2.1 Gamma 校正呼吸灯(视觉均匀)
人眼对亮度的感知是非线性的——在低亮度区域,微小的亮度变化就能被察觉;在高亮度区域,较大的变化才能被感知。线性改变占空比时,你会发现 LED 在暗的时候变化很快,亮的时候变化很慢,呼吸效果不够平滑。
通过 Gamma 校正(通常 γ=2.2),可以让亮度变化在视觉上更加均匀:
# Gamma 校正呼吸灯(视觉均匀)
from pyb import Pin, Timer
import time
import math
pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim = Timer(10, freq=1000)
ch = tim.channel(1, Timer.PWM, pin=pin)
GAMMA = 2.2
STEPS = 100
# 预计算 Gamma 查找表(节省运行时计算开销)
# 输入 0~100 的线性值,输出 0~100 的 Gamma 校正后占空比
gamma_table = []
for i in range(STEPS + 1):
# 线性亮度 0~1
brightness = i / STEPS
# Gamma 校正后的占空比(低电平点亮,所以取反)
corrected = 100 - int(math.pow(brightness, GAMMA) * 100)
gamma_table.append(corrected)
print("Gamma 校正呼吸灯启动(γ={})".format(GAMMA))
try:
while True:
# 渐亮
for i in range(STEPS + 1):
ch.pulse_width_percent(gamma_table[i])
time.sleep_ms(15)
# 渐灭
for i in range(STEPS, -1, -1):
ch.pulse_width_percent(gamma_table[i])
time.sleep_ms(15)
except KeyboardInterrupt:
ch.pulse_width_percent(100)
tim.deinit()
print("已停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
3.2.2 定时器回调驱动呼吸灯(非阻塞)
前面的呼吸灯都是在主循环中用 sleep 阻塞实现的,主循环被占满了。使用定时器回调可以让呼吸灯在后台自动运行,主循环可以做其他事情:
# 定时器回调驱动呼吸灯(非阻塞)
from pyb import Pin, Timer
pin = Pin('PB8', Pin.AF_PP, af=3) # AF3 = TIM10
tim_pwm = Timer(10, freq=1000)
ch = tim_pwm.channel(1, Timer.PWM, pin=pin)
# 呼吸状态
duty = [100] # 当前占空比(用列表包装,回调中可修改)
direction = [-1] # -1=渐亮,+1=渐灭
def breath_callback(t):
duty[0] += direction[0] * 2
if duty[0] <= 0:
duty[0] = 0
direction[0] = 1
elif duty[0] >= 100:
duty[0] = 100
direction[0] = -1
ch.pulse_width_percent(duty[0])
# 用另一个定时器(TIM2)驱动呼吸节奏,频率 60Hz
tim_breath = Timer(2, freq=60)
tim_breath.callback(breath_callback)
print("呼吸灯在后台运行,主循环可以做其他事情")
print("输入 tim_breath.callback(None) 停止")2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
四 常见问题
Q: LED 没有呼吸效果,一直亮或一直灭?
- 确认引脚配置正确:
Pin('PB8', Pin.AF_PP, af=3),注意是AF_PP不是OUT_PP,AF3 对应 TIM10。 - 确认定时器和通道号:TIM10 的 CH1 对应 PB8。
- 在 REPL 中手动测试:
ch.pulse_width_percent(50)看 LED 是否处于中等亮度。
Q: 呼吸效果不平滑,有明显的阶梯感?
- 减小每步的占空比变化量(如从 2 改为 1)
- 减小每步的延时时间(如从 20ms 改为 10ms)
- 使用 Gamma 校正方案(3.2.1 节),让视觉亮度变化更均匀
Q: 为什么占空比 0% 是最亮,100% 是最暗?
因为 PB8 是低电平点亮。占空比 0% 意味着输出全部是低电平,LED 全亮;占空比 100% 意味着输出全部是高电平,LED 全灭。
Q: 可以用天空星核心板的 PB2 做呼吸灯吗?
PB2 不支持硬件 PWM 输出(没有对应的定时器通道),所以无法用硬件 PWM 做呼吸灯。可以用软件模拟 PWM,但效果不如硬件 PWM 平滑。
五 本节参考文档
- MicroPython pyb.Timer 文档:
- MicroPython pyb.Pin 文档:
- 天空星硬件资料(原理图):