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_WAIT0,那就是单次定时器。

停止定时器更简单:

k_timer_stop(&my_timer);

嗯,这里要注意:停止一个已经停止的定时器是安全的,不会报错。我在项目中遇到过有人反复调用stop,结果代码里加了一堆判断,其实没必要。

4.2 定时器类型:周期与单次

Zephyr的定时器就两种模式:周期模式和单次模式。怎么区分?看启动时的第三个参数。

模式 启动参数(周期时间) 行为
单次 K_NO_WAIT0 延迟到期后执行一次回调,然后停止
周期 大于0的毫秒数 延迟到期后执行回调,然后每隔周期时间再执行

举个例子。单次定时器常用于“超时检测”——比如等待一个外设响应,如果100ms没反应就报错。周期定时器则用于“心跳任务”——比如每10ms采集一次传感器数据。

我曾经在一个电机控制项目里,用周期定时器生成PWM的基准时基。当时需要50μs的精度,Zephyr的定时器配合硬件定时器,跑得很稳。不过要注意,软件定时器的精度受系统时钟节拍影响,默认一般是10ms或1ms。如果你需要微秒级的定时,得用硬件定时器或者PWM模块。

避坑指南: 我曾经在单次定时器里忘了设置周期参数为K_NO_WAIT,结果定时器变成了周期模式,回调函数一直触发,把任务队列塞爆了。排查了半天才发现是参数传错了。所以,启动定时器前,一定确认好你要的是单次还是周期。

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线程去执行真正的任务。

我的经验: 在工业控制中,定时器回调里尽量不要直接操作硬件。比如PWM的占空比更新,我习惯在回调里设置一个标志位,然后在主循环或另一个线程里读取标志位并执行操作。这样能避免中断嵌套和资源竞争。

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经常配合使用。嗯,先把定时器玩熟了,后面就轻松了。