软件定时器(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 的职责

  1. 处理定时器命令队列中的命令(start/stop/reset/delete/change period)
  2. 检查哪些定时器到期
  3. 串行执行到期定时器的回调函数
  4. 执行 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配置返回目录