Chapter 07 · FreeRTOS 实时操作系统

软件定时器 Software Timer

不占用硬件定时器资源,用一个服务任务调度大量定时回调:单次/周期定时器、Start/Stop/Reset、以及回调函数的红线规则。

课程进度58%

关键概念

软件定时器 (Software Timer)
由 FreeRTOS 内核软件实现的定时器,超时后调用用户注册的回调函数。它不占用宝贵的硬件定时器外设,一个系统可轻松创建几十上百个,非常适合超时监控、周期性轻量任务、去抖延时等。精度取决于系统节拍 configTICK_RATE_HZ
定时器服务任务 (Timer Daemon)
启用 configUSE_TIMERS=1 时,调度器自动创建的一个后台任务,专门管理所有软件定时器。所有定时器回调都在这一个任务的上下文里串行执行。它的优先级 configTIMER_TASK_PRIORITY 和栈 configTIMER_TASK_STACK_DEPTH 需按回调负载合理设置。
定时器命令队列
用户调用 xTimerStart/Stop/Reset 等 API 时,并不会直接操作定时器,而是把"命令"发到一条队列,由 Daemon 任务取出执行。队列长度 configTIMER_QUEUE_LENGTH。这解释了为什么定时器操作是异步的、且带一个 xTicksToWait 参数(命令入队的等待时间)。
单次定时器 (One-shot)
xTimerCreateuxAutoReload=pdFALSE。启动后只触发一次回调,然后进入休眠(dormant)状态,需再次 Start 才会重新计时。适合"延时 N ms 后做一件事",如按键长按判定、通信超时。
周期定时器 (Auto-reload)
uxAutoReload=pdTRUE。每次超时后自动重新装载周期,无限循环触发回调,形成固定周期的心跳。适合状态灯闪烁、周期上报、看门狗喂狗等,无需为此专门开一个任务。
Timer ID 与回调参数
每个定时器可绑定一个 void* 的 ID(pvTimerGetTimerID 读取,vTimerSetTimerID 修改)。回调函数原型是 void cb(TimerHandle_t xTimer),通过句柄取回 ID,可用于让多个定时器共用一个回调并区分身份,或存放计数等小状态。
Start / Stop / Reset / ChangePeriod
四组控制命令。xTimerStart 启动/重启计时;xTimerStop 停止;xTimerReset 把计时归零重新开始(常用于"喂狗"式的超时刷新);xTimerChangePeriod 动态修改周期。都有 *FromISR 版本供中断使用。
软件定时器 vs 硬件定时器
硬件定时器(如 STM32 的 TIM)由芯片外设产生中断,精度高(微秒/纳秒级)、抖动小、可直接产生 PWM/捕获,但数量有限。软件定时器精度到 tick(毫秒级)、有 Daemon 调度延迟和抖动,但数量几乎无限、用起来简单。高精度/高频用硬件,管理型/低频用软件。

软件定时器的工作机制

软件定时器:命令队列 + Daemon 任务 ═══════════════════════════════════════════════════════════ 用户任务 / ISR Timer Daemon 任务 ┌──────────────┐ xTimerStart/Stop/Reset ┌────────────────────┐ │ xTimerReset()│ ───────命令────────────────►│ 从命令队列取命令 │ └──────────────┘ │ │ 维护所有定时器列表 │ ▼ │ 计时到期→调用回调 │ [定时器命令队列] │ cb(xTimer) 串行执行 │ len=configTIMER_QUEUE_LENGTH └─────────┬──────────┘ │ 所有回调都在此任务上下文运行 ────┘ → 回调里绝不能阻塞,否则拖垮全部定时器
回调函数三条红线

软件定时器回调运行在 Daemon 任务里,且所有定时器共用这一个任务,因此:① 绝不能阻塞——不能 vTaskDelay、不能用 portMAX_DELAY 等待队列/信号量,一旦阻塞,其他所有定时器全部停摆。② 必须简短——回调耗时越长,后续定时器抖动越大。③ 重活转交任务——需要长时间处理时,在回调里发个信号量/队列通知专门的任务去做。

创建周期定时器:LED 心跳

#include "FreeRTOS.h"
#include "timers.h"

TimerHandle_t g_blinkTimer;

