软件定时器 (Software Timer) 是能够处理很多“延时执行”或“周期性执行”的任务,而不需要占用宝贵的硬件资源。
补充知识点学习:
回调函数:
“回调函数” (Callback Function) 就是你写好一个函数,但是你不用去调用它,而是把这个函数像“电话号码”一样留给系统,让系统在“特定事情发生”的时候回头去调用它。
一:. 什么是软件定时器?
在单片机里,我们通常有两种定时器:
-
硬件定时器 (Hardware Timer): 这是芯片里面实实在在的电路(比如 STM32 的 TIM1, TIM2)。它们极其精准,通常用于产生 PWM 波形或者极短时间的精确计时(纳秒/微秒级)。缺点是数量有限。
-
软件定时器 (Software Timer): 这是 FreeRTOS 用代码模拟出来的定时器。它基于系统的 "Tick"(心跳)来计时。优点是只要内存够,你想创建多少个都可以。
通俗比喻:
-
硬件定时器就像是一个专业的秒表,适合用来测百米赛跑。
-
软件定时器就像是日常的闹钟,适合用来提醒你“10分钟后关灯”或者“每隔1秒闪烁一下LED”。
想象一下,你(作为 CPU)正在看书(执行主程序)。你需要做两件事:
-
30分钟后去关火(单次任务)。
-
每隔 1 小时喝一次水(周期任务)。
你不会一直盯着墙上的钟表看(那是轮询,非常浪费精力)。相反,你在手机上设了两个闹钟。闹钟响了,你就停下看书,去关火或喝水。
2. 软件定时器 vs 硬件定时器
初学者容易混淆这两者,它们的区别很重要:
| 特性 | 硬件定时器 (TIM) | 软件定时器 (FreeRTOS Timer) |
| 资源来源 | 单片机内部的硬件外设 | 基于系统滴答 (SysTick) 的软件模拟 |
| 精度 | 极高 (微秒/纳秒级) | 一般 (依赖于系统节拍,通常是毫秒级) |
| 执行环境 | 中断服务函数 (ISR) 中 | RTOS 的守护任务 (Daemon Task) 中 |
| 用途 | 电机控制、高频采样 | 按键消抖、LED闪烁、低频周期任务 |
| 数量限制 | 只有几个 (如 TIM1-TIM8) |
理论上无限 (受内存限制) |
二:软件定时器的特性
1. 时基依赖性 (Tick Dependency)
这是软件定时器精度的根本来源。
-
特性: 软件定时器不是靠晶振直接计数的,它是靠 系统节拍 (SysTick) 来计数的。
-
意味着什么:
-
它的精度取决于你在
FreeRTOSConfig.h里设置的configTICK_RATE_HZ(通常是 1000Hz,即 1ms)。 -
限制: 你无法创建一个 0.5ms 或者 100us 的软件定时器。最小单位就是 1 个 Tick。
-
抖动: 如果系统极其繁忙,导致 SysTick 处理稍有延迟,软件定时器的触发也可能产生极其微小的抖动(通常可忽略)。
-
2. 两种核心模式
就像手机闹钟一样,FreeRTOS 的定时器也有两种最常用的模式 :
-
单次定时器 (One-shot timers):
-
特点: “响”一次就结束了。
-
场景: 比如设备启动 5秒后关闭欢迎界面。
-
状态变化: 启动 -> 运行(Running) -> 时间到执行回调 -> 变为冬眠(Dormant)不再运行。
-
-
自动加载定时器 (Auto-reload timers):
-
特点: “响”完之后自动重置,过一段时间再“响”,无限循环。
-
场景: 比如每隔 1秒闪烁一次 LED 灯,或者每隔 1小时保存一次数据。
-
状态变化: 启动 -> 运行 -> 时间到执行回调 -> 自动重新启动 -> 继续运行。
-
3. 执行上下文:守护任务 (Daemon Task)
这是最重要、也是最容易出错的特性。
3.1概念
-
特性: 当定时器时间到了,你的回调函数不是在中断里跑,也不是在你的
main函数或创建它的任务里跑,而是跑在一个叫 RTOS 守护任务 (Daemon Task/Timer Service Task) 的专用任务里。 -
FreeRTOS 为了安全和效率,专门创建了一个后台任务,叫做 “守护任务” (Daemon Task) 。
-
守护任务: 所有的软件定时器回调函数,都是在这个任务里“排队”执行的。
-
命令队列: 当你调用
xTimerStart()启动定时器时,其实是往一个 “定时器命令队列” 里发了一条命令。守护任务从队列里取出命令,去处理定时器的启动、停止或复位 。
注意: 因为所有定时器都共用这个“守护任务”,所以你的定时器回调函数决不能写死循环,也不能调用会导致阻塞的函数(比如 vTaskDelay),否则会卡死其他所有的定时器 。
3.2 为什么需要守护任务?
你可能会问:“为什么不直接在系统时钟中断(Tick Interrupt)里执行定时器回调函数呢?”
-
原因: 硬件中断(ISR)必须非常快,不能耗时太久。如果你的定时器回调函数里有一些耗时的操作(比如打印日志、计算数据),放在中断里执行会严重影响系统的实时性,甚至导致系统崩溃 。
-
解决方案: FreeRTOS 专门创建了一个任务(即守护任务),把这些“杂活”从中断里剥离出来,放在这个任务里慢慢做。
3.3. 它是如何工作的?(核心机制)
你可以把“守护任务”想象成一个**“定时器管家”**。它主要做两件事:
-
处理命令: 当你在其他任务中调用
xTimerStart(启动)、xTimerStop(停止)等函数时,本质上是在给这个“管家”发指令 。 -
执行回调: 当定时器时间到了,“管家”会负责去调用你写好的回调函数 。
交互流程:定时器命令队列 (Timer Command Queue)
当你调用 API 函数时,其实是通过一个队列与守护任务通信的。
-
你的任务: 调用
xTimerStart()-> 发送命令到“定时器命令队列” -> 任务继续运行(或者因队列满而阻塞) -
守护任务: 从“定时器命令队列”取出命令 -> 真正地去启动定时器 -> 如果时间到了,执行回调函数 。
关键点: 这就是为什么
xTimerStart函数里有一个xTicksToWait参数。这个参数不是等待定时器启动的时间,而是等待命令写入队列的时间。如果队列满了,你的任务需要等待“管家”把队列里的旧命令处理完,腾出空间 。
3.4定时器回调函数决不能阻塞!
-
绝对禁止: 调用
vTaskDelay()。 -
绝对禁止: 调用会阻塞的
xQueueReceive(除非等待时间设为0)。 -
后果: 如果你在回调函数里“睡”了 1 秒,那么守护任务就会停工 1 秒。在这 1 秒内,系统中所有其他的软件定时器都无法被触发,命令队列也无法处理,整个定时器系统就会“卡死”。
总结
-
身份: 它是 FreeRTOS 自动创建的一个后台任务,专门管理软件定时器。
-
通信: 我们通过“命令队列”发指令指挥它干活。
-
禁忌: 它的回调函数里千万不能有阻塞代码
3.5守护任务的优先级和队列长度
在 FreeRTOS 中,守护任务(Daemon Task,旧称 Timer Server)虽然负责处理所有的软件定时器,但它本质上仍然是一个普通的 FreeRTOS 任务。这意味着它的运行完全遵循 FreeRTOS 的标准调度规则:只有当它是就绪态中优先级最高的任务时,它才会运行
守护任务有两个核心配置参数决定了它的行为表现:优先级和队列长度。
1. 守护任务的优先级 (configTIMER_TASK_PRIORITY)
守护任务的优先级在 FreeRTOSConfig.h 中通过 configTIMER_TASK_PRIORITY 进行配置 。这个优先级的设置直接决定了定时器命令处理的“实时性”。
文档中使用了两个生动的例子来说明优先级的影响:
情况 A:守护任务优先级 低于 当前用户任务
这是“你忙完了我再做”的模式。
-
发送命令: 你的任务(Task1)调用
xTimerStart()启动定时器。 -
入队: “启动”命令被放入命令队列,守护任务从阻塞态变为就绪态 。
-
延迟执行: 但因为守护任务优先级低,它抢不过 Task1,所以它只能在就绪列表中排队。Task1 继续运行,直到它自己阻塞(比如调用
vTaskDelay)或者时间片用完 4。 -
最终处理: 只有等 Task1 让出 CPU,守护任务才得以运行,从队列取出命令并真正启动定时器
-
后果: 定时器的实际启动时间点(tX)会比你调用函数的时刻晚,导致计时存在误差。
情况 B:守护任务优先级 高于 当前用户任务
这是“插队立刻做”的模式。
-
发送命令: 你的任务(Task1)调用
xTimerStart()6。 -
抢占: 命令一入队,高优先级的守护任务立刻就绪,并抢占(Preempt)了 Task1 的运行权 7。
-
立即处理: 守护任务马上处理命令,启动定时器。处理完后,它重新阻塞,Task1 才恢复运行 8。
-
后果: 定时器几乎在你调用 API 的瞬间就启动了,计时非常精准。
-
最佳实践: 为了保证定时器的准时性,通常建议将
configTIMER_TASK_PRIORITY设置得相对较高。
2. 定时器命令队列长度 (configTIMER_QUEUE_LENGTH)
守护任务不只是盯着时间看,它还要处理来自其他任务的“指令”(如启动、停止、复位定时器)。这些指令是通过一个队列传送的,这个队列的深度由 configTIMER_QUEUE_LENGTH 定义 9。
为什么队列长度很重要?
当你调用 xTimerStart(xTimer, xTicksToWait) 时,实际上是在往这个队列里写数据。
-
如果队列没满: 命令写入成功,函数返回
pdPASS。 -
如果队列满了:
-
这说明守护任务来不及处理堆积的命令(可能是因为守护任务优先级太低,或者瞬间爆发了太多定时器操作)。
-
此时,你的任务会进入阻塞状态,等待队列腾出空间。等待的时间由参数
xTicksToWait决定 10101010。 -
如果超时还没空间,函数返回
pdFAIL,定时器启动失败。
-
总结:调度示意图
守护任务的工作就是不断在“处理命令”和“执行回调”之间循环:
-
处理命令: 从队列里取出
start,stop等命令并执行。 -
执行回调: 检查是否有定时器超时,如果有,执行其回调函数 11。
| 配置项 | 建议 | 影响 |
| 优先级 | 设为较高 | 决定了启动/停止命令的响应速度,以及回调函数是否准时执行。 |
| 队列长度 | 根据业务量 | 决定了短时间内能并发处理多少个定时器操作命令。设太小会导致 API 调用阻塞甚至失败。 |
它的优先级决定了定时器的响应速度:
- 如果优先级较低: 当你的用户任务(优先级高)一直在运行时,守护任务抢不到 CPU,那么即使定时器时间到了,回调函数也无法执行,导致定时器“不准” 。
- 如果优先级较高: 守护任务会抢占用户任务来处理定时器,保证了定时器的准时性 。
通常建议将守护任务的优先级设置得比较高,以保证定时器的实时性。
意味着什么:
-
-
堆栈共享: 所有的软件定时器回调函数,公用同一个堆栈。这个堆栈大小由
configTIMER_TASK_STACK_DEPTH决定。如果你在回调函数里定义了巨大的局部数组,可能会导致这个任务栈溢出。 -
优先级影响: 守护任务也是任务,它也有优先级 (
configTIMER_TASK_PRIORITY)。-
如果它的优先级低,而你有一个高优先级任务一直在跑,定时器回调就会被“饿死”(推迟执行)。
-
通常建议将守护任务优先级设得相对高一些。
-
-
3. 命令队列机制 (Command Queue)
当你调用 xTimerStart() 或 xTimerStop() 时,其实并不是立即修改定时器的状态。
-
特性: 这些函数其实是向一个 “定时器命令队列” 发送了一条消息(命令)。
-
流程:
-
你的任务调用
xTimerStart()。 -
这个命令被扔进队列。
-
守护任务从队列里取出命令。
-
守护任务实际去修改定时器的链表和状态。
-
-
意味着什么:
-
阻塞时间:
xTimerStart(handle, 100)中的100不是定时器的延时,而是如果队列满了,你的任务愿意等多久把命令塞进去。 -
异步性: 严格来说,启动操作有一点点极微小的滞后(直到守护任务读取命令),但在毫秒级应用中完全感觉不到。
-
4. 两种状态:休眠与运行 (Dormant vs Running)
不像硬件定时器那样始终通电就在跑,软件定时器非常节省资源。
-
休眠态 (Dormant):
-
当你创建了定时器 (
xTimerCreate) 但还没启动,或者单次定时器跑完了一次。 -
此时它仅仅占用一点内存来保存结构体,完全不占用 CPU 时间,系统调度器根本不理它。
-
-
运行态 (Running):
-
调用
xTimerStart后。 -
此时它被挂在了一个“激活链表”上,系统每次 Tick 中断都会检查它是否到期。
-
5. 高效的各种 Reset 机制
软件定时器不仅仅是“倒计时”,它非常灵活。
-
特性: 如果一个定时器已经在跑(比如剩 2秒 触发),你再次调用
xTimerReset(),它会重新装载初始值(变回 10秒)。 -
经典应用场景: “看门狗”或“背光控制”。
-
例如:手机背光设置为“10秒无操作熄灭”。
-
用户按一下键,你就调一次
xTimerReset。 -
只要用户一直按键,定时器就一直被复位,永远到不了 0,背光就一直亮。
-
一旦用户停手,10秒后定时器到期 -> 回调函数 -> 关灯。
-
总结:初学者避坑指南
基于以上特性,送你三个编写代码的建议:
-
回调函数要短: 别在里面
delay,因为你阻塞的是“守护任务”,会卡死所有其他定时器。 -
优先级要注意: 确保
configTIMER_TASK_PRIORITY设置得够高,否则高负载下定时器可能不准。 -
栈空间要够: 如果回调函数逻辑稍微复杂,记得去
FreeRTOSConfig.h把configTIMER_TASK_STACK_DEPTH调大一点。
1168

被折叠的 条评论
为什么被折叠?



