2047 字
10 分钟
17-1 软件定时器解释
软件定时器
软件定时器
软件定时器服务任务(Timer Daemon Task)自身可以进入阻塞状态,并且它的大部分时间都处于阻塞状态,这是其高效工作的关键。但是,它的阻塞是由 FreeRTOS 内核智能控制的,而不是由用户的定时器回调函数决定的。
让我们来详细分解这一点。
一、服务任务自身的阻塞:核心工作机制
软件定时器服务任务的主体是一个无限循环,其核心逻辑如下:
void prvTimerTask( void *pvParameters ) { TickType_t xNextExpireTime; BaseType_t xListWasEmpty;
for( ;; ) { // 1. 计算下一个定时器到期的时间 xNextExpireTime = prvGetNextExpireTime( &xListWasEmpty );
// 2. 【关键阻塞点】等待直到下一个定时器到期 OR 收到命令 xQueueReceive( xTimerQueue, &xMessage, xNextExpireTime - xTaskGetTickCount() );
// 3. 处理收到的命令(启动、停止定时器等)或处理到期定时器 prvProcessReceivedCommands(); prvProcessExpiredTimers(); // 这里会执行回调函数 }}在这个流程中,服务任务会在步骤 2 主动进入阻塞状态:
- 阻塞的原因:它调用
xQueueReceive()来等待一个名为xTimerQueue的消息队列。 - 阻塞的超时时间:这个超时时间被设置为“距离下一个定时器到期还有多久”。
- 如果没有任何定时器,它会无限期阻塞(
portMAX_DELAY)。 - 如果有定时器,它会精确地阻塞到下一个定时器到期的那一刻。
- 如果没有任何定时器,它会无限期阻塞(
这种阻塞是积极且必要的:
- 节省CPU资源:在等待期间,该任务不消耗任何CPU时间,处理器可以执行其他任务或进入低功耗模式。
- 精准定时:通过计算精确的超时时间,内核可以确保在定时器到期的那一刻准时唤醒该任务。
二、问题的核心:回调函数执行期间绝不能阻塞
用户最关心的问题——“定时器服务任务可以阻塞吗?”——通常指的不是它自身的等待机制,而是:在执行用户定义的定时器回调函数时,这个任务可以阻塞吗?
答案是:绝对不可以。
从上面的代码可以看到,回调函数是在 prvProcessExpiredTimers() 函数中被调用的。这意味着:
- 回调函数在定时器服务任务的上下文中运行。
- 当回调函数在执行时,整个服务任务卡在
prvProcessExpiredTimers()函数调用里。 - 如果回调函数中调用了
vTaskDelay(),xSemaphoreTake(),xQueueReceive()等阻塞函数,整个定时器服务任务就会被挂起。
这将导致灾难性后果:
- 定时严重失准:所有其他已到期和即将到期的定时器回调都无法执行,因为服务任务被一个回调函数阻塞了。
- 命令响应延迟:其他任务发送的启动、停止定时器等命令也无法被处理(
prvProcessReceivedCommands()要等当前回调执行完才能被调用)。 - 系统可靠性降低:可能引发整个系统的时间序混乱。
总结与类比
| 场景 | 是否可以阻塞? | 解释 |
|---|---|---|
| 服务任务等待下一个到期事件时 | 可以且应该 | 这是其高效、节能工作的核心机制。它阻塞在队列上,等待命令或定时器到期。 |
| 服务任务执行用户回调函数时 | 绝对禁止 | 回调函数必须遵循“快进快出”原则。任何阻塞操作都会冻结整个定时器系统。 |
一个很好的类比:
把 软件定时器服务任务 想象成一个 公司的前台接待员。
- 他的主要工作状态是“等待”:他大部分时间坐在前台,等待电话铃响(
xQueueReceive阻塞),或者查看日程表等待下一个预约(计算xNextExpireTime)。这种“等待”是他的本职工作,是高效的。 - 当有事情处理时(回调函数):当一个访客到来(定时器到期),他需要处理访客的需求(执行回调函数)。这个处理必须快速,比如登记一下,然后通知相应的同事(通过队列/信号量通知其他任务)。
- 灾难性的行为:如果接待员亲自带访客去楼上参观整个公司(在回调函数中执行耗时操作),那么前台就没人了。接下来的电话没人接,新的访客没人接待,整个公司的对外接口就瘫痪了。
因此,正确的做法永远是: 在定时器回调函数中只做最紧急、最简短的操作(如设置一个标志、发送一个通知、投递一个消息到队列),然后立刻返回。所有耗时和可能阻塞的工作,都交给其他专门的任务去处理。
回调函数
软件定时器的回调函数是在一个独立的、高优先级的守护任务(Daemon Task)中执行的,因此回调函数本身绝对不允许阻塞。但是,这个限制背后的原因和影响需要详细解释。
一、为什么软件定时器回调函数不能阻塞?
这完全是由其执行上下文决定的。
- 执行环境:定时器守护任务 (Timer Daemon Task)
FreeRTOS 的软件定时器并非在硬件中断中执行,也不是在调用
xTimerStart()的任务中执行。它们是由一个叫做 “定时器服务任务” 或 “定时器守护任务” (默认为prvTimerTask)统一调度的。- 这个任务就像一个专门管理所有定时器的“管理员”。
- 当某个定时器到期时,这个“管理员”任务就会从它的等待状态中醒来,然后依次执行所有已到期定时器的回调函数。
- 关键问题:单线程执行
在默认配置下,所有定时器的回调函数都是在这个唯一的“管理员”任务中 顺序执行的(一个接一个)。
- 想象一下:如果
回调函数A内部调用了vTaskDelay()或xQueueReceive()并发生了阻塞,那么整个“管理员”任务就会被挂起。 - 这意味着:
回调函数B、回调函数C以及所有其他定时器的回调函数,即使已经到期,也必须等待回调函数A从阻塞中醒来并执行完毕后才能被执行。
- 想象一下:如果
- 严重后果
- 定时严重不准:后续所有定时器的触发时间都会因为一个回调函数的阻塞而被严重推迟。
- 系统响应性下降:定时器服务任务无法处理新的定时器命令(如启动、停止新定时器),因为它在忙着阻塞。
- 可能引发死锁:如果阻塞的函数还需要等待由其他任务释放的资源,而那个任务又因为等不到定时器信号而无法运行,就可能造成死锁。
结论:为了让这个“管理员”任务能高效、及时地处理所有定时器,FreeRTOS 强制要求所有回调函数必须设计为非阻塞的。
二、软件定时器回调函数的“生存法则”
由于不能阻塞,回调函数的设计必须遵循以下原则:
- 快进快出 (Run-to-Completion) 回调函数应尽可能短小精悍,只完成最紧急、最必要的操作,然后立刻返回。
- 通过通信解耦
这是最重要的模式。回调函数不应该自己完成耗时工作,而应该:
- 发送通知:使用
xTaskNotify()或xTaskNotifyFromISR()唤醒一个专门处理耗时工作的任务。 - 发送消息:使用
xQueueSend()或xQueueSendFromISR()向一个队列发送数据或事件标志,让另一个任务从队列中取出并处理。 - 设置事件标志:使用
xEventGroupSetBits()或xEventGroupSetBitsFromISR()通知其他任务。
- 发送通知:使用
- 避免使用非线程安全的函数
例如
printf()。如果必须使用,要确保有互斥锁保护,但要注意获取互斥锁也可能导致阻塞(虽然时间极短,但仍需谨慎)。
17-1 软件定时器解释
https://blog.luxunacc.xyz/posts/notes/17-1_软件定时器解释/