
说实话树莓派 Pico 的定时器看起来不难官方文档翻几页就能上手可真到项目里用的时候你会发现它和 STM32、51 那种传统定时器的思路完全不是一个路子。最近好几个从单片机转过来的朋友都在问我同一个问题Pico 的定时器到底怎么用API 有哪些为什么我照着 STM32 的写法总是卡住这篇文章就把树莓派 Pico 定时器的概要信息和 API 从头到尾捋一遍从硬件资源、MicroPython 侧到 C SDK 侧再到实际场景里的坑一次说透。适合刚接触 Pico 的嵌入式新手也适合想快速评估 Pico 能不能承担定时任务的硬件工程师内容尽量做到“极简”能查表的绝不多写废话。1. 先把定时器这件事说清楚Pico 的硬件时间从哪来1.1 为什么 MCU 需要定时器而不是一直 delay很多新手写代码喜欢用sleep_ms(100)来控制节奏比如让 LED 每 100ms 翻转一次。这个写法在只有单个任务的场景下没什么问题可一旦你要同时处理按键扫描、OLED 刷新、串口收发、舵机脉宽输出阻塞式的延时就立刻变成灾难——某个传感器等待响应的时候其他任务全部卡死。定时器的本质就是“把时间这个事情从 CPU 里拆出去”。CPU 设定好定时参数之后可以继续干别的事时间一到硬件产生中断或标记事件程序再回过头来处理。这种“非阻塞 回调/事件标志”的结构是构建复杂嵌入式系统的基本功。Pico 解决这个问题的方式很特别它不是传统意义上的“预分频 自动重装载计数器 比较寄存器”而是用了一个 64 位的微秒计数器一直在后台跑所有定时功能都围绕这个单调递增的时间戳展开。这个设计理念和 STM32 的 TIM 外设差异很大理解它之后很多 API 的命名逻辑你就秒懂了。1.2 Pico 的定时器硬件资源到底有多少树莓派 Pico 使用的 RP2040 芯片内部有两个硬件定时器编号分别是 Timer 0 和 Timer 1每个定时器又带有 4 路独立的报警比较通道。也就是说整个芯片最多可以同时设置 8 个独立的硬件定时报警点。每个报警通道都对应一个绝对时间戳absolute time当 64 位微秒计数器到达这个时间戳时就会触发一次中断。由于时间戳是 64 位的理论可以表示的时间范围极长大约 58 万年所以你完全不用担心它“溢出回绕”这在用绝对时间做任务调度的时候非常省心。主频方面Pico 默认系统时钟是 125MHz也就是说微秒计数器每 125 个时钟周期加 1。你可以在pico_set_clock里调整主频但微秒计数器会自动根据时钟频率校准不需要你手动做除法。这个细节在项目里非常实用你只管调用time_us_64()读时间戳就行底层已经处理好了。1.3 与 STM32、51 定时器的思维差异对比很多从 STM32 和 51 转过来的朋友一上来就习惯性地找“预分频器”“自动重装值”“PWM 输出模式”结果发现 Pico 定时器 API 里根本没有这些东西。我们先花两分钟把这个差异掰开后面看代码就顺了。对比项STM32 / 51 定时器Pico (RP2040)计数宽度16 位或 32 位64 位微秒计数器时间基准预分频 自动重装固定的 1us 时间戳PWM 输出定时器直接输出由独立的 PWM 外设负责多路定时多组 TIM 外设2 个 Timer4 路报警通道中断回调优先级别多需自己写 NVIC 配置一个回调函数指针参数传递用户数据也就是说在 Pico 上“PWM 输出”这个功能被彻底拆分给了 PWM 外设定时器只负责“在某个时间点提醒你”。你要输出舵机波形请去找machine.PWM或者 C SDK 的pwm_set_wrap你要定时做任务调度才来找定时器。这个思维转换很重要否则你会在错误的 API 里翻半天找不到想要的功能。2. MicroPython 下的定时器 API一句话记住一个函数2.1 machine.Timer 的两种模式一次性与周期执行MicroPython 侧使用定时器非常简单核心就一个类machine.Timer。它支持两种模式Timer.ONE_SHOT是一次性定时到达时间后只执行一次回调Timer.PERIODIC是周期执行每隔固定时间调用一次回调。from machine import Timer # 一次性1 秒后执行一次 def once_cb(t): print(time up) t1 Timer(modeTimer.ONE_SHOT, period1000, callbackonce_cb) # 周期每 100ms 执行一次 def loop_cb(t): print(tick) t2 Timer(modeTimer.PERIODIC, period100, callbackloop_cb)这里的period单位是毫秒回调函数接收一个参数t就是触发本次回调的 Timer 对象本身。我实测下来这种写法在捣鼓小型状态机的时候特别顺手比如按键长按检测、非阻塞轮询调度都能用很短的代码实现。有一点要提醒MicroPython 的定时器回调是在中断上下文中执行的。你可以在里面翻转引脚、修改全局变量、发送极短的串口数据但绝对不要在里面做延时等待、磁盘读写、大块内存分配这类操作否则轻则卡顿重则直接触发异常导致程序崩溃。正确做法是让回调尽快结束把复杂的处理扔给主循环里的标志位。2.2 初始化、启动、停止和释放完整生命周期在实际项目里定时器通常不是配好就不管了你经常需要动态地启停和释放。MicroPython 的 Timer 对象支持init()和deinit()两种方法deinit()之后可以再用init()重新配置不需要重新创建对象。from machine import Timer, Pin led Pin(25, Pin.OUT) state False def blink(t): global state state not state led.value(state) # 创建并启动 t Timer(modeTimer.PERIODIC, period200, callbackblink) # 运行 3 秒后停止 time.sleep(3) t.deinit() # 重新配置为 500ms 周期 t.init(modeTimer.PERIODIC, period500, callbackblink) # 最终释放 t.deinit()这里有个特别容易踩的坑如果你用Timer(...)创建对象后没有保存引用比如直接写成Timer(modeTimer.PERIODIC, period100, callbackcb)然后后面再也没用到这个对象MicroPython 的垃圾回收机制可能会把这个 Timer 对象回收掉导致回调莫名其妙停止。我在 Pico 上实测过确实会发生。所以建议所有定时器对象都用一个全局变量或者类成员明确持有别图省事匿名创建。如果是 C SDK 开发这个问题的表现方式不同但思路一样——你要保证报警回调注册之后相关的上下文结构体一直存活。2.3 用 time 模块做时间戳与延时测量除了 Timer 外MicroPython 还提供了基于系统时间的time模块里面有几个函数非常适合做时间测量和超时判断。我用得最多的是time.ticks_ms()、time.ticks_us()和time.ticks_diff()。import time start time.ticks_us() # 执行某段代码... delta time.ticks_diff(time.ticks_us(), start) print(elapsed us:, delta)注意这里的微秒精度取决于 MicroPython 固件的实现实测在 RP2040 上ticks_us()的精度可以到几微秒级别日常测性能完全够用。它跟定时器回调是两条线一个是主动测量的工具一个是被动通知的机制两者配合使用能覆盖大多数开发场景。3. C SDK 定时器 API底层控制力的真正来源3.1 sleep 系列与 alarm 系列阻塞和非阻塞怎么选如果项目用 C/C 开发Pico SDK 提供的定时器 API 更接近硬件控制力也更强。核心分为两大类一类是阻塞式的sleep_us()、sleep_ms()、sleep_until()另一类是非阻塞的add_alarm_in_us()、add_alarm_at()。#include pico/stdlib.h #include hardware/timer.h // 阻塞延时 100ms sleep_ms(100); // 非阻塞100ms 后执行回调 struct repeating_timer timer; add_repeating_timer_ms(100, repeat_callback, NULL, timer); // 一次性报警 absolute_time_t target make_timeout_time_ms(500); add_alarm_at(target, one_shot_callback, NULL, false);add_repeating_timer_ms是最常用的周期定时接口回调函数返回值决定是否继续周期执行返回true继续返回false停止。这个接口底层自动分配一个硬件报警通道所以你在应用层不需要关心 Timer 0 还是 Timer 1。如果你对硬件有洁癖也可以直接调用hardware_alarm_claim()、hardware_alarm_set_callback()和hardware_alarm_set_target()来手动管理报警通道。好处是你能精确控制每个通道坏处是代码量变多还要自己处理通道抢占。我的建议是绝大多数项目用add_alarm_*系列就够了等真的出现通道冲突或者极端时序需求再往下折腾。3.2 回调函数里的“中断上下文”约束无论你用 MicroPython 还是 C SDK定时器回调都运行在中断上下文。这个听起来很基础但很多新手就是在这里翻车的最典型的错误是在回调里调用printf。bool repeat_callback(struct repeating_timer *t) { printf(tick\n); // 危险操作 return true; }为什么说危险因为printf涉及串口输出串口驱动本身可能依赖中断在中断里再触发中断相关操作轻则输出卡顿重则死锁。我在调试时甚至遇到过用printf输出后整个定时器回路直接不工作的诡异现象。正确做法是回调里只设置一个全局标志位主循环检测到标志后再做串口输出和复杂处理。volatile bool flag false; bool repeat_callback(struct repeating_timer *t) { flag true; // 尽快返回 return true; } int main() { // 主循环轮询标志 while (true) { if (flag) { flag false; printf(main loop sees tick\n); } } }另外malloc这类动态内存操作在中断里也尽量别用容易造成内存碎片和不确定的延迟。Pico 的资源很有限宁可提前分配好静态缓冲区也不要在回调里临时申请。3.3 为什么不建议用定时器直接做 PWM我在群聊里经常看到有人问“能不能用定时器直接输出 PWM”这个问题放到 STM32 上确实可以因为 STM32 的定时器外设自带 PWM 模式。但 Pico 不一样它的 PWM 由独立的 PWM 外设实现定时器只管定时提醒。Pico 的 PWM 外设同样基于系统时钟能够输出频率和占空比都可调的方波。推荐的做法是#include hardware/pwm.h // 初始化 GPIO 0 的 PWM 通道 gpio_set_function(0, GPIO_FUNC_PWM); uint slice pwm_gpio_to_slice_num(0); pwm_set_wrap(slice, 12500); // 频率 125MHz / 12500 10kHz pwm_set_chan_level(slice, PWM_CHAN_A, 6250); // 占空比 50% pwm_set_enabled(slice, true);定时器在 PWM 场景下的角色是“节奏控制”比如让 PWM 占空比按照一定时间间隔平滑变化实现呼吸灯效果或舵机渐变。这两个外设各司其职组合起来才是一个完整的控制系统。4. 实操案例三个真实场景从零写完4.1 场景一非阻塞 LED 呼吸灯很多教程教你用sleep_ms写呼吸灯那种写法会导致整个程序卡在循环里其实用定时器可以非常优雅地实现。from machine import Pin, PWM, Timer led PWM(Pin(25)) # Pico 板载 LED led.freq(1000) duty 0 direction 1 def tick(t): global duty, direction duty direction * 100 if duty 65535: duty 65535 direction -1 elif duty 0: duty 0 direction 1 led.duty_u16(duty) Timer(modeTimer.PERIODIC, period1, callbacktick)代码的思路是每 1ms 更新一次 PWM 占空比变化步长是 100整个呼吸周期大约 1.3 秒。由于定时器回调只是改一个寄存器的值整个过程非常平滑。更重要的是这个方案下主循环是完全空闲的你可以在里面继续跑其他逻辑。如果把占空比变化步长从 100 改成 500呼吸速度会明显变快你可以根据自己的视觉效果微调这个参数。4.2 场景二按键消抖和长按检测按键消抖是嵌入式开发绕不开的经典话题。用定时器做周期扫描比用delay消抖要可靠得多。from machine import Pin, Timer btn Pin(16, Pin.IN, Pin.PULL_UP) counter 0 level 1 last_level 1 press_count 0 def scan(t): global counter, level, last_level, press_count now btn.value() if now ! last_level: counter 1 if counter 3: # 连续 3 次扫描都不同认为电平稳定 level now counter 0 if level 0: press_count 1 else: counter 0 last_level now Timer(modeTimer.PERIODIC, period5, callbackscan)这里的核心逻辑是每 5ms 扫描一次按键状态连续 3 次读到相同电平才确认状态变化相当于 15ms 的去抖窗口。这样做的好处是消抖过程完全在后台进行主循环随时可以读取press_count来执行快捷键逻辑。注意我给scan函数里那些变量都加了global声明MicroPython 里如果忘了声明全局变量函数内部会默认创建局部变量按键状态就永远更新不到了。这是新手最容易忽略的坑。4.3 场景三Pico 控制舵机平滑转动结合很多人搜的“树莓派 pico 控制舵机”这里用一个 SG90 舵机做演示。SG90 需要 50Hz周期 20ms的 PWM 信号高电平时间 0.5ms~2.5ms 对应 0 度到 180 度。换算成duty_u16的范围是 1638~8192。from machine import Pin, PWM, Timer servo PWM(Pin(15)) servo.freq(50) duty 1638 step 15 # 每步变化量 def tick(t): global duty, step duty step if duty 8192: duty 8192 step -step elif duty 1638: duty 1638 step -step servo.duty_u16(duty) # 每 20ms 更新一次占空比 Timer(modeTimer.PERIODIC, period20, callbacktick)把时间周期设为 20ms正好是舵机一个 PWM 周期。每次回调让占空比步进一点舵机就会在 0 度和 180 度之间来回平滑摆动不会出现跳跃感。这里要特别提醒Pico 的 PWM 外设和定时器是两个独立模块上面代码里其实用到了 PWM 外设输出脉宽波形而定时器只是控制“什么时候改占空比”。很多新手以为舵机的周期是靠定时器生成的其实不是舵机的 20ms 周期是 PWM 外设自己保证的定时器只是介入调整角度。5. 常见问题与排查技巧实录5.1 定时器回调不执行或只执行一次这是出现频率最高的问题通常有三个原因定时器对象被垃圾回收。MicroPython 里没保存引用回调跑几次后对象被回收。回调函数内部发生异常。比如在回调里访问了不存在的变量MicroPython 会把异常打印到串口但不重启看起来就像定时器“停了”。一次性定时器模式用成了周期模式或者反过来回调返回值导致周期停止。排查方法很简单在回调第一行加一个print或者翻转一个 LED 引脚先确认回调本身有没有被调用再逐步注释代码定位异常点。5.2 定时精度不够周期抖动大用哪种开发环境、哪种调度方式直接影响定时精度。我实测过一组对比使用方式典型精度适合场景MicroPython Timer±0.1ms 到 ±0.5ms呼吸灯、扫描、超时判断MicroPython ticks_us 测量几微秒级耗时测量C SDK repeating_timer微秒级抖动较小精密波形、数据采集C SDK hardware_alarm 手动最精确可控性最高极端时序要求如果你用 MicroPython 发现定时器周期抖动明显优先检查是不是回调里做了耗时操作比如打印。MicroPython 的 GC 垃圾回收也会偶尔造成数百微秒的停顿这在时序严苛的场景下是致命的。对策是高精度需求一律上 C SDK别跟 MicroPython 的软实时特性较劲。5.3 回调里发生 HardFault 或系统卡死C SDK 开发时HardFault 多半是回调里干了不该干的事。我见过最典型的错误就是在定时器回调里调用sleep_ms结果中断一直嵌套系统直接挂死。另一个常见原因是没有正确保存报警回调的用户数据指针。当你传入一个指向局部变量的指针给add_alarm_in_us函数返回后局部变量销毁等回调触发时访问这个悬空指针轻则数据错误重则崩溃。正确做法是传一个静态变量或者全局结构体的地址。5.4 官方资料速查这些文档位置要记住每次写 Pico 定时器代码我必看的资料就两个C SDKpico-sdk/src/rp2_common/hardware_timer/include/hardware/timer.h和pico-sdk/src/rp2_common/hardware_pwm/include/hardware/pwm.hMicroPython 文档machine.Timer部分RP2040 的芯片数据手册讲得更底层里面有 64 位计数器、报警通道、中断标志的寄存器级说明我现在写到这里想精确控制中断标志时会去查一下。这些资料全部是英文的但结构很清晰配合本文的速查表就能快速定位到你需要的 API。6. 定时器 API 速查表一句话记住常用的6.1 MicroPython 侧速查需求写法创建一次性定时器Timer(modeTimer.ONE_SHOT, periodms, callbackcb)创建周期定时器Timer(modeTimer.PERIODIC, periodms, callbackcb)停止并释放t.deinit()重新配置t.init(mode..., period..., callback...)取系统毫秒时间time.ticks_ms()测量耗时time.ticks_diff(time.ticks_us(), start)6.2 C SDK 侧速查需求函数阻塞延时sleep_us()/sleep_ms()一次性报警add_alarm_in_us(us, cb, user_data, false)周期报警add_repeating_timer_ms(ms, cb, user_data, timer)读取时间戳time_us_64()手动配置 PWM 频率pwm_set_wrap()pwm_set_chan_level()底层手动设置报警hardware_alarm_set_target()6.3 回调函数注意点清单再强调一次无论用 MicroPython 还是 C SDK定时器回调里都不能做这些事阻塞延时、动态内存分配、复杂计算、长时间的串口打印、文件系统操作。这些操作会让中断响应时间拉长甚至触发未知异常。需要复杂逻辑时在回调里设置标志位主循环去处理。我个人在实际调试中还有一个习惯定时器回调里只做三件事读时间戳、改标志位、改 GPIO 电平。所有需要经过多层函数调用的逻辑一律放到主循环。这样写出来的代码不但稳定排查问题也快——定时器这边出问题基本就锁定在“标志位有没有置位”这个环节。这套定时器的用法够你在 Pico 上跑起绝大多数小项目了。如果后面你在实际项目里遇到更刁钻的时序需求比如多路高精度采样的同步触发我建议直接研究硬件报警通道的手动配置那才是 RP2040 定时器真正发力的地方。