ARTICLE DETAIL

资讯详情

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

树莓派 Pico 定时器完全指南:硬件资源、C SDK 与 MicroPython 实战

树莓派 Pico 定时器完全指南:硬件资源、C SDK 与 MicroPython 实战 1. 为什么需要单独聊聊 Pico 的定时器树莓派 Pico 这块板子我前后玩了一年多从最初的 LED 闪烁到后来的多传感器数据采集绕不开的一个外设就是定时器。很多人一开始上手 Pico用的都是sleep()系列函数做延时跑通几个 demo 之后就开始放飞自我但等到真正要做点带时序要求的项目——比如舵机控制、PWM 波形测量、多任务时间片调度——就会发现单纯靠延时轮询根本扛不住。这时候就需要系统地了解一下 Pico 的定时器资源以及 C SDK 和 MicroPython 环境下的 API 用法。这篇内容不是官方文档的翻译是我自己踩了不少坑之后梳理出的一份极简参考。核心围绕三件事硬件上有哪些定时器可以用、C SDK 和 MicroPython 下怎么调用、实际项目中常见的坑和规避方式。适合刚接触 Pico 但已经跑通过基本程序的开发者也适合从 Arduino/STM32 转过来的朋友快速对齐概念。先说结论Pico 的定时器资源分两大块一个是系统级的片上定时器一般叫 Timer另一个是每个 PIO 状态机内部的时钟分频功能。日常开发中打交道最多的是前者所以这篇重点讲它。2. 硬件层面的定时器资源盘点2.1 RP2040 里到底有几个定时器RP2040 这颗芯片里定时器相关的硬件资源主要有三类一个 64 位系统定时器System Timer这是全局统一的时基频率通常跑在系统时钟频率默认 125 MHz 或 133 MHz取决于你设置的时钟提供微秒级和毫秒级的时间基准。四个可独立配置的硬件定时器Timer 0~3每个都可以工作在单次模式或者周期模式支持中断回调。两个 PIO 块每个块有四个状态机状态机内部有 16 位的时钟分频器主要用于精确波形生成和协议模拟不属于通用定时器范畴但功能上也能实现类似定时的效果。展开说一下。64 位系统定时器对于应用层开发者来说基本透明你不需要直接操作它的寄存器SDK 里的time_us_64()、time_us_32()、time_ms()等函数都是基于它实现的。真正需要开发者配置和管理的是那四个通用定时器。四个通用定时器各自独立可以同时运行而不互相干扰。每个定时器有一个 16 位的前分频器prescaler和一个 32 位的计数器/比较寄存器。前分频器可以把系统时钟分频让定时器的计数周期变长比较寄存器则决定了定时器溢出并产生中断的时间点。2.2 和 STM32、51 的定时器做个对比我见过不少从 STM32 转过来的朋友上来就找 Pico 定时器的“定时器时钟树”想看有没有 APB1、APB2 这种总线分频的概念。实际上 Pico 的定时器要简单得多——它没有复杂的时钟树定时器的输入时钟就是系统时钟本身通过前分频器做整数分频然后计数器累加到设定值触发中断。和 51 单片机的定时器对比Pico 的定时器更像是一个“增强版”。51 的定时器是 16 位的Pico 的通用定时器计数器是 32 位的直接定时范围大了几个数量级。而且 Pico 的定时器支持自动重装载periodic 模式51 早期型号需要手动重装初值这点体验差异很大。不过 Pico 的定时器也有一个明显的限制四个通用定时器在功能上是比较“朴素”的没有像 STM32 那样的输入捕获/输出比较通道也没有编码器接口模式。如果你需要测频率、测脉宽要么用 PIO 配合 DMA要么在回调里基于系统定时器做软件测量。这一点下面会详细展开。2.3 定时精度和时钟源Pico 板载的晶振一般是 12 MHz通过内部 PLL 倍频到 125 MHz 或 133 MHz不同出厂固件版本可能有差异。默认情况下系统时钟是 125 MHz所以定时器计数频率也是 125 MHz即一个计数周期是 8 ns。这是理论上的最高分辨率。实际上回调函数的执行、中断的响应延迟等因素会引入额外的抖动所以如果你要做的定时任务精度要求超过微秒级就不能单靠 Timer 了得考虑 PIO 或者直接用 PWM 硬件模块。我实测下来Pico 定时器中断的响应抖动大概在 ±2~3 微秒左右做一般的 LED 扫描、按键消抖、传感器轮询毫无压力但做高精度信号采样就要谨慎。3. C SDK 环境下定时器 API 的极简梳理3.1 创建和启动定时器Pico C SDK 里定时器的编程模型基于struct repeating_timer结构体和回调函数。核心 API 主要有以下几个我列一个速查表API 函数功能说明注意事项add_repeating_timer_ms以毫秒为单位创建周期定时器回调在中断上下文执行不能做耗时操作add_repeating_timer_us以微秒为单位创建周期定时器精度更高适合微秒级任务add_alarm_in_ms单次定时若干毫秒后触发回调一次性任务用这个add_alarm_in_us单次定时若干微秒后触发回调一次性任务精度微秒级cancel_repeating_timer取消周期定时器需要在回调外调用timer_pending查询定时器是否还在等待触发多线程场景下排查用代码写起来大概是这个风格#include pico/stdlib.h #include hardware/timer.h bool timer_callback(struct repeating_timer *t) { // 注意这段代码运行在中断上下文 // 不要在这里做 printf、malloc、长循环等操作 static uint32_t counter 0; counter; if (counter % 100 0) { gpio_put(LED_PIN, !gpio_get(LED_PIN)); } return true; // 返回 true 保持继续触发 } int main() { stdio_init_all(); gpio_init(LED_PIN); gpio_set_dir(LED_PIN, GPIO_OUT); struct repeating_timer timer; add_repeating_timer_ms(500, timer_callback, NULL, timer); while (1) { // 主循环做别的事情 tight_loop_contents(); } }这段代码就是一个最基础的 500ms 周期定时器回调里翻转 LED。看起来简单但里面有几个值得注意的细节。3.2 回调函数的执行上下文和限制第一次用 Pico 定时器的时候我踩过一个大坑在回调里直接调用了printf结果程序跑着跑着就卡死了。后来查文档才知道定时器回调是在中断上下文执行的而且优先级不低你在里面做任何阻塞操作都会影响其他中断的响应。具体来说回调函数里不应该做这些事调用printf或任何会触发阻塞 I/O 的操作。动态内存分配比如malloc、free。持有互斥锁或者调用可能阻塞的同步原语。执行时间过长的循环或计算。那回调里到底能干什么我的习惯是只做轻量级的标志位设置、变量累加、GPIO 翻转把这些事件记录下来真正的逻辑处理放到主循环里通过轮询标志位完成。比如volatile bool timer_fired false; volatile uint32_t tick_count 0; bool timer_callback(struct repeating_timer *t) { tick_count; timer_fired true; return true; } int main() { // ...初始化... while (1) { if (timer_fired) { timer_fired false; // 在这里处理真正的业务逻辑 process_tick(tick_count); } } }这种方式既能保证中断回调足够短又能保证主循环里拿到完整的数据做复杂处理是我在多个项目里验证过最稳的模式。3.3 单次定时器怎么用周期定时器用得多但单次定时器在某些场景下也特别有用。比如你需要在指定延迟后执行某个操作又不想在主线程里阻塞等待就可以用add_alarm_in_ms。#include hardware/timer.h int64_t one_shot_callback(alarm_id_t id, void *user_data) { // 单次定时回调 gpio_put(LED_PIN, 0); // 返回 0 表示只执行一次 return 0; } // 500ms 后执行一次回调 alarm_id_t alarm add_alarm_in_ms(500, one_shot_callback, NULL, true);这里最后一个参数fire_if_past是布尔值表示如果设定时间已经过去是否立即触发回调。实际开发中如果你的系统负载比较高定时任务可能被延后这个参数就看你是想“尽快补执行”还是“直接放弃”。单次定时器还有个用法实现超时机制。比如等待外部传感器响应时设置一个 100ms 的闹钟如果在闹钟触发前收到了正确数据就调用cancel_alarm取消闹钟。这样可以优雅地处理“设备无响应”的场景。3.4 定时器 ID 和排查技巧C SDK 里每个定时器实例会有一个alarm_id_t类型的 ID通过cancel_alarm或者cancel_repeating_timer时可以用这个 ID 精确取消。如果你在调试时遇到定时器取消不掉的问题可以用timer_pending检查定时器是否仍在等待状态。我遇到过一个诡异的情况创建了一个周期定时器但在某个条件下想取消它调用了cancel_repeating_timer之后回调函数还是继续触发了。排查了半天最后发现是因为我没有检查返回值——cancel_repeating_timer返回false表示定时器已经不存在或取消失败。实际上如果回调正好在取消操作的那一刻被触发就会竞争需要增加同步机制。4. MicroPython 环境下的定时器 API4.1 machine.Timer 的基本用法很多新手用 Pico 玩 MicroPython因为上手门槛低。MicroPython 的定时器 API 封装得比较干净和 C SDK 的复杂度不在一个量级。from machine import Timer # 创建定时器 timer Timer() # 周期模式500ms 触发一次 timer.init(period500, modeTimer.PERIODIC, callbacklambda t: led.toggle()) # 单次模式2秒后触发一次 timer.init(period2000, modeTimer.ONE_SHOT, callbacklambda t: print(time up))就这么简单。Timer.PERIODIC是周期循环Timer.ONE_SHOT是单次触发回调函数接收一个参数定时器对象本身用 lambda 或者普通函数都行。MicroPython 的 Timer 类底层映射到 RP2040 的硬件定时器但你不需要关心具体用了哪一个解释器会帮你管理。对于大多数脚本层面的应用这个抽象的便利性远超那一点点性能损耗。4.2 MicroPython 定时器的回调限制MicroPython 的回调同样运行在中断上下文所以print虽然在 MicroPython 里不会直接导致崩溃但它会消耗大量时间极端情况下会影响定时精度甚至嵌套中断导致复位。我的建议是MicroPython 环境下回调里只做最小操作比如设置一个标志位from machine import Timer tick_counter 0 timer_fired False def timer_cb(t): global tick_counter, timer_fired tick_counter 1 timer_fired True timer Timer() timer.init(period100, modeTimer.PERIODIC, callbacktimer_cb) while True: if timer_fired: timer_fired False print(tick:, tick_counter)另外MicroPython 支持创建多个 Timer 实例但底层硬件定时器总共只有 4 个所以实际可用的实例数量有限。如果你在代码里创建了超过 4 个 Timer 同时工作会直接抛出异常。我在一个项目里同时需要 3 个定时器当时没注意结果第四个初始化直接报错排查了很久才发现是这个限制。4.3 用定时器驱动舵机热词里有“树莓派 pico 控制舵机”这也是定时器在实际项目里最典型的应用之一。舵机控制的核心是 50Hz 的 PWM 信号脉宽在 0.5ms~2.5ms 之间对应 0°~180°。用 MicroPython 的定时器来做数字舵机控制思路很简单定时周期设为 20ms50Hz然后在回调里根据目标角度控制高电平持续时间。不过纯回调实现精度有限实际项目中我更推荐用 Pico 的 PWM 硬件模块来输出舵机信号定时器只负责管理角度更新的节奏from machine import Pin, PWM, Timer servo PWM(Pin(15)) servo.freq(50) # 50Hz def set_angle(angle): # 0.5ms ~ 2.5ms 对应 0~180度 # 20ms 周期下占空比约为 2.5% ~ 12.5% pulse_width 0.5 (angle / 180.0) * 2.0 duty int(pulse_width / 20.0 * 65535) servo.duty_u16(duty) # 用定时器每隔 500ms 更新一次角度 angle 0 def angle_updater(t): global angle angle (angle 10) % 180 set_angle(angle) timer Timer() timer.init(period500, modeTimer.PERIODIC, callbackangle_updater)这里的关键点是PWM 输出用硬件模块保证波形稳定定时器只负责定期改变角度二者配合能实现非常顺滑的舵机运动控制。5. 定时器在项目中的实际应用模式5.1 时间片轮询替代裸机 delay 的进阶方案裸机开发最常见的写法是主循环里加延时轮询传感器。这种方式写起来简单但一旦外设变多、逻辑变复杂延时会导致整个系统的响应时间不可控。用定时器做时间片轮询是更优雅的方案。假设你需要同时处理按键扫描、LED 闪烁、传感器读取三件事每件事的周期不一样就可以拆成三个定时器每个定时器对应一个任务bool key_scan_cb(struct repeating_timer *t) { key_scan_flag true; return true; } bool led_blink_cb(struct repeating_timer *t) { gpio_put(LED_PIN, !gpio_get(LED_PIN)); return true; } bool sensor_read_cb(struct repeating_timer *t) { sensor_read_flag true; return true; }然后在主循环里根据标志位依次处理。这种模式的好处是任务间互不阻塞每个任务都在自己的时间片内完成系统的实时性远好于逐个延时。5.2 软件 PWM没有硬件 PWM 通道的备选方案Pico 的 PWM 硬件模块其实有 8 个通道一般情况下够用了。但如果你需要同时输出多路不同频率的方波信号8 个通道也会不够这时可以用定时器做软件 PWM。思路很简单定时器频率设为 PWM 的节拍频率回调里根据占空比决定当前引脚输出高还是低。比如要输出 1kHz 的 PWM占空比 50%就把定时器设为 1ms 周期前 500us 输出高后 500us 输出低。这个方案能实现但有个硬伤精度和通道数是矛盾的。多个软件 PWM 通道共用一个定时器时只能按顺序在回调里逐个处理通道多了精度就会下降。我实测在 4 路软件 PWM 的情况下精度还能勉强保持在 5% 以内超过 4 路就不太行了。建议优先用硬件 PWM软件 PWM 只在特殊场景下作为备选。5.3 利用系统定时器实现高精度延时除了通用定时器Pico 的 64 位系统定时器也是日常开发中很常用的资源。busy_wait_us()和busy_wait_ms()是阻塞式延时精度很高但会占用 CPU。非阻塞等待可以用uint64_t start time_us_64(); while (time_us_64() - start 100) { // 非阻塞等待 100us }这种写法适合在需要精确控制时序又不想塞住整个系统的场景。比如和某些时序敏感的传感器通信时发完命令等待 N 微秒读取数据用这种方式最合适。6. 常见问题与排查技巧实录6.1 问题速查表现象可能原因解决方案回调不执行定时器未正确初始化检查add_repeating_timer_ms最后一个参数的结构体地址是否有效回调只执行一次回调函数返回了false返回true保持周期触发定时器取消不掉回调与取消操作发生竞争在回调外调用 cancel并检查返回值MicroPython 创建第四个 Timer 报错硬件定时器数量限制合并定时任务或者改用 C SDK定时精度明显偏差回调函数执行时间过长精简回调逻辑只设置标志位定时器触发间隔不规律主循环中存在阻塞操作排查主循环的阻塞延时改用非阻塞方式6.2 一个让我排查了一整天的 Bug说一个真实案例。有次做一个多传感器数据采集项目三个传感器分别用三个定时器控制采样周期运行一段时间后系统总会无规律地重启。最开始以为是供电问题换了电源还是一样。用串口打印调试发现重启之前必定有某个定时器回调执行耗时特别长的情况。排查代码才发现我在一个定时器回调里调用了stdio_printf而另一个定时器的优先级和它冲突导致嵌套中断。MicroPython/C SDK 的中断虽然支持嵌套但我们的代码没有做嵌套保护栈溢出直接复位。从那以后我的定时器回调都严格遵守“只设标志位不做重活”的规则再没出现过类似问题。6.3 定时器资源不够时的调整策略如果你发现四个通用定时器不够用有几条路可以走合并任务把多个周期相同的任务放到同一个回调里处理。用系统定时器轮询time_us_64()做软件计时器可以模拟出无数个“伪定时器”精度取决于主循环的响应速度。用 PIO 状态机PIO 可以独立于 CPU 产生精确的时序信号适合波形生成类任务。我在实际项目中经常是“通用定时器 软定时器”混用既保证了关键任务的中断精度又不会浪费宝贵的硬件定时器资源。7. 几个送给大家的实操建议最后分享几点我在实际使用中的体会。第一无论用 C SDK 还是 MicroPython回调里只做最轻量级的事。这个原则非常非常重要宁可多写几行标志位检查逻辑也不要在回调里图省事直接处理业务。我见过太多人一开始觉得“我就打印一行没事的”结果上了复杂项目之后被各种诡异问题折磨。第二定时器初始化之前务必确认你的系统时钟配置。如果你改了 PLL 频率定时器的计数频率也会跟着变相同参数下的实际定时时间会有偏差。如果你的系统对定时精度有要求最好显式地通过clock_get_hz(clk_sys)获取当前系统时钟频率据此计算分频系数。第三MicroPython 环境下的定时器周期参数单位是毫秒不能设置微秒级的周期精度也到不了那么高。需要微秒级定时的话要么用 C SDK要么直接用time_us_64()做忙等待。第一次把 Pico 的定时器理清楚之后你会发现以前很多绕弯子的实现都可以大幅简化。定时器这东西看着简单用好了能省掉不少后续排查问题的精力。希望这篇梳理对你的项目有帮助。
返回列表