MicroPython pyboard 教程精讲:使用 PWM 实现 LED 渐亮与呼吸灯效果
2026/9/20 20:56:36 网站建设 项目流程

MicroPython pyboard 教程精讲:使用 PWM 实现 LED 渐亮与呼吸灯效果

【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython

本教程源自 docs/pyboard/tutorial/fading_led.rst,是 MicroPython pyboard 官方入门教程系列中关于脉宽调制(PWM)输出的核心实战指南。你将学会如何用一条数字引脚输出可变的平均电平,从而平滑地控制 LED 亮度,实现"渐亮"与"呼吸灯"两种经典效果;同时文章会结合仓库源码,带你理解 PWM 在 STM32 定时器层级的真实实现原理,为后续用 PWM 驱动舵机、调光、发声等应用打下基础。

为什么需要 PWM:数字引脚也能输出"模拟亮度"

LED 最简单的玩法是用数字引脚拉高/拉低,实现亮与灭的开关控制。但数字引脚只有 0 和 1 两个状态,无法表达"半亮"。解决思路是Pulse-Width Modulation(脉宽调制):让引脚在一个固定周期内,一部分时间输出高电平、一部分时间输出低电平。只要切换频率足够高,人眼感知到的就是介于全亮与全灭之间的平均亮度——高电平占比(占空比)越高,LED 越亮

这正是本教程的出发点:在"只开关 LED"的基础上,通过控制占空比实现任意亮度,从而做出渐亮动画。

硬件清单与电路搭建

本教程需要的元件非常少:

  • 标准5 mm 或 3 mm 直插 LED一颗;
  • 100 Ω 限流电阻一颗(防止流过 LED 的电流过大);
  • 若干导线
  • 面包板(可选,但能让接线更整洁、更易复现)。

接线方式(本教程使用X1引脚):

  1. 将电阻的一端接到X1
  2. 电阻的另一端接到 LED 的阳极(anode)——即较长的那根引脚
  3. 将 LED 的阴极(cathode)接到板子的GND(地)。

下面这张面包板接线图来自仓库 docs/pyboard/tutorial/img/fading_leds_breadboard_fritzing.png,直观展示了完整的连接拓扑:

提示:接线方向很关键——LED 是单向导电器件,接反了不会发光。认准"长脚为阳极"这一经验法则即可。

认识 X1 引脚:为什么它能输出 PWM

不是所有引脚都适合做 PWM 输出。在 STM32 上,PWM 信号由硬件定时器的通道产生,因此必须选择"引脚 → 定时器通道"这一映射关系成立的组合。

根据 docs/pyboard/quickref.rst 的说明,X1连接到定时器 5 的通道 1(TIM5 CH1)。在仓库中可以找到对应证据:

  • ports/stm32/boards/PYBV11/pins.csv 第 1 行:X1,PA0,即 pyboard v1.1 的X1对应 STM32 的PA0引脚;
  • ports/stm32/boards/stm32f405_af.csv 第 3 行:PA0引脚的复用功能(Alternate Function)表中同时包含TIM2_CH1/TIM2_ETRTIM5_CH1

所以PA0是一个"一孔多能"的引脚——quickref 中 PWM 示例用的是Timer(2),而本教程选择Timer(5),两者都是X1合法的硬件映射。这正体现了定时器与引脚的"可选复用"特性:同一引脚可以绑定到不同定时器的对应通道。

核心代码:用 Timer + TimerChannel 输出 PWM

明确了硬件映射后,代码分两步走:先创建定时器对象,再为指定通道创建TimerChannel对象并配置 PWM 模式。

from pyb import Timer from time import sleep # timer 5 以 100 Hz 的频率工作 tim = Timer(5, freq=100) tchannel = tim.channel(1, Timer.PWM, pin=pyb.Pin.board.X1, pulse_width=0)

注意:原文档此处写作tim = pyb.Timer(5, freq=100),但脚本开头已通过from pyb import Timer导入,pyb这个名字并未被导入,实际运行时应写为Timer(5, freq=100)或把导入改为import pyb。以下代码均按可运行版本给出。

代码要点:

  • Timer(5, freq=100):将定时器 5 配置为100 Hz,即每个 PWM 周期为1 / 100 = 0.01 s(10 ms);
  • tim.channel(1, Timer.PWM, pin=pyb.Pin.board.X1, pulse_width=0):取通道 1,模式为Timer.PWM,输出引脚绑定X1,初始脉宽为 0(LED 初始全灭)。

脉宽(pulse_width)是什么

