freertos学习笔记12--个人自用-第16章 软件定时器(software timer)

软件定时器 (Software Timer) 是能够处理很多“延时执行”或“周期性执行”的任务,而不需要占用宝贵的硬件资源。

补充知识点学习:

回调函数:
“回调函数” (Callback Function) 就是你写好一个函数,但是你不用去调用它,而是把这个函数像“电话号码”一样留给系统,让系统在“特定事情发生”的时候回头去调用它。

一:. 什么是软件定时器?

在单片机里,我们通常有两种定时器:

  • 硬件定时器 (Hardware Timer): 这是芯片里面实实在在的电路(比如 STM32 的 TIM1, TIM2)。它们极其精准,通常用于产生 PWM 波形或者极短时间的精确计时(纳秒/微秒级)。缺点是数量有限。

  • 软件定时器 (Software Timer): 这是 FreeRTOS 用代码模拟出来的定时器。它基于系统的 "Tick"(心跳)来计时。优点是只要内存够,你想创建多少个都可以。

    通俗比喻:

  • 硬件定时器就像是一个专业的秒表,适合用来测百米赛跑。

  • 软件定时器就像是日常的闹钟,适合用来提醒你“10分钟后关灯”或者“每隔1秒闪烁一下LED”。

想象一下,你(作为 CPU)正在看书(执行主程序)。你需要做两件事:

  1. 30分钟后去关火(单次任务)。

  2. 每隔 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. 它是如何工作的?(核心机制)

你可以把“守护任务”想象成一个**“定时器管家”**。它主要做两件事:

  1. 处理命令: 当你在其他任务中调用 xTimerStart(启动)、xTimerStop(停止)等函数时,本质上是在给这个“管家”发指令 。

  2. 执行回调: 当定时器时间到了,“管家”会负责去调用你写好的回调函数 。

交互流程:定时器命令队列 (Timer Command Queue)

当你调用 API 函数时,其实是通过一个队列与守护任务通信的。

  • 你的任务: 调用 xTimerStart() -> 发送命令到“定时器命令队列” -> 任务继续运行(或者因队列满而阻塞) 

  • 守护任务: 从“定时器命令队列”取出命令 -> 真正地去启动定时器 -> 如果时间到了,执行回调函数 。

    关键点: 这就是为什么 xTimerStart 函数里有一个 xTicksToWait 参数。这个参数不是等待定时器启动的时间,而是等待命令写入队列的时间。如果队列满了,你的任务需要等待“管家”把队列里的旧命令处理完,腾出空间 。

3.4定时器回调函数决不能阻塞!

  • 绝对禁止: 调用 vTaskDelay()

  • 绝对禁止: 调用会阻塞的 xQueueReceive(除非等待时间设为0)。

  • 后果: 如果你在回调函数里“睡”了 1 秒,那么守护任务就会停工 1 秒。在这 1 秒内,系统中所有其他的软件定时器都无法被触发,命令队列也无法处理,整个定时器系统就会“卡死”。

总结

  1. 身份: 它是 FreeRTOS 自动创建的一个后台任务,专门管理软件定时器。

  2. 通信: 我们通过“命令队列”发指令指挥它干活。

  3. 禁忌: 它的回调函数里千万不能有阻塞代码

3.5守护任务的优先级和队列长度

在 FreeRTOS 中,守护任务(Daemon Task,旧称 Timer Server)虽然负责处理所有的软件定时器,但它本质上仍然是一个普通的 FreeRTOS 任务。这意味着它的运行完全遵循 FreeRTOS 的标准调度规则:只有当它是就绪态中优先级最高的任务时,它才会运行

守护任务有两个核心配置参数决定了它的行为表现:优先级队列长度

1. 守护任务的优先级 (configTIMER_TASK_PRIORITY)

守护任务的优先级在 FreeRTOSConfig.h 中通过 configTIMER_TASK_PRIORITY 进行配置 。这个优先级的设置直接决定了定时器命令处理的“实时性”。

文档中使用了两个生动的例子来说明优先级的影响:

情况 A:守护任务优先级 低于 当前用户任务

这是“你忙完了我再做”的模式。

  1. 发送命令: 你的任务(Task1)调用 xTimerStart() 启动定时器。

  2. 入队: “启动”命令被放入命令队列,守护任务从阻塞态变为就绪态 。

  3. 延迟执行: 但因为守护任务优先级,它抢不过 Task1,所以它只能在就绪列表中排队。Task1 继续运行,直到它自己阻塞(比如调用 vTaskDelay)或者时间片用完 4。

  4. 最终处理: 只有等 Task1 让出 CPU,守护任务才得以运行,从队列取出命令并真正启动定时器 

  5. 后果: 定时器的实际启动时间点(tX)会比你调用函数的时刻晚,导致计时存在误差。

情况 B:守护任务优先级 高于 当前用户任务

这是“插队立刻做”的模式。

  1. 发送命令: 你的任务(Task1)调用 xTimerStart() 6。

  2. 抢占: 命令一入队,高优先级的守护任务立刻就绪,并抢占(Preempt)了 Task1 的运行权 7。

  3. 立即处理: 守护任务马上处理命令,启动定时器。处理完后,它重新阻塞,Task1 才恢复运行 8。

    • 后果: 定时器几乎在你调用 API 的瞬间就启动了,计时非常精准。