/* 回调:翻转 LED。简短、不阻塞 */
void BlinkCallback(TimerHandle_t xTimer) {
  (void) xTimer;
  HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void setup_timer(void) {
  g_blinkTimer = xTimerCreate(
      "blink",                    /* 名字(调试用) */
      pdMS_TO_TICKS(500),          /* 周期 500ms */
      pdTRUE,                      /* uxAutoReload:周期定时器 */
      (void *)0,                  /* Timer ID */
      BlinkCallback);              /* 回调函数 */
  configASSERT(g_blinkTimer);

  /* 启动:命令入队等待最多 100ms */
  if (xTimerStart(g_blinkTimer,
                  pdMS_TO_TICKS(100)) != pdPASS) {
    printf("timer start cmd failed\r\n");
  }
}

单次定时器 + Reset:通信超时监控

典型"看门狗"用法:每收到一包数据就 Reset 定时器;若一段时间没数据,单次定时器超时触发,判定链路断开。

TimerHandle_t g_linkWatchdog;

void LinkTimeout(TimerHandle_t t) {
  (void) t;
  printf("link lost: no data for 2s\r\n");
  set_link_state(LINK_DOWN);   /* 简短标记,重活交任务 */
}

void setup_watchdog(void) {
  g_linkWatchdog = xTimerCreate("link",
      pdMS_TO_TICKS(2000), pdFALSE,  /* 单次 2s */
      NULL, LinkTimeout);
  xTimerStart(g_linkWatchdog, 0);
}

/* 每收到一包数据就调用:把 2s 计时归零重新开始 */
void on_packet_received(void) {
  xTimerReset(g_linkWatchdog, 0);
}

/* 在 ISR 里刷新超时用 FromISR 版本 */
void USART_IRQHandler(void) {
  BaseType_t woken = pdFALSE;
  /* ... 读取数据 ... */
  xTimerResetFromISR(g_linkWatchdog, &woken);
  portYIELD_FROM_ISR(woken);
}

用 Timer ID 让多个定时器共用回调

/* 三个定时器共用一个回调,用 ID 区分 */
void SharedCallback(TimerHandle_t t) {
  uint32_t id = (uint32_t)pvTimerGetTimerID(t);
  switch (id) {
    case 0: on_sensor_poll();  break;
    case 1: on_led_update();   break;
    case 2: on_heartbeat();    break;
  }
  /* 也可把 ID 当计数器累加,实现"第 N 次触发做特殊事" */
}
Reset 与 Start 的微妙区别

对已在运行的定时器:xTimerStart 也会把计时重新归零(等价于 Reset)。对已停止(dormant)的定时器,两者都会启动它。xTimerReset 的语义就是"从现在起重新计满一个周期",在"喂狗"场景下语义最清晰。要动态改周期用 xTimerChangePeriod,它同时会启动定时器。

软件 vs 硬件定时器选型

维度软件定时器硬件定时器 (TIM)
精度tick 级(通常 1ms)μs / ns 级
抖动受 Daemon 调度影响,较大极小,由硬件保证
数量几乎无限(受内存)有限(芯片外设数量)
能否直接产 PWM/捕获
回调上下文Daemon 任务中断上下文
典型用途超时监控、周期轻任务、去抖PWM、精确采样、电机换相
Daemon 优先级配置不当会出问题

configTIMER_TASK_PRIORITY 设得太低,高优先级任务长期占用 CPU 时定时器回调会被拖延,周期严重抖动;设得太高,回调里的任何耗时又会抢占重要任务。经验:把 Daemon 优先级设在中等偏高,并严格保证回调简短。命令队列 configTIMER_QUEUE_LENGTH 太短则高频 Start/Reset 命令会入队失败。

本章小结

软件定时器由内核软件实现,不占用硬件 TIM 外设,可创建大量实例,精度到 tick(通常毫秒级)。启用 configUSE_TIMERS=1 后内核自动创建一个 Timer Daemon 任务,所有定时器回调都在它这一个上下文里串行执行;用户的 Start/Stop/Reset/ChangePeriod 命令先发到定时器命令队列,由 Daemon 异步执行(故这些 API 带命令入队超时参数并有 FromISR 版本)。xTimerCreateuxAutoReload 决定单次(pdFALSE)还是周期(pdTRUE)。每个定时器可绑定 void* ID 供回调区分身份。回调三红线:绝不阻塞、必须简短、重活转交任务,否则拖垮所有定时器。xTimerReset 把计时归零,是超时监控/喂狗的最佳语义。软件定时器适合超时监控、周期轻任务、去抖;需要高精度、低抖动、PWM/捕获时用硬件定时器。Daemon 的优先级与栈、命令队列长度需按负载合理配置。