Skip to content

Software Timer(软件定时器)

用一句话说明这篇笔记最终要解决的问题。

1. 问题与背景

Software Timer A ─┐ Software Timer B ─┤ Software Timer C ─┤ Software Timer D ─┘ │ │ 到期 ↓ ┌──────────────────────┐ │ Timer Service Task │ │ │ │ 执行对应Callback │ └──────────────────────┘

所有 Software Timer Callback 默认都运行在同一个 Timer Service Task 的上下文中。 Software Timer Callback 必须短、小、非阻塞。

Software Timer Callback 和 ISR 的思想非常相似 工程原则很像:

ISR → 快速记录 / 通知 → Task干重活

Timer Callback → 快速更新 / 通知 → Task干重活

区别: ISR → Handler Mode → 中断上下文 → FromISR API

Timer Callback → Timer Service Task上下文 → 普通Task环境 → 普通Task API

所以 Callback 可以调用普通 RTOS API。 但:能阻塞,不代表应该阻塞。

2. 必要基础

Software Timer 分两大类 单次定时器 周期自动重载定时器

调用:xTimerStart(timer, ...); 并不是当前 Task 直接跑到 timers.c:“好,我现在亲自把这个 Timer 插入活动链表。”

FreeRTOS Software Timer 有个非常重要的机制:Timer Command Queue

结构:

你的Task │ │ xTimerStart() ↓ ┌────────────────────┐ │ Timer Command Queue│ └────────────────────┘ │ ↓ ┌────────────────────┐ │ Timer Service Task │ └────────────────────┘ │ ↓ 真正处理Timer状态

所以:xTimerStart()本质上更接近:向 Timer Service Task 发送一条“请启动这个 Timer”的命令。

把 Timer 状态修改集中到一个唯一执行者——Timer Service Task。 其他地方:只发Command

BaseType_t ret; ret = xTimerStart(timer, 0); 返回:pdPASS

更准确: 启动 Timer 的命令已经成功发送到 Timer Command Queue。 至于 Timer Service Task 什么时候真正处理这条命令,还受到: Timer Task Priority 调度状态 队列中已有多少命令

影响。

Timer API 的失败不一定意味着“Timer对象坏了”,也可能是 Timer Command Queue 无法接收这条命令。不排除这个队列已经满了的情况

3. 底层原理

Timer Service Task 本身也有 Priority 和 Stack

配置里通常有: configTIMER_TASK_PRIORITY configTIMER_TASK_STACK_DEPTH configTIMER_QUEUE_LENGTH

所以: Timer Callback跑在哪个Stack? → Timer Service Task的Stack 不是每个 Timer 自己一块 Stack。

这就是 Software Timer 节省 RAM 的关键原因之一。 但这也带来一个风险:所有 Callback 共用同一块 Stack

假设: TimerCallbackA() { uint8_t buffer[500]; }

那这 500B:吃的是 Timer Service Task Stack 另一个 Callback:TimerCallbackB()也是同一个 Stack。

虽然它们一般不是同时执行,但: 最大 Callback 调用路径决定 Timer Task 的 Stack 风险。 所以如果 Timer Callback 里:

大数组 sprintf printf浮点 复杂协议

不仅慢,还可能:把 Timer Service Task Stack 搞爆。 这时候 Stack Overflow Hook 可能告诉你: Tmr Svc或者对应版本里的 Timer Task 名字爆栈。

Timer Service Task 的 Priority 要根据 Timer Callback 的时间敏感程度和系统任务设计决定,而不是无脑最高或最低。

4. 数据流与调用流程

Software Timer仍然基于:FreeRTOS Tick+Task Scheduler 比如:1 Tick = 1ms Timer 到期意味着: Kernel认为这个Timer时间到了 ↓ Timer Service Task需要得到CPU ↓ Callback才真正执行 不是精准的定时器,所以,绝对不要用 Software Timer 干 PWM同步 ADC采样点 微秒级时序 严格硬实时

5. 最小验证实验

Hardware Timer STM32: TIM1 TIM2 TIM3 TIM7 ...

真实硬件计数器: Timer Clock PSC ARR CNT CCRx

可以做到: μs级 硬件PWM 输入捕获 输出比较 编码器 Trigger IRQ Software Timer

FreeRTOS 内核对象: 依赖Tick 依赖Scheduler 依赖Timer Service Task

适合: ms级 / 秒级逻辑定时 系统超时 状态管理 周期轻任务

