3、定时器回调函数机制
回调函数,说白了就是定时器到期后自动调用的那个函数。很多初学者觉得这玩意儿很简单——不就是个函数指针嘛,到期了调用一下呗。但实际项目中,回调函数里踩的坑可不少。我见过太多因为回调函数写得不对,导致系统莫名其妙死机或者任务卡死的案例了。
回调函数的执行上下文
这个问题很关键。你得搞清楚:定时器回调函数到底在哪个上下文里执行?
FreeRTOS的软件定时器回调函数,是在一个叫"定时器服务任务"(Timer Service Task)的上下文里执行的。这个任务由内核自动创建,专门用来处理所有软件定时器的到期事件。
什么意思呢?就是说你的回调函数并不是在创建定时器的任务里跑的,也不是在中断里跑的,而是在一个独立的系统任务里。这个任务的优先级由 configTIMER_TASK_PRIORITY 配置,栈大小由 configTIMER_TASK_STACK_DEPTH 配置。
核心要点:所有定时器回调函数共享同一个任务上下文。这意味着:
- 回调函数不能阻塞太久,否则会影响其他定时器的响应
- 回调函数里不能调用会导致任务挂起的API
- 回调函数执行期间,其他定时器到期事件会被排队等待
我举个例子你就明白了。假设你创建了三个定时器,周期分别是10ms、20ms、50ms。如果10ms那个回调函数执行了100ms,那20ms和50ms的回调就得排队等着。这期间系统看起来就像"卡住"了一样。
回调函数中的注意事项
嗯,这里要重点说说。回调函数里能做什么、不能做什么,是有严格限制的。我根据自己的项目经验,总结了几条铁律:
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,但回调里也能用
说白了,回调函数里只能做"非阻塞"的操作。你想想看,定时器服务任务就像一条高速公路,你的回调函数就是上面跑的车。如果有一辆车停下来不动了,后面的车全得堵着。
最佳实践总结:
- 回调函数执行时间控制在1ms以内
- 只做标志位设置、信号量释放、消息发送等轻量操作
- 绝对不要调用任何可能阻塞的API
- 访问共享资源时加临界区保护
- 耗时操作通过消息队列委托给其他任务
我记得有一次做工业控制项目,需要在10ms定时器回调里采集传感器数据。一开始我直接在回调里做了数据滤波和计算,结果发现定时器经常"丢 tick"。后来改成回调里只把原始数据扔进队列,让另一个任务去处理,问题就解决了。
这个教训让我养成了一个习惯:回调函数越短越好,最好只有一行代码。要么发个信号量,要么置个标志位,完事。