在 PWM 模式下,LED 的亮度由脉宽决定——即每个周期内 LED 处于点亮状态的时间长度。定时器频率为 100 Hz 时,每个周期 10 ms,脉宽就是这 10 ms 内高电平占用的时长。

从源码看,脉宽的单位是定时器时钟的 tick,而不是纳秒/微秒。相关证据在 ports/stm32/timer.c:

  • 第 1647~1658 行注释明确说明:capturecomparepulse_width同一个底层函数的不同别名,在 PWM 模式下应当使用pulse_width这个语义名;
  • 第 1660~1661 行注释给出换算关系:边沿对齐(edge aligned)模式下,pulse_width = period + 1时占空比为 100%;中心对齐模式下,pulse_width = period时占空比为 100%。

也就是说,占空比 =pulse_width / (period + 1)。以 pyboard 的定时器时钟(APB 总线时钟,其计算逻辑见 ports/stm32/timer.c 第 354~429 行的注释)可以推断:100 Hz 对应约 84 万个 tick 的周期,因此教程中的min_width = 20000对应约 2.4% 的占空比(暗光),max_width = 200000对应约 23.8% 的占空比(较亮,但远未到全亮),为渐亮动画留足了动态范围。

实现渐亮动画(Fading Effect)

掌握了脉宽与亮度的关系,渐亮动画就顺理成章:从最小脉宽开始,每个循环把脉宽增加一个固定步长,超过最大值后回到最小值,周而复始:

# 最大和最小脉宽,分别对应最大和最小亮度 max_width = 200000 min_width = 20000 # 每一步脉宽的改变量 wstep = 1500 cur_width = min_width while True: tchannel.pulse_width(cur_width) # 决定多久改变一次脉宽,相当于动画的"帧率" sleep(0.01) cur_width += wstep if cur_width > max_width: cur_width = min_width

运行后即可看到 LED 从暗到亮、然后瞬间跳回最暗、再次渐亮的循环效果。其中:

  • tchannel.pulse_width(cur_width)是每次循环的核心调用,它直接写定时器通道的比较寄存器(对应 ports/stm32/timer.c 第 1662~1672 行的pyb_timer_channel_capture_compare实现,最终调用__HAL_TIM_SET_COMPARE);
  • sleep(0.01)相当于动画的帧间隔:配合 1500 的步长,从 20000 走到 200000 大约需要 120 帧、约 1.2 秒,形成肉眼舒适平滑的渐变。

进阶:呼吸灯效果(Breathing Effect)

"渐亮后瞬间熄灭"是循环动画,而"渐亮 → 渐暗 → 渐亮"的往复效果更像呼吸。实现方法只需一个技巧:在到达亮度上限时反转wstep的符号,到达下限时再次反转。

while True: tchannel.pulse_width(cur_width) sleep(0.01) cur_width += wstep if cur_width > max_width: cur_width = max_width wstep *= -1 elif cur_width < min_width: cur_width = min_width wstep *= -1

与前一版代码相比只多了两处边界处理:

  • 超过max_width时,把脉宽钳制在max_width并让wstep取反,亮度开始下降;
  • 低于min_width时,把脉宽钳制在min_width并再次取反,亮度重新上升。

如此往复,LED 亮度呈现正弦般平滑的"呼吸"节奏。

为什么"亮得快、灭得慢":韦伯定律与指数补偿

细心的读者运行呼吸灯代码后会注意到:LED 似乎从暗到亮的变化更快,从亮到暗的变化更慢。这不是代码 bug,而是人眼的感知特性造成的错觉。

