4、定时器子系统入门:Zephyr定时器API概览、定时器类型(周期/单次)、定时器回调机制
各位同学,欢迎来到第四章。今天咱们聊聊Zephyr的定时器子系统。
定时器这东西,在工业控制里太常见了。你想想看,PLC的扫描周期、伺服电机的脉冲输出、传感器的采样间隔,哪个离得开定时器?我刚开始做嵌入式那会儿,用的还是裸机定时器,每次都要手动配置寄存器,麻烦得很。后来转到Zephyr,发现它的定时器API设计得挺清爽,用起来顺手多了。
4.1 定时器API概览
Zephyr的定时器API,说白了就是一套标准接口。你不需要关心底层是SysTick还是硬件定时器,API帮你封装好了。我个人习惯把定时器API分成三类:创建与配置、启动与停止、状态查询。
先看核心的数据结构——struct k_timer。这个结构体就是定时器的“身份证”,里面存了定时器的状态、回调函数指针、剩余时间等信息。使用时,你得先定义一个这样的变量。
struct k_timer my_timer;
然后调用初始化函数:
k_timer_init(&my_timer, my_expiry_fn, my_stop_fn);
这里有两个回调函数参数,后面会细讲。初始化之后,定时器就处于“待命”状态,还没开始跑。
启动定时器用这个:
k_timer_start(&my_timer, K_MSEC(100), K_MSEC(200));
第一个参数是定时器对象,第二个是初始延迟,第三个是周期时间。如果第三个参数设为K_NO_WAIT或0,那就是单次定时器。
停止定时器更简单:
k_timer_stop(&my_timer);
嗯,这里要注意:停止一个已经停止的定时器是安全的,不会报错。我在项目中遇到过有人反复调用stop,结果代码里加了一堆判断,其实没必要。
4.2 定时器类型:周期与单次
Zephyr的定时器就两种模式:周期模式和单次模式。怎么区分?看启动时的第三个参数。
| 模式 | 启动参数(周期时间) | 行为 |
|---|---|---|
| 单次 | K_NO_WAIT 或 0 |
延迟到期后执行一次回调,然后停止 |
| 周期 | 大于0的毫秒数 | 延迟到期后执行回调,然后每隔周期时间再执行 |
举个例子。单次定时器常用于“超时检测”——比如等待一个外设响应,如果100ms没反应就报错。周期定时器则用于“心跳任务”——比如每10ms采集一次传感器数据。
我曾经在一个电机控制项目里,用周期定时器生成PWM的基准时基。当时需要50μs的精度,Zephyr的定时器配合硬件定时器,跑得很稳。不过要注意,软件定时器的精度受系统时钟节拍影响,默认一般是10ms或1ms。如果你需要微秒级的定时,得用硬件定时器或者PWM模块。
4.3 定时器回调机制
回调机制是定时器的灵魂。Zephyr的定时器支持两个回调:到期回调和停止回调。
到期回调:定时器到期时触发。这是最常用的回调,你可以在里面执行周期性任务。
停止回调:定时器被手动停止时触发。这个回调用得少,但某些场景下很有用。比如,你想在定时器被外部停止时做一次“清理工作”。
回调函数的原型是这样的:
void my_expiry_fn(struct k_timer *timer_id)
{
// 定时器到期时执行的代码
printk("Timer expired!\n");
}
void my_stop_fn(struct k_timer *timer_id)
{
// 定时器被停止时执行的代码
printk("Timer stopped!\n");
}
初始化时把这两个函数传进去:
k_timer_init(&my_timer, my_expiry_fn, my_stop_fn);
如果你不需要停止回调,可以传NULL:
k_timer_init(&my_timer, my_expiry_fn, NULL);
回调函数是在中断上下文还是线程上下文执行的?这个问题很关键。Zephyr的定时器回调默认在系统时钟中断里执行。这意味着:
- 回调函数要尽量短,不能阻塞
- 不能调用会阻塞的API(比如信号量获取、消息队列发送等)
- 不能做浮点运算(除非使能了浮点支持)
如果你需要在回调里做耗时操作,怎么办?我建议用工作队列(work queue)把任务丢出去。具体做法是:在回调里提交一个work item,让work queue线程去执行真正的任务。
4.4 实用技巧与避坑
讲几个我实际用下来的心得。
第一,定时器状态查询。 你可以用k_timer_status_get()获取定时器到期的次数。这个函数返回自上次调用以来定时器到期的次数。如果你在回调里调用它,返回的通常是1。但如果你在别的地方调用,可能得到多个累积值。
uint32_t remaining = k_timer_remaining_get(&my_timer);
这个函数返回定时器剩余时间,单位是系统时钟节拍。如果你想知道还有多少毫秒,需要自己换算。
第二,定时器同步。 有时候你需要等待定时器到期。Zephyr提供了k_timer_status_sync(),它会阻塞当前线程直到定时器到期。这个函数在单次定时器里特别有用——你可以让一个线程等待定时器,到期后继续执行。
第三,资源限制。 Zephyr的定时器数量是有限制的,由CONFIG_TIMER_COUNT配置项决定。默认可能只有几个。如果你需要大量定时器,记得在prj.conf里把这个值调大。
- 定时器初始化后不会自动启动,需要调用k_timer_start
- 单次定时器的周期参数设为K_NO_WAIT或0
- 回调函数在中断上下文执行,要短小精悍
- 需要耗时操作时,用工作队列转发
- 定时器数量受配置限制,按需调整
好了,这一章的内容就这些。定时器是Zephyr里最基础也最实用的模块之一。下一章我们会讲PWM驱动,到时候你会发现,定时器和PWM经常配合使用。嗯,先把定时器玩熟了,后面就轻松了。