软件定时器(Software Timer)由 FreeRTOS 内核实现,不需要硬件定时器支持,可以创建任意数量。定时器到期时,内核调用你注册的回调函数。
两种定时器
| 特性 | One-shot(单次) | Auto-reload(自动重载) |
|---|---|---|
| 回调执行次数 | 仅一次 | 无限循环 |
| 到期后状态 | 进入休眠态(Dormant) | 自动重启,继续计时 |
| 典型用途 | 超时检测、单次延迟操作 | 周期性任务(心跳、轮询) |
| 重启方式 | 需手动调用 xTimerStart()/xTimerReset() | 自动,无需干预 |
创建和启动
TimerHandle_t xTimerCreate(
const char *pcTimerName, // 名称(调试用)
TickType_t xTimerPeriodInTicks, // 定时周期(tick数),用 pdMS_TO_TICKS() 转换
UBaseType_t uxAutoReload, // pdTRUE=自动重载,pdFALSE=单次
void *pvTimerID, // 定时器ID(回调中可用,区分多个定时器共用一个回调)
TimerCallbackFunction_t pxCallbackFunction // 回调函数指针
);
回调函数原型:
void vTimerCallback(TimerHandle_t xTimer);
启动定时器:
// xTicksToWait: 命令队列满时等待多久(一般填 0 或 portMAX_DELAY)
xTimerStart(xTimer, xTicksToWait);
创建后不会自动启动
xTimerCreate()只是创建定时器,创建后处于休眠态(Dormant),必须调用xTimerStart()才开始计时。
其他常用 API
xTimerStop(xTimer, 0); // 停止定时器,进入休眠态
xTimerReset(xTimer, 0); // 重置计时(从现在重新算周期),也能启动休眠定时器
xTimerDelete(xTimer, 0); // 删除定时器
Daemon Task(守护任务)
软件定时器的回调函数不在中断中执行,而是在一个叫做 Daemon Task(旧称 Timer Service Task)的特殊任务中执行。
调用 vTaskStartScheduler()
│
▼
内核自动创建 Daemon Task(你不需要手动创建)
│
├─ 平时阻塞在"定时器命令队列"上,不占 CPU
│
└─ 两种情况唤醒:
1. 命令队列来了新命令(Start/Stop/Reset/Delete...)
2. 有定时器到期了 → 执行回调函数
Daemon Task 的职责
- 处理定时器命令队列中的命令(start/stop/reset/delete/change period)
- 检查哪些定时器到期
- 串行执行到期定时器的回调函数
- 执行
xTimerPendFunctionCall()推送过来的函数(在 Daemon Task 上下文中执行一个函数)
关键配置
在 FreeRTOSConfig.h 中:
#define configUSE_TIMERS 1 // 启用软件定时器
#define configTIMER_TASK_PRIORITY (configMAX_PRIORITIES - 1) // 建议设为最高优先级
#define configTIMER_QUEUE_LENGTH 10 // 命令队列长度
#define configTIMER_TASK_STACK_DEPTH configMINIMAL_STACK_SIZE // 栈大小
定时器回调的重要规则
所有定时器回调共用同一个 Daemon Task,串行执行:
定时器A回调 ─┐ 定时器B回调 ─┤──▶ 全部在 Daemon Task 中依次执行 定时器C回调 ─┘ 时间轴:[回调A执行]──[回调B执行]──[回调C执行] ↑ 如果回调A执行很慢,B和C都要排队等,定时精度下降!因此回调函数必须短小快速,不能阻塞,不能调用
vTaskDelay()。
回调函数 vs 中断
回调函数本质上是 Daemon Task 中的普通函数调用,它和 ISR 有本质区别。详见 04-中断处理 > 中断与回调的区别。
回调函数中可以做的事比 ISR 多(可以用 printf、可以发队列),但因为它在 Daemon Task 中执行,不能阻塞,不能调用会让任务进入 Blocked 状态的 API(如 vTaskDelay()、带非零超时的 xQueueReceive() 等),否则会阻塞整个 Daemon Task,导致所有定时器都无法工作。
定时器状态机
xTimerStart() / xTimerReset()
┌────────────────────────────────┐
│ │
▼ │
┌────────┐ One-shot 到期 │
│ 运行态 │──────────────────────────┐│
│(Running)│ ▼▼
└────────┘ Auto-reload 到期 ┌────────┐
│ (自动重启) │ 休眠态 │
└─────────────────────────▶│(Dormant)│
└────────┘
- 休眠态(Dormant):存在但不计时,不占用 CPU 资源
- 运行态(Running):正在计时,到期后根据类型决定行为
xTimerChangePeriod:动态修改周期
BaseType_t xTimerChangePeriod(
TimerHandle_t xTimer,
TickType_t xNewPeriod,
TickType_t xTicksToWait
);
这个函数有两个使用场景:
场景一:定时器正在运行
调用前:定时器运行中,周期=1000ms,还有600ms到期
│
▼
xTimerChangePeriod(xTimer, pdMS_TO_TICKS(500), ...)
│
▼
不是从原来剩余600ms继续算!
而是从"调用这一刻"重新开始计时500ms
场景二:定时器在休眠态
调用前:定时器处于 Dormant(从未启动或被 Stop)
│
▼
xTimerChangePeriod(xTimer, pdMS_TO_TICKS(500), ...)
│
▼
自动进入 Running 态! ← 相当于顺带启动了定时器
500ms后触发回调
隐藏能力
xTimerChangePeriod()和xTimerReset()都能让休眠态的定时器进入运行态,调用后不需要再单独xTimerStart()。
xTimerReset:喂狗机制
Reset 不是”暂停后继续”,而是**“从现在重新开始计时”**,非常适合实现看门狗/超时检测:
定时器周期 = 6 ticks
t1 t2 t3 t4 t5 t6 t7 t8 t9 t10 t11 t12 t13 t14 t15
| | | | | | | | | | | | | | |
o o o ■
↑ ↑ ↑ ↑
启动 Reset Reset 回调执行
t1+6=t7(原本到期) t5+6=t11(重算) t9+6=t15(重算)
t5 Reset了, t9又Reset了 这次没Reset
不会在t7触发 不会在t11触发 t15触发回调(超时了!)
典型应用:
// 某个外设通信时"喂狗"
void vCommReset(void) {
xTimerReset(xCommTimeoutTimer, 0);
}
// 超时回调:通信出错,重连
void vCommTimeoutCallback(TimerHandle_t xTimer) {
vReconnectDevice();
}
只要持续收到数据/心跳,就 Reset 定时器,回调不会触发;一旦数据中断,定时器到期触发回调,执行错误处理。
定时器 ID 的用途
多个定时器可以共用同一个回调函数,通过 ID 区分:
// 创建两个定时器,共用同一个回调
TimerHandle_t xTimer1 = xTimerCreate("T1", pdMS_TO_TICKS(1000), pdTRUE, (void*)1, vCommonCallback);
TimerHandle_t xTimer2 = xTimerCreate("T2", pdMS_TO_TICKS(2000), pdTRUE, (void*)2, vCommonCallback);
void vCommonCallback(TimerHandle_t xTimer)
{
int id = (int)pvTimerGetTimerID(xTimer);
switch(id) {
case 1: /* T1 到期处理 */ break;
case 2: /* T2 到期处理 */ break;
}
}
下一步:06-资源管理
相关:04-中断处理 | 10-FreeRTOSConfig配置 | 返回目录