news 2026/9/7 11:00:04

树莓派Pico定时器完全指南:API详解与实战避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Pico定时器完全指南:API详解与实战避坑

说实话,树莓派 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 个 Timer,4 路报警通道
中断回调优先级别多,需自己写 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(mode=Timer.ONE_SHOT, period=1000, callback=once_cb) # 周期:每 100ms 执行一次 def loop_cb(t): print("tick") t2 = Timer(mode=Timer.PERIODIC, period=100, callback=loop_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(mode=Timer.PERIODIC, period=200, callback=blink) # 运行 3 秒后停止 time.sleep(3) t.deinit() # 重新配置为 500ms 周期 t.init(mode=Timer.PERIODIC, period=500, callback=blink) # 最终释放 t.deinit()

这里有个特别容易踩的坑:如果你用Timer(...)创建对象后没有保存引用,比如直接写成Timer(mode=Timer.PERIODIC, period=100, callback=cb)然后后面再也没用到这个对象,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(mode=Timer.PERIODIC, period=1, callback=tick)

代码的思路是每 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(mode=Timer.PERIODIC, period=5, callback=scan)

这里的核心逻辑是每 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(mode=Timer.PERIODIC, period=20, callback=tick)

把时间周期设为 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 SDK:pico-sdk/src/rp2_common/hardware_timer/include/hardware/timer.hpico-sdk/src/rp2_common/hardware_pwm/include/hardware/pwm.h
  • MicroPython 文档:machine.Timer部分

RP2040 的芯片数据手册讲得更底层,里面有 64 位计数器、报警通道、中断标志的寄存器级说明,我现在写到这里,想精确控制中断标志时会去查一下。这些资料全部是英文的,但结构很清晰,配合本文的速查表就能快速定位到你需要的 API。

6. 定时器 API 速查表:一句话记住常用的

6.1 MicroPython 侧速查

需求写法
创建一次性定时器Timer(mode=Timer.ONE_SHOT, period=ms, callback=cb)
创建周期定时器Timer(mode=Timer.PERIODIC, period=ms, callback=cb)
停止并释放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 定时器真正发力的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 10:59:34

多线程编程全攻略:提升性能与线程安全的必备知识

目录 一、线程概念 (一)基本概念理解 (二)逻辑线程 vs 硬件线程 逻辑线程(Logical Thread) 硬件线程(Hardware Thread) 理解示例分析 (三)线程、核心、函数 核心(Core) 线程(Thread) 函数(Function) 示例理解分析 (四)程序、进程、线程、协程 程…

作者头像 李华
网站建设 2026/9/7 10:59:29

eLLM:让CPU在长上下文推理中逆袭GPU的实战指南

最近我在做长文档总结、代码仓库分析和多轮 Agent 这类长上下文任务时&#xff0c;一直有个很别扭的体验&#xff1a;明明 GPU 的算力指标高得吓人&#xff0c;但在某些场景下&#xff0c;推理速度却怎么也上不去&#xff0c;显存倒是越吃越紧。后来看到 MIT 那边放出的 eLLM 项…

作者头像 李华
网站建设 2026/9/7 10:56:58

Java虚拟时间轴实现:安全高效的时间加速与物体衰老效果

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 10:53:53

单位考核发稿怎么发?从流程到渠道一篇讲透

每到季度末或年底&#xff0c;就有不少企事业单位的宣传干事、行政人员开始为“考核发稿”发愁了&#xff1a;稿子写了&#xff0c;往哪儿投&#xff1f;投了能不能发&#xff1f;发了算不算数&#xff1f;这一连串问题&#xff0c;本质上是单位新闻宣传工作制度化、规范化程度…

作者头像 李华
网站建设 2026/9/7 10:53:23

【单片机课设毕设项目】基于 STM32 的 DS1302 时钟定时服药设备设计 基于 STM32 的多功能老年智能服药辅助装置设计

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/7 10:52:53

notepad-- 迁移指南:从 notepad++ 到开源跨平台轻量编辑器

做开发这十几年&#xff0c;我电脑里始终固定躺着几件东西&#xff1a;浏览器、IDE、终端、文本编辑器。前三个换起来很随意&#xff0c;唯独文本编辑器&#xff0c;我在 Windows 上被 notepad 绑了很久。你问它哪里特别好&#xff0c;我也说不上来&#xff0c;无非是打开快、多…

作者头像 李华