场景更倾向
500ms LED闪烁Software Timer
2s通信超时One-shot Software Timer
3s启动超时One-shot Software Timer
蜂鸣器500ms后关闭Software Timer
100Hz复杂传感处理Task / Event driven
UART DMA数据到达Notification / Task
1kHz控制循环高优先级Task或硬件同步
20kHz电流采样Hardware Timer/ADC/ISR
长时间协议重连Task
简单周期统计Software Timer

Task / ISR │ │ Start / Stop / Reset / ChangePeriod ↓ ┌─────────────────────────┐ │ Timer Command Queue │ └─────────────────────────┘ │ ↓ ┌─────────────────────────┐ │ Timer Service Task │ │ │ │ 管理活动Timer │ │ 等待下一个Expiry │ │ 处理Command │ │ 执行Callback │ └─────────────────────────┘ │ Timer到期 ↓ Timer Callback │ ↓ 短、小、不阻塞 │ 必要时通知业务Task

6. 调试观察

7. 常见错误与根因

8. 真实工程中的用途

9. 我的结论

Software Timer 不是 Task,而是由 Timer Service Task 统一管理的定时对象。 所有 Timer Callback 默认共享 Timer Service Task 的执行上下文和 Stack。 Timer Callback 必须短小、尽量非阻塞,复杂业务应通知真正的业务 Task。 Start/Stop/Reset 等操作通常通过 Timer Command Queue 交给 Timer Service Task 串行处理。 Software Timer 是基于 RTOS Tick 和调度的软件逻辑定时,不适合微秒级或严格硬实时控制。

xTimerStart(xTimer,pdMS_TO_TICKS(100)); 第二个参数表示: 如果 Timer Command Queue 满了,我最多阻塞100ms等待队列空间。

Software Timer 到期精度不仅由 Tick 决定,也由 Timer Service Task 能否及时获得 CPU 决定。

所以“Timer 到期”和“Callback 执行”之间也存在延迟 理论 Expiry ↓ ----|---------|======== ↑ Timer Task Running ↓ Callback

中间:Expiry → Callback Start

可能受到:

高优先级Task ISR Critical Section Timer Task Priority 前面Callback执行时间 Command处理

影响。

所以:Software Timer 也有 Jitter

推荐模式 Callback: void CommTimeoutCallback(TimerHandle_t timer) { xTaskNotifyGive(CommTaskHandle); }

或者: void BuzzerTimeoutCallback(TimerHandle_t timer) { App_PostEvent(EVENT_BUZZER_TIMEOUT); }

核心: Callback ↓ 只产生一个状态变化/事件 ↓ 立即返回

复杂处理: 交给真正的业务Task

这和 ISR 的架构原则已经统一了,现在可以建立一个很漂亮的工程原则:

ISR Timer Callback 其他短触发点 ↓ 尽量只做:记录 / Set Bit / Queue / Notify ↓ 业务Task ↓ 执行复杂状态机

这样: 中断上下文短 Timer Task短 业务职责集中

系统就更容易维护。

Timer Service Task 是系统里的一个真实 Task 所以你以后 Debug 时可以看到类似:Tmr Svc 它有自己的: TCB Stack Priority Task State

也可能: Blocked Ready Running Stack Overflow

如果某个 Callback 写炸了,Stack Overflow Hook报的可能不是你业务名字,而是:Timer Service Task

这时候第一反应就应该是: “哪个 Timer Callback 在这个公共 Task Stack 上干了重活?”

这就是前面 Stack Overflow 知识和 Software Timer 的真正连接。

Software Timer 的源码主线你现在可以记成这一条 xTimerStart() ↓ xTimerGenericCommand() ↓ Timer Command Queue ↓ Timer Service Task 被唤醒 ↓ 处理 START Command ↓ 计算 Expiry Tick ↓ 插入 Timer Active List ↓ Timer Service Task 再次 Block ↓ 等:Command 或最近Timer Expiry ↓ Timer到期 ↓ 移出/重载 ↓ 调用 Callback ↓ 继续处理其他Timer

这条链如果理解了,Software Timer 就已经不是黑盒了。

这一节真正值得学走的工程思想 不是 xTimerStart() 怎么写,而是:

  1. Single Owner Timer Service Task

统一拥有 Timer 内核状态。 2. Command Queue

其他执行流不直接修改共享复杂状态,而是:发消息 3. Event + Timeout Blocking

Timer Task 平时:不轮询,而是:等待命令或最近到期时间

  1. Callback 只是公共 Worker Context 里的函数 所以:不能当独立Task乱用

  2. 软件定时器依然受调度影响 因此:适合逻辑时间,不适合严格硬件时序

FreeRTOS 内核与工程实践