ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

树莓派Pico定时器与PWM详解:从RP2040原理到MicroPython实战

树莓派Pico定时器与PWM详解:从RP2040原理到MicroPython实战 1. 内容整体设计与思路拆解1.1 为什么树莓派 Pico 的定时器值得单独拿出来写一篇很多玩单片机的朋友一开始接触树莓派 Pico都是把它当一块“能跑 MicroPython 的高级 Arduino”来用。点亮 LED、读个温湿度、控制个舵机确实上手极快。但等到你想做精确的时间控制比如让一个电机精准地转 0.5 秒、采集 PWM 信号的频率、或者让多路输出严格保持相位关系单纯靠time.sleep()或者while循环轮询就完全不够用了。time.sleep()的精度在毫秒级还算凑合但到了微秒级别就纯粹靠运气因为 MicroPython 的解释执行本身就有不确定的指令开销。这时候你就需要真正理解 Pico 这颗 RP2040 芯片上的定时器硬件它是怎么工作的、有哪些工作模式、如何配置才能发挥最大价值。这篇文章我从硬件寄存器层面讲到 MicroPython 的实际调用把定时器的原理和几种典型工作模式一次说透希望能帮你省下自己翻手册、试错的时间。1.2 这颗芯片上到底藏着几个定时器先说结论RP2040 芯片上一共有两类和“定时”相关的硬件。第一类是Timer它本质是一个 64 位向上计数的计数器时钟源默认是系统主频可以配置分频它的作用偏向“绝对时间测量”比如记录程序运行了多久、某个事件发生时的精确时间戳。MicroPython 的machine.Timer就是基于它实现的。第二类是PWM 模块RP2040 有 8 个 PWM slice每个 slice 带两个输出通道A/B每个 PWM 模块内部都有一个 16 位的计数器。虽然名字叫 PWM但你完全可以把它当作定时器来用比如输出指定频率的方波、测量外部信号的脉宽、甚至捕获脉冲计数。很多新手不知道的是Pico 的 PWM 模块支持电平捕获模式Input mode这在用 Pico 做频率计、占空比分析时非常有用。搞懂这两类硬件的区别你才能在做项目时选对工具。需要“现在是什么时刻”用 Timer需要“持续产生精确波形”或者“测量外部信号”用 PWM 模块。接下来我把这两类硬件的底层细节逐个拆开。2. 核心硬件原理64 位计数器与 PWM 计数器的本质2.1 64 位系统定时器的工作机制RP2040 的系统定时器Datasheet 里叫 Timer是一个 64 位递增计数器从 0 开始一直往上数。它的时钟源是系统主频默认 125MHz也就是说每纳秒计数值加 0.125每 8 纳秒加 1。64 位的计数值意味着它可以连续运行几百年不回绕所以你完全不需要担心溢出问题——这在做长时间运行的项目时特别省心。这个计数器有四个报警通道Alarm每个定时器报警通道都可以设置一个目标值当计数器的值到达这个目标时会触发一次中断。MicroPython 的Timer类本质上就是把其中一个报警通道包装成了软件定时器每次触发后在中断里重新装载目标值从而实现周期性回调。我在实际调试中发现MicroPython 的machine.Timer.init()直接使用这个硬件报警通道所以理论上中断触发精度很高。但有个坑是 MicroPython 层的回调函数本身需要执行时间如果你在回调里做了太多事情比如打印字符串、操作文件系统那么回调的执行时间会直接影响下一次触发的准确性。这点在精密时序场景下要特别留意。提示如果需要微秒级的定时精度建议直接在 C SDK 里操作寄存器绕开 MicroPython 的抽象层。MicroPython 的 Timer 回调本身很适合毫秒级任务比如每 10 毫秒采集一次传感器、每 1 秒刷新一次 OLED 显示。2.2 PWM 模块内部的 16 位计数器再来看 PWM 模块。每个 PWM slice 内部的计数器是一个 16 位递增/递减计数器频率由系统时钟经过分频后驱动。分频器是 8 位整数分频加 4 位小数分频的组合所以你可以实现非常细的频率调节。PWM 模块的计数器不仅用于生成波形它的工作模式还包括Free-running counter计数器从 0 数到WRAP值然后清零重新开始这是标准 PWM 模式。Input mode输入捕获计数器受外部引脚电平控制只有引脚为高电平时才计数。这个模式用来测量外部信号的脉宽特别有用。Rising/Falling edge capture边沿捕获计数器会在外部信号的上升沿或下降沿记录当前计数值从而推算出频率或脉宽。很多做机器人、航模的朋友买 Pico 是为了读取遥控接收机的 PWM 信号。遥控接收机输出的是一路 50Hz 左右、脉宽 1~2ms 的方波信号用普通 GPIO 外部中断配合time.ticks_us()也可以测但误差往往有几微秒到几十微秒。而用 PWM 模块的输入捕获模式可以做到纳秒级的边沿时间戳记录效果天差地别。3. 工作模式详解与 MicroPython 配置方法3.1 周期定时模式最常用的软件定时器MicroPython 的machine.Timer用法非常直观。用它实现周期回调代码就几行from machine import Timer timer Timer() timer.init(modeTimer.PERIODIC, period1000, callbacklambda t: print(tick))这段代码会每 1000 毫秒1 秒执行一次回调函数。period的单位是毫秒如果你要更短的周期可以传小数比如period0.5表示 500 微秒。但实测时需要注意MicroPython 解释器处理回调本身有开销过短的周期会导致回调堆积实际触发频率达不到设定值。还有一个有力的参数是modeTimer.ONE_SHOT它会在指定的延时后只触发一次回调。这个模式在做“延时后关断设备”“等待一定时间后发送心跳包”这类逻辑时非常清爽不需要自己维护标志位。3.2 计数器计数模式统计外部脉冲数量有些场景需要统计脉冲个数比如读取编码器的输出脉冲计算电机转速统计流量计的脉冲信号累计流量统计外部事件触发的 GPIO 边沿次数RP2040 的 PWM 模块天生支持在 GPIO 引脚上计数。在 MicroPython 里你可以用machine.PWM的input()方法进入输入模式然后读取计数器的值。代码是这样from machine import Pin, PWM pwm PWM(Pin(16)) # 使用 GP16 作为输入引脚 pwm.freq(1000) # 设置一个基础频率仅为了初始化 pwm.duty_u16(0) pwm.input() # 切换到输入捕获模式然后你可以通过读取pwm.counter()方法拿到当前的脉冲计数值。这个计数器是 16 位的最大计数范围是 0~65535如果脉冲数量可能超过这个值你需要在软件里处理溢出每次读完后判断是否回绕。3.3 PWM 输出模式精确控制波形的频率和占空比PWM 输出模式本质上是“定时器 比较器”的组合。你需要设置三个关键参数频率决定 PWM 波形的周期占空比决定高电平在一个周期内占的比例相位可选多路 PWM 之间的起始相位关系在 MicroPython 中最简单的方式是from machine import Pin, PWM pwm PWM(Pin(0)) # GP0 输出 PWM pwm.freq(50) # 50Hz常用于舵机控制 pwm.duty_u16(3276) # 占空比 3276 / 65535 ≈ 5%这里的duty_u16取值范围是 0~65535对应占空比 0%~100%。对于舵机控制通常脉宽需求是 0.5ms~2.5ms在 50Hz周期 20ms下对应占空比 2.5%~12.5%也就是duty_u16在 1638~8191 之间。我试过直接在上面代码的 Pico 上接 SG90 舵机把duty_u16(1638)设为 0 度、duty_u16(8191)设为 180 度转动非常平滑无抖动。3.4 输入捕获模式测量外部信号脉宽和频率这是 Pico 一个很惊艳的特性。当 PWM 模块处于输入模式时它内部有个机制可以捕获外部信号上升沿/下降沿时的计数值从而算出脉宽和周期。在 MicroPython 中虽然没有直接的官方库把输入捕获封装得很完整但你可以用 C 扩展或者直接操作寄存器。对大部分玩家来说更现实的方案是用machine.Pin.irq()配合time.ticks_us()来做边沿时间戳记录from machine import Pin import time last_rise 0 pulse_width 0 def pin_handler(pin): global last_rise, pulse_width if pin.value() 1: last_rise time.ticks_us() else: pulse_width time.ticks_us() - last_rise pin Pin(16, Pin.IN) pin.irq(triggerPin.IRQ_RISING | Pin.IRQ_FALLING, handlerpin_handler)这段代码可以测量输入信号的单个高电平脉宽精度在几十微秒量级对于大多数航模遥控信号脉宽 1~2ms来说精度足够。如果你需要更高精度请走 PWM 输入捕获寄存器或 C SDK 方案。4. 实操过程从延时到舵机控制再到频率测量4.1 用定时器实现高精度延时不依赖time.sleep()而是用定时器寄存器来实现短延时。在 MicroPython 里time.ticks_us()本身是微秒级的时间戳配合忙等待可以实现相对精确的延时import time def delay_us(us): start time.ticks_us() while time.ticks_diff(time.ticks_us(), start) us: pass # 使用 delay_us(100) # 延时 100 微秒这种方法比time.sleep(0.0001)准确得多因为time.sleep在 MicroPython 中延迟精度受解释器调度影响实测误差可达 10%~30%。而忙等待是用 CPU 循环计数误差主要来自ticks_us()本身的调用开销和循环判断分支实测误差在几微秒以内对大多数传感器时序控制完全够用。4.2 多路 PWM 输出实现舵机控制项目来一个完整小项目用 Pico 同时控制两个舵机一个做周期性摆动一个根据电位器角度调整位置。from machine import Pin, PWM, ADC import time # 初始化两个舵机 PWM servo1 PWM(Pin(0)) servo1.freq(50) servo2 PWM(Pin(1)) servo2.freq(50) # 初始化电位器 adc ADC(Pin(26)) def set_angle(pwm_obj, angle): # 角度范围 0~180脉宽 0.5ms~2.5ms50Hz 下对应 duty_u16 pulse_min 1638 pulse_max 8191 duty int(pulse_min (pulse_max - pulse_min) * angle / 180) pwm_obj.duty_u16(duty) while True: # 舵机1 从 0 度到 180 度循环 for angle in range(0, 181, 2): set_angle(servo1, angle) time.sleep_ms(10) # 舵机2 跟随电位器 val adc.read_u16() # 0~1023 angle val / 65535 * 180 set_angle(servo2, angle) time.sleep_ms(20)注意几点舵机 PWM 频率设置为 50Hz 是标准值但不是绝对的很多数字舵机支持更高频率如 100Hz~330Hz设置更高频率可以减少舵机抖动但要确认舵机支持范围。set_angle函数里用了线性映射不同舵机的脉宽边界略有差异你可以通过实测微调pulse_min和pulse_max来获得满量程转动范围。4.3 用定时器周期采集传感器并处理毛刺在工业级数据采集中定时器经常用来以固定的时间间隔采样。比如每 10ms 读一次 ADC做滑动平均滤波from machine import Timer, ADC, Pin adc ADC(Pin(26)) values [] def sample_cb(t): values.append(adc.read_u16()) if len(values) 50: values.pop(0) timer Timer() timer.init(modeTimer.PERIODIC, period10, callbacksample_cb) # 主循环可以处理其他任务比如显示或者通信 while True: if len(values) 10: avg sum(values[-10:]) // 10 print(avg:, avg) time.sleep_ms(50)这样的好处是采样时间间隔严格由硬件定时器保证主循环里的其他代码不会干扰采样节拍。若直接在while循环里用time.sleep_ms(10)采样一旦循环里的处理逻辑耗时波动采样间隔就会抖动后期做 FFT 或控制算法时会出现杂散噪声。4.4 通过定时器实现软件串口Bit-banging有些场景下你的串口不够用了比如多路传感器都要走 UART这时候可以用定时器配合 GPIO 做个软件串口。虽然 Pico 的 UART 硬件资源有限但好在 RP2040 主频足够高用定时器中断模拟 9600 波特率的串口发送完全可行。基本思路是引脚配置为输出模式将一字节数据拆成 8 个 bit加上起始位和停止位用定时器设定每个 bit 的时间间隔9600 波特率下约 104.2 微秒每中断一次输出一个 bit这是个进阶玩法不过理解了定时器中断的精确性你还可以做更复杂的协议比如 WS2812B 灯带的时序控制。WS2812B 要求 bit 周期精确到亚微秒级用 MicroPython 的time.sleep_us会有不可控误差实际用下来 800kHz 的数据率很难稳定驱动。这时候就需要把时序敏感的部分写到 C 扩展里或者使用 PIO 模块。这也是为什么我建议你在掌握定时器之后顺手了解一下 PIO——它是 RP2040 上另一大神器。5. 常见问题与排查技巧实录5.1 定时器回调不执行或执行频率不对现象写了Timer.PERIODIC但回调要么不触发要么触发的频率远远低于设定值。排查思路确认回调函数没有长时间阻塞。如果回调里有time.sleep()、耗时算法、文件读写会严重拖慢下一次触发。确认period的单位是毫秒。写period1是 1 毫秒不是 1 微秒。检查是否创建了多个 Timer 实例。MicroPython 的machine.Timer在 RP2040 上通常只支持有限的实例数创建过多或者重复init()可能导致冲突。如果你的回调执行时间超过了period那么实际上周期会被拉长到“回调执行时间 剩余等待时间”这是正常现象不是硬件故障。我在实际项目中遇到过回调里做了一行print()在 Mu 编辑器的串口终端里看没问题但接上某些 USB 转串口工具后打印阻塞了导致整体节奏被打乱。排查半天才发现是打印的锅。5.2 PWM 输出频率和理论值对不上现象你设置pwm.freq(1000)但用示波器或者逻辑分析仪看实际频率是 999.5Hz 或 1001Hz 这样的小偏差。原因RP2040 的 PWM 分频器是整数分频加 4 位小数分频不是所有频率都能精确表示。以 125MHz 系统时钟为例要得到精确的 1000Hz需要计数器 wrap 值为 125000 - 1这个数值是整数因此 1000Hz 可以精确。但如果你设置 333Hz125000000 / 333 ≈ 375375.375这个 wrap 值不能精确取整所以实际频率会有偏差。解决在要求精确频率的场景先反推系统时钟、分频器和 wrap 值选择可以整除的配置组合。或者使用 C SDK 直接设置寄存器避免 MicroPython 层的近似处理。5.3 输入捕获模式下读到的计数器值异常现象用 PWM 输入模式测量脉冲频率输出的值和万用表/示波器差很多。排查思路确认 GPIO 引脚是否支持 PWM 输入RP2040 并非所有引脚都有 PWM 功能需要通过复用功能表查。输入信号的电压逻辑是否匹配Pico 的 GPIO 是 3.3V 逻辑5V 信号需要分压或电平转换。检查计数器回绕问题。16 位计数器在 125MHz 下约 524 微秒回绕一次如果你测量的信号周期大于这个值需要扩展计数或者测量多个周期取平均值。5.4 多路定时任务如何合理安排当你有多个定时任务比如同时需要 50Hz 舵机控制、10Hz 传感器采样、1Hz 状态刷新最自然的方式是创建多个Timer实例。但 Pico 上的硬件报警通道只有 4 个MicroPython 的Timer类实现会占用其中一部分。更优雅的做法是使用一个 1ms 周期的 “tick” 定时器然后维护软件计时器列表from machine import Timer import time tasks [] def tick(t): now time.ticks_ms() for task in tasks: if time.ticks_diff(now, task[last]) task[interval]: task[last] now task[func]() timer Timer() timer.init(modeTimer.PERIODIC, period1, callbacktick) tasks.append({interval: 20, func: my_servo_update, last: 0}) tasks.append({interval: 100, func: sample_sensor, last: 0}) tasks.append({interval: 1000, func: update_display, last: 0})这种方式只用了一个硬件定时器其他任务全部由软件切片管理避免了多 Timer 实例的资源冲突问题。需要注意的是tick回调必须非常轻量只做变量更新和最简判断复杂的任务逻辑放到主循环去执行。5.5 定时器中断和 MicroPython 垃圾回收的相互影响MicroPython 运行在 RP2040 上时会周期性地执行内存垃圾回收GC。如果在定时器中断回调中创建对象比如list.append()、字符串拼接那么回调执行时间会因为 GC 波动而变得不稳定。解决办法回调函数只设置标志位或简单变量不做对象分配需要传递数据时预先分配好 list 或者数组回调里直接赋值主循环里集中处理数据避免在中断上下文做复杂逻辑我在做电机控制时就发现定时器回调里如果调用了math.sin()执行时间波动很大导致 PID 输出抖动。后来我把所有数学计算移到主循环回调只负责采样和置标志位效果立竿见影。6. 从定时器到更多外设常用配置速查表为了方便日常开发我整理了一张常用定时器方案的速查表覆盖了我在项目中用得最多的几个场景场景硬件模块核心参数MicroPython 示例周期任务调度Timerperiod 毫秒级Timer.PERIODIC, period10短延时Timer busy wait微秒级time.ticks_us()忙等待舵机控制PWM50Hz脉宽 0.5~2.5msfreq(50),duty_u16(1638~8191)LED 呼吸灯PWM1kHz占空比渐变配合定时器累加 duty频率测量PWM 输入模式16 位计数器参考上文输入捕获脉冲计数PWM 输入模式计数器累加读pwm.counter()软件串口Timer GPIO波特率决定中断间隔位定时 104.2us 9600多路任务调度单 Timer 软件列表1ms tick参考上文 tasks 列表这张表不是万能手册但可以作为你构建项目的起点。实际项目里定时器和 PWM 往往是配合使用的别把精力全耗在单一外设的细节上多想想它们之间的组合能力。7. 最后再分享一个调试技巧如果你手头没有示波器和逻辑分析仪可以用一个最“笨”但有效的办法来验证 PWM 输出和定时器中断的实际频率把待测引脚外接到另一个 GPIO然后用time.ticks_us()测量两次跳变之间的时间差打印到串口。这个方法虽然精度有限但能快速发现明显的问题比如分频配错导致频率差了一个数量级、回调阻塞导致周期翻倍等。另外RP2040 的数据手册RP2040 Datasheet第 4 章关于 Timer 和 PWM 的内容值得精读网上也能找到很多人分享的寄存器操作示例。我在看完数据手册后很多 MicroPython 层面令人困惑的行为都变得合理了——上层封装再怎么方便底层逻辑才是定海神针。希望你读完这篇文章后也可以根据需求灵活选择 Timer 还是 PWM而不是遇到定时需求就一律sleep遇到波形输出就硬靠 GPIO 手动翻转。最后补充一点如果做的是量产产品在定时精度要求高的场景建议直接用 C SDK 开发MicroPython 更适合原型验证和快速迭代。两条路线互补别把话说死毕竟工具是死的人是活的。
返回列表