人眼对亮度的感知不是线性的,而是对数的——这被称为韦伯定律(Weber's Law)(相关背景资料见原文档引用的 telescope-optics.net 亮度响应讨论)。也就是说,人眼对"暗处的微小变化"敏感,对"亮处的同等幅度变化"不敏感;而我们的代码让脉宽等步长线性增减,实际亮度增量恒定,映射到人眼感知后就显得不均匀了。

原文档给出的思考题是:对数函数的反函数是什么?答案是指数函数(y = e^x)。要做感知均匀的渐变,应当让脉宽按指数曲线变化,这样人眼感知到的亮度增量才是近似均匀的。参考解法:

from pyb import Timer from time import sleep import math tim = Timer(5, freq=100) tchannel = tim.channel(1, Timer.PWM, pin=pyb.Pin.board.X1, pulse_width=0) max_width = 200000 min_width = 20000 steps = 100 ratio = max_width / min_width # 指数曲线的动态范围 while True: # 先上升:指数插值 for i in range(steps): cur = int(min_width * (ratio ** (i / steps))) tchannel.pulse_width(cur) sleep(0.01) # 再下降:反向指数插值 for i in range(steps - 1, -1, -1): cur = int(min_width * (ratio ** (i / steps))) tchannel.pulse_width(cur) sleep(0.01)

用指数曲线驱动脉宽后,"呼吸"节奏在感知上会更加均匀自然。这是一个非常典型的"硬件线性输出 vs 感官非线性感知"的工程折中问题,也是本教程最有价值的引申思考。

备选方案:DAC 与 PWM 的取舍

原文档的附注(Addendum)还提到另一种实现途径:数字模拟转换器(DAC)。pyboard 的X5引脚支持 DAC,在 docs/pyboard/quickref.rst 中有对应示例:

from pyb import Pin, DAC dac = DAC(Pin('X5')) dac.write(120) # 输出范围 0~255

两种方案各有利弊:

  • DAC 方案:直接输出连续的模拟电压,代码上更直观;但 DAC 输出的是电压,LED 的电流取决于电路,且 DAC 通道数量有限;
  • PWM 方案(本教程采用):每次都以相同的电流驱动 LED,只是点亮的时间长短不同(见原文档 Addendum 的说明)。由于 LED 的亮度与驱动电流之间未必呈线性关系,PWM 这种"恒定电流 + 可变时长"的方式反而能带来更精确、更一致的亮度控制效果。

在实际工程中,PWM 也是驱动 LED、舵机、蜂鸣器等最常见的手段,通用性更强。

源码视角:PWM 的完整配置面与底层机制

最后,把教程里的 API 放到 ports/stm32/timer.c 的源码语境中,可以看清它的完整能力面:

1.Timer.channel()的关键字参数(timer.c 第 1164~1213 行)

参数含义
mode必填,Timer.PWM即本教程使用的脉宽调制模式
pin输出引脚,不传则保持None;传入时会为该引脚配置对应定时器通道的复用功能,引脚与定时器不匹配会报错
pulse_width初始脉宽(tick 单位,整数)
pulse_width_percent初始脉宽(占空比百分比,0~100,支持浮点,如 25 表示 25% 占空比)
pulse_width_us初始脉宽(微秒)
pulse_width_ns初始脉宽(纳秒)
callback通道回调函数

2. 脉宽的多种设置方式(timer.c 第 1675~1725 行)

除了教程使用的pulse_width()TimerChannel还提供pulse_width_percent()pulse_width_us()pulse_width_ns(),均可"不传参读取、传参设置"。其中pulse_width_percent的实现(compute_percent_from_pwm_value/compute_pwm_value_from_percent)会把 0~100 的百分比换算成对应的比较寄存器值,适合需要"直接按占空比调光"的场景。比如 quickref 中的写法:

p = Pin('X1') # X1 同时具备 TIM2 CH1 的复用功能 tim = Timer(2, freq=1000) ch = tim.channel(1, Timer.PWM, pin=p) ch.pulse_width_percent(50) # 直接设为 50% 占空比

3. 定时器频率如何落地(timer.c 第 449~498 行)

Timer(5, freq=100)的 100 Hz 并非直接写入硬件,而是由compute_prescaler_period_from_freq()根据定时器输入时钟,计算出一组预分频系数(prescaler)与计数周期(period),再配置到硬件定时器寄存器。这也是为什么脉冲宽度的最大值天然受period约束——脉宽超过period + 1后占空比将超过 100%,实际效果等同恒高电平,所以教程用 200000 作为安全上限而不是更大值。

总结

本教程用最少的硬件(一颗 LED、一个电阻、几根线)演示了 MicroPython 最具代表性的硬件控制能力之一——PWM 脉宽调制输出。回顾要点:

  1. 选对引脚:PWM 必须使用与定时器通道存在硬件映射的引脚,X1(PA0)的复用功能同时支持TIM2_CH1TIM5_CH1
  2. 两个对象Timer(决定频率/周期)与TimerChannel(决定脉宽/占空比),脉宽本质是定时器 tick 数,占空比 =pulse_width / (period + 1)
  3. 三种动画:线性渐亮、往返呼吸灯、基于指数曲线的感知均匀渐变;
  4. 两种输出方式:PWM(恒定电流 + 可变时长)与 DAC(可变电压),本场景 PWM 更优。

继续深入可阅读同目录下的 pyboard 快速参考、pyb.Timer 模块文档 以及定时器完整实现的 ports/stm32/timer.c。掌握了 PWM 之后,下一个自然的进阶方向是用相同思路驱动舵机角度或生成 PWM 音频信号。

【免费下载链接】micropythonMicroPython - a lean and efficient Python implementation for microcontrollers and constrained systems项目地址: https://gitcode.com/gh_mirrors/mi/micropython

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询