3、定时器回调函数机制

回调函数,说白了就是定时器到期后自动调用的那个函数。很多初学者觉得这玩意儿很简单——不就是个函数指针嘛,到期了调用一下呗。但实际项目中,回调函数里踩的坑可不少。我见过太多因为回调函数写得不对,导致系统莫名其妙死机或者任务卡死的案例了。

回调函数的执行上下文

这个问题很关键。你得搞清楚:定时器回调函数到底在哪个上下文里执行?

FreeRTOS的软件定时器回调函数,是在一个叫"定时器服务任务"(Timer Service Task)的上下文里执行的。这个任务由内核自动创建,专门用来处理所有软件定时器的到期事件。

什么意思呢?就是说你的回调函数并不是在创建定时器的任务里跑的,也不是在中断里跑的,而是在一个独立的系统任务里。这个任务的优先级由 configTIMER_TASK_PRIORITY 配置,栈大小由 configTIMER_TASK_STACK_DEPTH 配置。

核心要点:所有定时器回调函数共享同一个任务上下文。这意味着:

  • 回调函数不能阻塞太久,否则会影响其他定时器的响应
  • 回调函数里不能调用会导致任务挂起的API
  • 回调函数执行期间,其他定时器到期事件会被排队等待

我举个例子你就明白了。假设你创建了三个定时器,周期分别是10ms、20ms、50ms。如果10ms那个回调函数执行了100ms,那20ms和50ms的回调就得排队等着。这期间系统看起来就像"卡住"了一样。

定时器回调函数执行上下文 定时器服务任务 (Timer Service Task) 定时器命令队列 (Timer Command Queue) 回调函数 A (定时器1) 回调函数 B (定时器2) 回调函数 C (定时器3) ⚠ 重要:所有回调函数在同一个任务上下文中串行执行 一个回调阻塞 → 所有定时器延迟响应

回调函数中的注意事项

嗯,这里要重点说说。回调函数里能做什么、不能做什么,是有严格限制的。我根据自己的项目经验,总结了几条铁律:

1. 执行时间要短

回调函数应该像"闪电"一样快。我个人习惯把回调函数控制在1ms以内。如果要做耗时操作,比如写Flash、发网络包,那就发个信号量或者消息队列给其他任务去处理。

我的经验:回调函数里只做"标记"和"通知"两类事。标记就是置个标志位,通知就是发个信号量。具体干活交给别的任务。

2. 注意临界区保护

回调函数里访问全局变量时,要考虑保护问题。因为回调函数可能被中断打断,也可能被更高优先级的任务抢占。我建议用 taskENTER_CRITICAL()taskEXIT_CRITICAL() 来保护共享数据。

// 正确的做法
void vTimerCallback(TimerHandle_t xTimer) {
    taskENTER_CRITICAL();
    g_shared_counter++;
    taskEXIT_CRITICAL();
    
    // 通知处理任务
    xSemaphoreGive(xSemaphore);
}

3. 不要假设执行顺序

多个定时器的回调函数执行顺序是不确定的。我曾经踩过这个坑:两个定时器分别控制前后两个动作,我假设先创建的定时器先执行,结果发现完全不是那么回事。后来老老实实加了个状态机来控制顺序。

不能在回调中调用的API

这部分是重点中的重点。FreeRTOS明确规定了哪些API不能在定时器回调函数里调用。我把它们分成三类:

类别 禁止调用的API 原因
阻塞类 vTaskDelay(), vTaskDelayUntil() 回调在定时器服务任务中执行,阻塞会拖死整个定时器系统
等待类 xQueueReceive() (带阻塞时间), xSemaphoreTake() (带阻塞时间) 同上,任何形式的阻塞都不允许
挂起类 vTaskSuspend(), vTaskResume() 可能导致定时器服务任务被挂起,系统定时器失效

⚠ 特别警告:千万不要在回调函数里调用 vTaskDelay()!我曾经见过一个同事这么写,结果整个系统的定时器全部停摆,所有周期性任务都乱了套。排查了整整两天才找到问题。

那回调函数里到底能调用哪些API呢?我列个清单:

  • xTimerReset() — 重置定时器,这个可以
  • xTimerChangePeriod() — 修改周期,也可以
  • xQueueSend() — 发送消息到队列,注意要用不阻塞的方式
  • xSemaphoreGive() — 释放信号量,这个很常用
  • vTaskNotifyGiveFromISR() — 任务通知,注意这个函数名带FromISR,但回调里也能用

说白了,回调函数里只能做"非阻塞"的操作。你想想看,定时器服务任务就像一条高速公路,你的回调函数就是上面跑的车。如果有一辆车停下来不动了,后面的车全得堵着。

最佳实践总结:

  1. 回调函数执行时间控制在1ms以内
  2. 只做标志位设置、信号量释放、消息发送等轻量操作
  3. 绝对不要调用任何可能阻塞的API
  4. 访问共享资源时加临界区保护
  5. 耗时操作通过消息队列委托给其他任务

我记得有一次做工业控制项目,需要在10ms定时器回调里采集传感器数据。一开始我直接在回调里做了数据滤波和计算,结果发现定时器经常"丢 tick"。后来改成回调里只把原始数据扔进队列,让另一个任务去处理,问题就解决了。

这个教训让我养成了一个习惯:回调函数越短越好,最好只有一行代码。要么发个信号量,要么置个标志位,完事。