最佳实践: 为了保证定时器的准时性,通常建议将 configTIMER_TASK_PRIORITY 设置得相对较高。

2. 定时器命令队列长度 (configTIMER_QUEUE_LENGTH)

守护任务不只是盯着时间看,它还要处理来自其他任务的“指令”(如启动、停止、复位定时器)。这些指令是通过一个队列传送的,这个队列的深度由 configTIMER_QUEUE_LENGTH 定义 9。

为什么队列长度很重要?

当你调用 xTimerStart(xTimer, xTicksToWait) 时,实际上是在往这个队列里写数据。

  • 如果队列没满: 命令写入成功,函数返回 pdPASS

  • 如果队列满了:

    • 这说明守护任务来不及处理堆积的命令(可能是因为守护任务优先级太低,或者瞬间爆发了太多定时器操作)。

    • 此时,你的任务会进入阻塞状态,等待队列腾出空间。等待的时间由参数 xTicksToWait 决定 10101010。

    • 如果超时还没空间,函数返回 pdFAIL,定时器启动失败。

总结:调度示意图

守护任务的工作就是不断在“处理命令”和“执行回调”之间循环:

  1. 处理命令: 从队列里取出 start, stop 等命令并执行。

  2. 执行回调: 检查是否有定时器超时,如果有,执行其回调函数 11。

配置项建议影响
优先级设为较高决定了启动/停止命令的响应速度,以及回调函数是否准时执行。
队列长度根据业务量决定了短时间内能并发处理多少个定时器操作命令。设太小会导致 API 调用阻塞甚至失败。

它的优先级决定了定时器的响应速度:

  • 如果优先级较低: 当你的用户任务(优先级高)一直在运行时,守护任务抢不到 CPU,那么即使定时器时间到了,回调函数也无法执行,导致定时器“不准” 。

  • 如果优先级较高: 守护任务会抢占用户任务来处理定时器,保证了定时器的准时性 。

通常建议将守护任务的优先级设置得比较高,以保证定时器的实时性。

意味着什么:

    • 堆栈共享: 所有的软件定时器回调函数,公用同一个堆栈。这个堆栈大小由 configTIMER_TASK_STACK_DEPTH 决定。如果你在回调函数里定义了巨大的局部数组,可能会导致这个任务栈溢出。

    • 优先级影响: 守护任务也是任务,它也有优先级 (configTIMER_TASK_PRIORITY)。

      • 如果它的优先级,而你有一个高优先级任务一直在跑,定时器回调就会被“饿死”(推迟执行)。

      • 通常建议将守护任务优先级设得相对一些。

3. 命令队列机制 (Command Queue)

当你调用 xTimerStart()xTimerStop() 时,其实并不是立即修改定时器的状态。

  • 特性: 这些函数其实是向一个 “定时器命令队列” 发送了一条消息(命令)。

  • 流程:

    1. 你的任务调用 xTimerStart()

    2. 这个命令被扔进队列。

    3. 守护任务从队列里取出命令。

    4. 守护任务实际去修改定时器的链表和状态。

  • 意味着什么:

    • 阻塞时间: xTimerStart(handle, 100) 中的 100 不是定时器的延时,而是如果队列满了,你的任务愿意等多久把命令塞进去。

    • 异步性: 严格来说,启动操作有一点点极微小的滞后(直到守护任务读取命令),但在毫秒级应用中完全感觉不到。

4. 两种状态:休眠与运行 (Dormant vs Running)

不像硬件定时器那样始终通电就在跑,软件定时器非常节省资源。

  • 休眠态 (Dormant):

    • 当你创建了定时器 (xTimerCreate) 但还没启动,或者单次定时器跑完了一次。

    • 此时它仅仅占用一点内存来保存结构体,完全不占用 CPU 时间,系统调度器根本不理它。

  • 运行态 (Running):

    • 调用 xTimerStart 后。

    • 此时它被挂在了一个“激活链表”上,系统每次 Tick 中断都会检查它是否到期。

5. 高效的各种 Reset 机制

软件定时器不仅仅是“倒计时”,它非常灵活。

  • 特性: 如果一个定时器已经在跑(比如剩 2秒 触发),你再次调用 xTimerReset(),它会重新装载初始值(变回 10秒)。

  • 经典应用场景: “看门狗”或“背光控制”

    • 例如:手机背光设置为“10秒无操作熄灭”。

    • 用户按一下键,你就调一次 xTimerReset

    • 只要用户一直按键,定时器就一直被复位,永远到不了 0,背光就一直亮。

    • 一旦用户停手,10秒后定时器到期 -> 回调函数 -> 关灯。


总结:初学者避坑指南

基于以上特性,送你三个编写代码的建议:

  1. 回调函数要短: 别在里面 delay,因为你阻塞的是“守护任务”,会卡死所有其他定时器。

  2. 优先级要注意: 确保 configTIMER_TASK_PRIORITY 设置得够高,否则高负载下定时器可能不准。

  3. 栈空间要够: 如果回调函数逻辑稍微复杂,记得去 FreeRTOSConfig.hconfigTIMER_TASK_STACK_DEPTH 调大一点。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值