常见嵌入式操作系统中的软件定时器机制


目录

  • 1. 引言:硬件定时器与软件定时器
  • 2. 软件定时器通用概念
    • 2.1 系统节拍(Tick)与 Tickless
    • 2.2 一次性与周期性
    • 2.3 回调执行上下文(最重要的概念)
    • 2.4 精度、抖动与漂移
  • 3. 常见实现机制总览
    • 3.1 有序链表与增量链表(Delta List)
    • 3.2 时间轮(Timer Wheel)
    • 3.3 红黑树(高精度定时器)
    • 3.4 跳表(Skip List)
    • 3.5 守护任务 / 工作队列投递
    • 3.6 各机制复杂度对比
    • 3.7 定时计数绕回(Tick Wraparound)及其处理
  • 4. 各操作系统定时器机制详解
    • 4.1 Linux
    • 4.2 FreeRTOS
    • 4.3 μC/OS-II
    • 4.4 μC/OS-III
    • 4.5 RT-Thread
    • 4.6 Zephyr
    • 4.7 ThreadX(Eclipse ThreadX)
    • 4.8 NuttX(Apache)
    • 4.9 LiteOS(Huawei LiteOS)
    • 4.10 AliOS Things(Rhino 内核)
  • 5. 横向对比
    • 5.1 接口对照总表
    • 5.2 机制与上下文对照表
    • 5.3 按需求选型速查
  • 6. 使用建议与常见陷阱
    • 6.1 回调写法的通用纪律
    • 6.2 生命周期管理
    • 6.3 时序语义
    • 6.4 低功耗配合
  • 7. 参考资料

1. 引言:硬件定时器与软件定时器

在嵌入式系统中,"定时"需求无处不在:协议超时重传、按键去抖、LED 闪烁、看门狗喂狗、周期采样、低功耗唤醒…… 支撑这些需求的有两类定时器:

  • 硬件定时器:MCU/SoC 片上的定时/计数外设(如 STM32 的 TIM、ARM 的 SysTick、通用 GPT)。数量有限、精度高,通常被 OS 内核占用一路作为**系统节拍(System Tick)**源,或留给应用做 PWM、输入捕获等专用功能。
  • 软件定时器:OS 在系统节拍或高精度时钟之上,用软件方式维护的定时对象。数量几乎不受限(只受内存约束),应用可按需创建、启动、停止、删除。

软件定时器的核心思想是:用一路硬件时钟中断,复用出任意多个逻辑定时器。每个节拍(或时钟事件)到来时,内核检查所有挂起的定时器,找出到期者并触发其到期动作(通常是调用用户注册的回调函数)。

各 OS 的差异主要体现在四个维度:

  1. 时间管理数据结构——用什么结构组织大量定时器,决定了插入/删除/到期检查的复杂度;
  2. 回调执行上下文——回调在中断里跑还是在任务里跑,决定了回调能做什么、不能做什么;
  3. 触发模式——一次性(one-shot)还是周期性(periodic/auto-reload);
  4. 时钟驱动方式——固定节拍 tick、动态节拍 tickless、还是高精度时钟事件。

在这里插入图片描述


2. 软件定时器通用概念

2.1 系统节拍(Tick)与 Tickless

绝大多数 RTOS 以固定频率的周期性中断作为时间基准,称为系统节拍(tick)。典型频率 100 Hz~1000 Hz:

  • 每个节拍中断中,内核推进全局 tick 计数(如 Linux 的 jiffies、FreeRTOS 的 xTickCount、RT-Thread 的 rt_tick);
  • 软件定时器的超时时间通常以 tick 为单位指定(如 pdMS_TO_TICKS(500) 把 500 ms 换算成 tick 数);
  • 定时精度受节拍周期限制:1000 Hz 下理论分辨率 1 ms,100 Hz 下只有 10 ms,且存在最多一个节拍的对齐误差。

固定节拍的代价是:即使系统无事可做,CPU 也要周期性被唤醒,不利于电池供电设备。于是出现了 **tickless(动态节拍)**技术:

  • 进入空闲时,内核计算"下一个最近到期事件"还有多久,把硬件定时器编程为到那时再中断,中间的周期性节拍全部省略;
  • 唤醒后补偿 tick 计数,对软件定时器透明;
  • 各 OS 的实现:Linux 的 NO_HZ(idle/full)、FreeRTOS 的 tickless idle、Zephyr 默认开启的 tickless kernel、LiteOS/AliOS Things 的 tickless 低功耗、NuttX 的 CONFIG_SCHED_TICKLESS、RT-Thread PM 框架中的 tickless 支持。

2.2 一次性与周期性

  • 一次性(one-shot):到期触发一次后自动停止,常用于超时检测(如"500 ms 内未收到应答则报错")。多数系统的一次性定时器到期后仍占内存,需要显式删除或重新启动。
  • 周期性(periodic / auto-reload):到期后内核自动按周期重新装载,持续触发,常用于周期任务(心跳、采样)。

周期定时器有一个容易忽视的语义差异——周期基准点

  • 大多数实现以"到期时刻"为基准重新装载(expires += period),长期看无累积漂移;
  • 若回调执行本身耗时较长,部分实现可能以"回调完成时刻"为基准(或丢弃中间错过的触发),会产生漂移或"吞拍"。对时序敏感的场景应查阅具体 OS 语义。

2.3 回调执行上下文(最重要的概念)

定时器到期后回调在哪执行,直接决定了回调代码的写法约束:

  • 中断/软中断上下文:回调在 tick ISR、时钟中断或软中断中直接执行。延迟最低,但绝对不能阻塞、不能睡眠,只能调用中断安全的 API(如 xQueueSendFromISR)。耗时操作必须以"置标志/发消息/唤醒任务"的方式移交任务上下文。
  • 任务上下文:到期事件被投递给一个专门的守护任务/定时器线程(或系统工作队列),由该线程执行回调。回调可以做更多事,但注意所有定时器共享这一条执行线程——一个回调阻塞或跑飞,会拖累系统里所有定时器到期处理。

在这里插入图片描述

2.4 精度、抖动与漂移

  • 精度(resolution):tick 型定时器受节拍周期限制;Linux hrtimer 这类高精度定时器可达纳秒级(实际受硬件 clocksource 分辨率约束)。
  • 抖动(jitter):到期时刻与回调实际执行时刻的偏差。中断上下文回调抖动小;任务上下文回调还要排队等守护任务调度,抖动更大,且受守护任务优先级影响。
  • 漂移(drift):周期定时器长期运行与理想时刻的累积偏差,取决于重装载基准与时钟源精度。

3. 常见实现机制总览

3.1 有序链表与增量链表(Delta List)

最朴素的机制:把所有定时器按到期时刻(或相对前一个节点的剩余时间,即增量)串成有序链表。

  • 到期检查:只看链表头,头节点没到期就不用继续看——每节拍 O(1);
  • 插入:需要遍历找到位置——O(n);
  • 采用者:NuttX 看门狗定时器(wdog,增量链表)、Zephyr 内核超时链表(_timeout 增量链表)、ThreadX 定时器链表(按剩余 tick 排序)、LiteOS swtmr(按到期 tick 排序的双向链表)。

变体——双链表轮换:FreeRTOS 定时器守护任务内部维护"当前列表 + 溢出列表"两张有序链表,tick 计数回绕时切换,避免处理 32 位 tick 溢出的边界问题。

3.2 时间轮(Timer Wheel)

把"时间轴"映射为一个环形数组(轮),每个槽位挂一条定时器链表:

  • 插入/删除槽位 = (当前槽 + 到期tick数) mod 槽数,O(1);
  • 到期检查:指针每 tick 前进一格,只处理当前槽——O(1);
  • 代价:超出轮覆盖范围的超时需要多圈标记(单层轮)或级联迁移(多层轮);槽内链表过长时处理退化。

单层轮:μC/OS-II 的软件定时器轮(槽数 OS_TMR_CFG_WHEEL_SIZE 可配)。

多层轮:Linux timer_list 自 2.6 起采用多级时间轮(4.8 内核后重构为 8 级、每级 64 槽的桶数组,按到期距离决定放在哪一级,到期临近时逐级级联迁移到细粒度层),可覆盖整个 32 位 jiffies 范围,插入删除均为 O(1) 且摊销迁移成本极低。

在这里插入图片描述

3.3 红黑树(高精度定时器)

定时器数量不大但要求 ns 级精度时,可以用红黑树按到期时刻排序:

  • 插入/删除 O(log n),取最早到期者 O(1)(缓存最左节点);
  • 配合可单次编程(one-shot)的时钟事件设备,实现真正的事件驱动定时,不依赖周期节拍;
  • 采用者:Linux hrtimer(每 CPU、每时钟基 MONOTONIC/BOOTTIME/REALTIME 各一棵红黑树)。

3.4 跳表(Skip List)

在有序链表上叠加多级"快车道"索引,插入/查找 O(log n),实现比红黑树简单、无平衡开销:

  • 采用者:RT-Thread 的 rt_timer(可通过 RT_TIMER_SKIP_LIST_LEVEL 配置跳表层数,层数为 1 时退化为普通有序链表,小内存设备可关掉)。

3.5 守护任务 / 工作队列投递

与数据结构正交的另一条主线是回调在哪里执行

  • 守护任务模式:内核创建一个定时器服务任务(FreeRTOS 的 Timer Service Task、μC/OS-II/III 的 OS_TmrTask、LiteOS 的 swtmr 任务、AliOS Rhino 的定时器任务、RT-Thread 的 timer 线程)。ISR 中只做最轻的工作(推进 tick、标记到期、向队列投命令),回调由该任务执行;
  • 工作队列模式:复用系统工作队列(Linux delayed_work、NuttX work_queue + delay、Zephyr k_work_delayable),到期项排队到工作线程执行,进程/线程上下文,部分实现允许睡眠;
  • ISR 直接执行:Zephyr k_timer、ThreadX 应用定时器、NuttX wdog、RT-Thread HARD_TIMER、Linux timer_list/hrtimer——回调就在中断或软中断里跑。

前两种模式本质上都属于"任务/线程上下文"路径,与"ISR 直接执行"形成对照。下图把两条路径的特点、限制与典型采用者放在一起对比:

在这里插入图片描述

3.6 各机制复杂度对比

机制 插入 删除 每 tick 到期检查 典型采用者
有序/增量链表 O(n) O(1)~O(n) O(1)(只看表头) NuttX、Zephyr、ThreadX、LiteOS
单层时间轮 O(1) O(1) O(槽内链表长) μC/OS-II
多层时间轮 O(1)(摊销) O(1) O(1)(摊销) Linux timer_list
红黑树 O(log n) O(log n) O(1)(最左节点) Linux hrtimer
跳表 O(log n) O(log n) O(1) RT-Thread

注意:对定时器数量几十以内的小系统,上述复杂度差异实际意义不大;真正的差异在回调上下文tickless 支持

3.7 定时计数绕回(Tick Wraparound)及其处理

这是软件定时器机制中最容易被忽视、后果却最严重的一个共性问题,单独成节分析。

3.7.1 现象本质

绝大多数 OS 的系统 tick 计数是 32 位无符号整数(Linux jiffies、FreeRTOS xTickCount、RT-Thread rt_tick、ThreadX _tx_timer_system_clock、LiteOS/Rhino 的 tick 计数等),计满后回到 0,称为绕回(wraparound / rollover)

tick 位宽 / 节拍率 绕回周期
32 位 @ 1000 Hz 49.7 天
32 位 @ 100 Hz 约 497 天
16 位 @ 1000 Hz(如 FreeRTOS configUSE_16_BIT_TICKS 65.5 秒

定时器的到期时刻expires = now + delay在回绕点附近会"绕"回小数值。若内核或应用用直接大小比较now >= expires)判定到期,在回绕点附近判断会完全反转——典型症状是设备连续运行几十天后,定时器集体提前触发或永不到期,而且实验室短时测试根本暴露不出来。

在这里插入图片描述

3.7.2 通用化解法(五种)
解法 原理 约束
有符号差值比较 (int32_t)(expires - now) <= 0 判定到期;无符号回绕后的差值恰为正确的剩余量 单次定时跨度必须 < 2³¹ tick(1000 Hz 下约 24.8 天)
增量链表(delta list) 节点只存"相对前驱的差值",每 tick 递减头节点计数,从不做绝对时刻比较 天然免疫回绕,但插入 O(n)
双列表轮换 按"当前周期 / 下一周期(溢出)"分两张有序链表,tick 回绕时整表切换 FreeRTOS 专有设计
时间轮 槽位本身就是环形结构,"当前槽 + delay"取模即得插入位置,环形语义天然覆盖回绕 单层轮需处理多圈标记
64 位计数 位宽足够大,回绕周期拉长到亿年级(64 位 @ 1000 Hz 约 5.8 亿年),问题从根上消失 32 位 MCU 上 64 位比较/加减有额外开销
3.7.3 各 OS 的具体处理
操作系统 tick 位宽 回绕处理机制
Linux timer_list 32 位(unsigned long,另有 64 位 jiffies_64 对外提供 time_after() / time_before() / time_in_range() 宏(本质是有符号差值比较);时间轮级联天然按环形语义工作
Linux hrtimer 64 位(ktime_t 纳秒) 无回绕问题(64 位 ns 约 292 年才溢出)——高精度场景的另一个隐性优势
FreeRTOS 32 位(可配 16 位) 守护任务内部当前列表 + 溢出列表双表轮换(prvSwitchTimerLists);应用层 vTaskSetTimeOutState / xTaskCheckForTimeOut 用差值法
μC/OS-III 32 位(OS_TICK tick list 与定时器链表均为增量链表,天然免疫
RT-Thread 32 位(rt_tick_t 定时器到期判断用差值法(形如 rt_tick - timeout_tick < RT_TICK_MAX/2 的比较)
Zephyr 32 位(可选 CONFIG_TIMEOUT_64BIT _timeout 增量链表天然免疫;64 位选项可从根上消除
ThreadX 32 位 定时器有序链表 + 差值比较;应用层直接用 tx_time_get() 算超时的代码需自行用差值法
NuttX 32 位 clock_t(部分配置可 64 位) wdog 增量链表天然免疫
LiteOS 32 位 swtmr 有序链表按差值语义比较;LOS_TickCountGet 的溢出由内核统一处理
AliOS Things 32 位(tick_t 定时器有序链表 + 差值比较
3.7.4 应用层必须自守的三条纪律
  1. 自己写的超时判断一律用差值法(int32_t)(deadline - now) <= 0,禁止 now >= deadline 直比;
  2. 单次定时跨度不要超过 2³¹ tick(1000 Hz 下约 24.8 天;16 位 tick 下只有约 32 秒)——超长定时用"分段重装"或软件分层计数(如应用层维护"天计数 + 当天 tick");
  3. 开启 16 位 tick 的低配 MCU 项目(FreeRTOS configUSE_16_BIT_TICKS)要把回绕周期当秒级问题对待——65.5 秒就绕一圈,任何"长超时"逻辑都必须复核。

4. 各操作系统定时器机制详解

4.1 Linux

Linux 是其中唯一提供双定时器子系统的操作系统:面向毫秒级普通定时的 timer_list,和面向纳秒级高精度的 hrtimer,外加建立在工作队列之上的 delayed_work 与用户态 POSIX 定时器。

在这里插入图片描述

4.1.1 timer_list(传统节拍定时器)
  • 机制:以 jiffies 为基准,内核用多层时间轮(每 CPU 一个 timer base,8 级桶数组)管理;节拍中断触发 TIMER_SOFTIRQ 软中断,到期回调在软中断上下文执行——不能睡眠。
  • 精度:一个 jiffies,即 1000/HZ ms(HZ 常见 100/250/300/1000,嵌入式常为 100 或 250)。

核心接口(include/linux/timer.h):

接口 说明
timer_setup(timer, callback, flags) 初始化定时器并绑定回调(4.14 起取代 init_timer+手工赋值)
add_timer(timer) 启动定时器(按已设置的 expires 到期)
mod_timer(timer, expires) 修改到期时间并启动(最常用的"重新武装"接口)
del_timer(timer) / del_timer_sync(timer) 删除;后者等待回调执行完,SMP 下安全
timer_pending(timer) 查询是否处于挂起状态
from_timer(ptr, timer, field) 在回调中由 timer_list 反查外层结构体(容器包含手法)

典型用法:

static void my_timer_fn(struct timer_list *t)
{
    struct my_dev *dev = from_timer(dev, t, timer);
    /* 软中断上下文:不能睡眠,不能拿 mutex */
    mod_timer(&dev->timer, jiffies + msecs_to_jiffies(500)); /* 手动重装载实现周期 */
}

timer_setup(&dev->timer, my_timer_fn, 0);
mod_timer(&dev->timer, jiffies + msecs_to_jiffies(500));

注意:timer_list 本身没有内建的周期模式,周期定时器靠回调里再次 mod_timer 实现。

4.1.2 hrtimer(高精度定时器)
  • 机制:以 ktime_t(纳秒)为基准,每 CPU、每时钟基(CLOCK_MONOTONIC/CLOCK_REALTIME/CLOCK_BOOTTIME)一棵红黑树;依赖硬件高精度时钟事件设备(one-shot clockevent),不需要周期节拍驱动,到期时直接编程硬件产生中断;回调在硬中断上下文执行(4.16 起可选 HRTIMER_MODE_SOFT 在 HRTIMER_SOFTIRQ 软中断执行)。
  • 使能条件CONFIG_HIGH_RES_TIMERS(有高精度 clocksource/clockevent 硬件时默认开)。

核心接口(include/linux/hrtimer.h):

接口 说明
hrtimer_init(timer, clockid, mode) 初始化,指定时钟基与绝对/相对模式
hrtimer_start(timer, tim, mode) 启动(ktime_set(sec, nsec) / ms_to_ktime() 构造时间)
hrtimer_cancel(timer) / hrtimer_try_to_cancel(timer) 取消;前者等待回调结束
hrtimer_restart(timer) 在回调中重启(配合返回值实现周期)
hrtimer_forward_now(timer, interval) 以"现在"为基准向前推进一个周期,防漂移
回调返回值 HRTIMER_RESTART(重新调度)/ HRTIMER_NORESTART(一次性)
static enum hrtimer_restart my_hrtimer_fn(struct hrtimer *t)
{
    hrtimer_forward_now(t, ms_to_ktime(10));   /* 以理论到期点为基准推进 10ms,无漂移 */
    return HRTIMER_RESTART;                     /* 周期定时器 */
}
4.1.3 delayed_work 与 POSIX 定时器
  • delayed_workINIT_DELAYED_WORK() / schedule_delayed_work(&dw, delay_jiffies) / mod_delayed_work() / cancel_delayed_work_sync()。到期后把工作项投递到系统工作队列(kworker 线程),进程上下文,可以睡眠——回调里要做 I2C/SPI 访问、申请内存、拿 mutex 时的正确选择。
  • 用户态 POSIX 定时器timer_create(CLOCK_MONOTONIC, &sevp, &tid) / timer_settime() / timer_delete(),到期通过信号(SIGEV_SIGNAL)或新建线程(SIGEV_THREAD)通知,底层基于 hrtimer。
  • ticklessCONFIG_NO_HZ_IDLE(空闲时停节拍)、CONFIG_NO_HZ_FULL(指定 CPU 上只有一个任务时也停节拍),由 tick 广播框架在多核间协调。
4.1.4 Linux 小结
需求 选用
ms 级超时/轮询/去抖(回调简短) timer_list
μs/ns 级精确定时 hrtimer
回调中可能睡眠 delayed_work
用户态程序 POSIX timer_create

4.2 FreeRTOS

4.2.1 机制概述

FreeRTOS 软件定时器为可选组件,核心设计是"命令队列 + 守护任务":

  • 内核在调度器启动时创建一个 定时器服务任务(Timer Service / Daemon Task)和一条定时器命令队列
  • xTimerStart() 等 API 并不直接操作定时器,而是向命令队列发送一条命令,由守护任务取出并执行;
  • 定时器到期同样由守护任务检测(它被安排在"最近到期时刻"被唤醒),所有回调都在守护任务上下文执行
  • 守护任务内部用两张有序链表(当前列表 + 溢出列表)管理定时器,天然处理 tick 计数回绕。

关键配置(FreeRTOSConfig.h):

配置项 作用
configUSE_TIMERS 置 1 才启用软件定时器
configTIMER_TASK_PRIORITY 守护任务优先级(决定回调响应及时性)
configTIMER_QUEUE_LENGTH 命令队列长度(影响峰值并发命令数)
configTIMER_TASK_STACK_DEPTH 守护任务栈深(回调用栈都从这儿出)
4.2.2 核心接口
接口 说明
xTimerCreate(name, period, autoReload, id, cb) 动态创建;autoReload=pdTRUE 为周期,pdFALSE 一次性;id 是用户私有数据指针
xTimerCreateStatic(...) 静态创建(内存用户提供,适配无堆系统)
xTimerStart / xTimerStop / xTimerReset(t, block) 启动 / 停止 / 复位重新计时;ISR 中用 ...FromISR 版本
xTimerChangePeriod(t, newPeriod, block) 改周期(同时改一次性为周期等)
xTimerDelete(t, block) 删除
xTimerIsTimerActive(t) 查询是否激活
pvTimerGetTimerID / vTimerSetTimerID 取/设私有数据(回调里区分多个实例的常用手法)
xTimerGetPeriod / xTimerGetExpiryTime 查询周期 / 到期时刻
xTimerPendFunctionCall(fn, arg1, arg2, block) 把任意函数投递到守护任务执行——ISR 中延后处理(defer)的标准手段
TimerHandle_t t = xTimerCreate("led", pdMS_TO_TICKS(500), pdTRUE, NULL, led_cb);
xTimerStart(t, 0);
4.2.3 上下文约束与 tickless
  • 回调跑在守护任务里:禁止调用会阻塞的 API(如 vTaskDelay、带超时取队列),否则所有定时器都被卡住;
  • ISR 中只能调 ...FromISR 版本,且本质是"往命令队列发消息",回调仍在守护任务执行;
  • 低功耗:configUSE_TICKLESS_IDLE=1 时,空闲任务会计算下个到期事件,调用 vPortSuppressTicksAndSleep() 停节拍睡过去——软件定时器到期本身就会参与"最近唤醒时刻"的计算。

4.3 μC/OS-II

4.3.1 机制概述

μC/OS-II 自 V2.8x 起提供软件定时器(OS_TMR),由**定时器管理任务(OSTmr_Task)**驱动:

  • 开启条件:OS_TMR_EN = 1os_cfg.h),内核初始化时自动创建 OSTmr_Task(优先级由 OS_TASK_TMR_PRIO 配置);
  • 数据结构:单层时间轮——OSTmrWheelTbl[],槽数 OS_TMR_CFG_WHEEL_SIZE(默认 8),槽内为定时器链表;
  • 独立时基:定时器任务不必每 tick 都跑,OS_TMR_CFG_TICKS_PER_SEC(默认 10 Hz)决定定时器时基,从而摊薄开销——这也是 II 代定时器精度较低(默认 100 ms 量级)的原因;
  • 回调在定时器任务上下文执行,官方要求回调尽量短,因为所有定时器共享这一个任务。
4.3.2 核心接口
接口 说明
OSTmrCreate(dly, period, opt, cb, arg, name, err) 创建:dly 首次延时;period>0 为周期模式;optOS_TMR_OPT_ONE_SHOT / OS_TMR_OPT_PERIODIC
OSTmrStart(tmr, err) 启动(挂入时间轮)
OSTmrStop(tmr, opt, arg, err) 停止;OS_TMR_OPT_CALLBACK_ARG 可传新参数给停止回调
OSTmrDel(tmr, err) 删除
OSTmrRemainGet(tmr, err) 查询剩余时间
OSTmrStateGet(tmr, err) 查询状态(未启动/运行/停止)
OSTmrNameGet / OSTmrNameSet 名字管理(调试辅助)
OS_TMR *tmr;
tmr = OSTmrCreate(0, 5, OS_TMR_OPT_PERIODIC, my_cb, NULL, "t5", &err); /* 5个定时器tick为周期 */
OSTmrStart(tmr, &err);

4.4 μC/OS-III

4.4.1 机制概述与 II 代的差异

III 代重写了定时器子系统,接口风格全面转向"句柄传参 + OS_ERR 尾参",主要变化:

  • 数据结构由时间轮改为按到期 tick 排序的增量链表(配合新的 tick list 机制),定时器任务 OS_TmrTask 的唤醒由内核 tick list 精确调度,不再依赖固定时基轮询;
  • 定时器任务速率仍可配(OS_CFG_TMR_TASK_RATE_HZ,典型 10 Hz),即定时器分辨率仍独立于系统节拍率;
  • 新增 OSTmrSet():不删除即可修改延时/周期/回调——II 代没有;
  • 回调同样在 OS_TmrTask 任务上下文执行。
4.4.2 核心接口
接口 说明
OSTmrCreate(tmr, name, dly, period, opt, cb, arg, err) 创建(静态分配 OS_TMR 由用户提供);optOS_OPT_TMR_ONE_SHOT / OS_OPT_TMR_PERIODIC
OSTmrStart(tmr, err) 启动
OSTmrStop(tmr, opt, arg, err) 停止
OSTmrDel(tmr, err) 删除
OSTmrSet(tmr, dly, period, cb, arg, err) III 代新增:在线修改参数
OSTmrRemainGet(tmr, err) 剩余时间
OSTmrStateGet(tmr, err) 状态查询
OS_TMR tmr;
OSTmrCreate(&tmr, "hb", 0, OS_CFG_TMR_TASK_RATE_HZ, OS_OPT_TMR_PERIODIC,
            hb_cb, (void *)0, &err);   /* 每秒一次心跳 */
OSTmrStart(&tmr, &err);

II/III 共同注意点:定时器任务速率(10 Hz 典型)远低于系统节拍(100~1000 Hz)时,定时器分辨率被定时器任务速率决定,想要 1 ms 级分辨率就得把 RATE_HZ 调到 1000,开销随之上升——精度与开销的权衡要自己把握。


4.5 RT-Thread

4.5.1 机制概述

RT-Thread 的软件定时器(rt_timer)有两大特色:双回调上下文模式跳表管理

  • HARD_TIMER 模式(默认):到期回调直接在 tick 中断上下文执行——要求极短、不能阻塞、不能调用可能挂起的 API;
  • SOFT_TIMER 模式:内核创建 timer 线程(软件定时器线程),到期回调由该线程执行——可以调用更多 API,但同样不能长时间占用;
  • 数据结构:按到期时间排序的跳表(RT_TIMER_SKIP_LIST_LEVEL 可配层数,默认 1 层即普通有序链表;定时器数量多的系统可调高层数换取 O(log n) 插入);
  • 每个 tick 中断中调用 rt_timer_check() 处理 HARD_TIMER 到期,SOFT_TIMER 到期则通知 timer 线程执行 rt_soft_timer_check()
4.5.2 核心接口
接口 说明
rt_timer_create(name, cb, param, time, flag) 动态创建;flagRT_TIMER_FLAG_ONE_SHOT / RT_TIMER_FLAG_PERIODICRT_TIMER_FLAG_HARD_TIMER / RT_TIMER_FLAG_SOFT_TIMER 的组合
rt_timer_init(timer, name, cb, param, time, flag) 静态初始化(内存用户提供)
rt_timer_start / rt_timer_stop(timer) 启动 / 停止
rt_timer_delete(timer) / rt_timer_detach(timer) 删除(动态)/ 脱离(静态)
rt_timer_control(timer, cmd, arg) 控制:RT_TIMER_CTRL_SET_TIME(改超时)、RT_TIMER_CTRL_GET_TIMERT_TIMER_CTRL_SET_ONESHOTRT_TIMER_CTRL_SET_PERIODICRT_TIMER_CTRL_SET_FUNC(改回调)等
rt_tick_get() 获取当前 tick(手动算超时常用)
rt_timer_t t = rt_timer_create("led", led_cb, RT_NULL,
                               rt_tick_from_millisecond(500),
                               RT_TIMER_FLAG_PERIODIC | RT_TIMER_FLAG_SOFT_TIMER);
rt_timer_start(t);
4.5.3 模式选择要点
HARD_TIMER SOFT_TIMER
回调上下文 tick 中断 timer 线程
延迟/抖动 最小 取决于线程优先级(RT_TIMER_THREAD_PRIO
回调可用 API 仅中断安全 API 大部分内核 API(仍不可久占)
适用 超轻量动作:置标志、发事件 需要调用 IPC、较复杂逻辑

低功耗方面,RT-Thread 的 PM 组件(rt_pm)提供 tickless 支持:进入深度睡眠前停掉 tick,唤醒后补偿。


4.6 Zephyr

4.6.1 机制概述

Zephyr 的 k_timer 直接建立在内核超时(timeout)基础设施之上:

  • 内核维护一条按到期 tick 排序的增量链表_timeout 节点),线程睡眠、超时等待、k_timer 都挂在同一机制上;
  • tickless 是默认行为CONFIG_TICKLESS_KERNEL=y):时钟驱动只在"最近到期时刻"编程下一次中断,系统节拍率 CONFIG_SYS_CLOCK_TICKS_PER_SEC(默认 10000)仅作为时间单位基准,不产生固定周期中断;
  • 到期回调(expiry function)在系统时钟 ISR 上下文执行;定时器被停止时可选地调用 stop function,同样在 ISR 上下文。
4.6.2 核心接口
接口 说明
k_timer_init(timer, expiry_fn, stop_fn) 初始化并绑定到期/停止回调
k_timer_start(timer, duration, period) 启动:duration 首次延时;period=K_NO_WAIT(0) 为一次性,非零为周期。时间用 K_MSEC()/K_SECONDS()/K_TIMEOUT_ABS_TICKS() 构造
k_timer_stop(timer) 停止
k_timer_status_get(timer) 查询自上次状态读取以来触发了多少次(并清零计数)
k_timer_status_sync(timer) 阻塞等待直到定时器至少触发一次(同步化使用)
k_timer_remaining_get(timer) / k_timer_remaining_ticks(timer) 剩余时间
k_timer_user_data_set/get 用户私有数据
K_TIMER_DEFINE(name, expiry, stop) 静态定义
void expiry(struct k_timer *t) { /* ISR 上下文!*/ }
K_TIMER_DEFINE(my_timer, expiry, NULL);

k_timer_start(&my_timer, K_MSEC(500), K_MSEC(500));  /* 500ms 周期 */
4.6.3 配套:k_work_delayable

ISR 上下文跑不了的重活,Zephyr 的惯用组合是:定时器 expiry 里 k_work_schedule() 一个 delayable work,由系统工作队列线程执行:

接口 说明
k_work_init_delayable(work, handler) 初始化可延时工作项
k_work_schedule(work, delay) / k_work_reschedule 延时调度到系统工作队列
k_work_cancel_delayable(work) 取消

这套"定时器(ISR)→ 延时工作项(线程)"两段式,是 Zephyr 中处理周期性重活的标准范式。


4.7 ThreadX(Eclipse ThreadX)

4.7.1 机制概述

ThreadX 的应用定时器(TX_TIMER)以简洁的 ISR 模型著称:

  • 所有激活定时器挂在一条按剩余 tick 排序的有序链表上,由 tick 中断中的 tx_timer_interrupt 推进;
  • 到期回调(expiration function)默认在定时器 ISR 上下文直接执行——ThreadX 官方明确建议回调只做"唤醒线程/发消息"这类轻动作;
  • 提供配置开关 TX_TIMER_PROCESS_IN_ISR:不定义该宏时,定时器处理被推迟到一个内部定时器线程中执行,换取更短的 ISR 关闭时间(回调仍要求简短,因为共用一个线程);
  • 无独立"周期属性",用创建时的重装载 tick 数表达:reschedule_ticks=0 即一次性,非零则到期后自动按该值重装。
4.7.2 核心接口
接口 说明
tx_timer_create(timer, name, fn, arg, init_ticks, resched_ticks, flag) 创建;flagTX_AUTO_ACTIVATE(创建即启动)/ TX_NO_ACTIVATE
tx_timer_activate(timer) / tx_timer_deactivate(timer) 激活 / 停用
tx_timer_change(timer, init_ticks, resched_ticks) 修改首次延时与重装载周期(需先 deactivate)
tx_timer_delete(timer) 删除
tx_timer_info_get(timer, ...) 查询名字/激活状态/剩余 tick 等
TX_TIMER my_timer;
tx_timer_create(&my_timer, "t", my_cb, 0, 100, 100, TX_AUTO_ACTIVATE); /* 100 tick 周期 */

ThreadX 的定时器 tick 与系统节拍同源(节拍率由底层移植的硬件定时器决定,通常配 100~1000 Hz)。因为回调在 ISR 中,回调里只能调用 ThreadX 标明 ISR 安全的服务(如 tx_thread_resumetx_queue_send 的非挂起用法),绝不能调用任何可能挂起当前上下文的服务。


4.8 NuttX(Apache)

NuttX 走 POSIX 兼容路线,同时保留了一套内核传统的**看门狗定时器(watchdog timer)**机制,并配合工作队列提供延迟执行能力。

4.8.1 看门狗定时器(wdog)——内核传统机制
  • struct wdog_s 节点挂在一条按剩余 tick 排序的增量链表上,由 tick 中断(wd_timer)递减并触发;
  • 回调在中断上下文执行,只能做中断安全操作;
  • 一次性触发,周期性需求靠回调里再次 wd_start()
接口 说明
wd_start(wdog, delay, entry, arg) 启动;delay 以系统 tick 为单位(USEC2TICK/MSEC2TICK 换算)
wd_cancel(wdog) 取消
wd_gettime(wdog) 查询剩余 tick
4.8.2 POSIX 定时器——应用层标准接口

完整支持 POSIX.1 定时器族,对用户代码最友好:

接口 说明
timer_create(clockid, sigevent, timerid) 创建,通知方式 SIGEV_SIGNAL(信号)/ SIGEV_THREAD(线程回调)
timer_settime(timerid, flags, value, ovalue) 设置首次触发与周期(it_interval 非零即周期定时器)
timer_gettime(timerid, value) 查询剩余
timer_delete(timerid) 删除

另有简化封装:oneshot_alloc() / oneshot_start() / oneshot_cancel()(内部即 wdog+信号封装)。

4.8.3 延迟工作队列——可睡眠上下文
  • work_queue(HPWORK/LPWORK, &work, worker, arg, delay):把 worker 延迟 delay tick 后投递到高优先级或低优先级内核工作队列线程执行,线程上下文,可调用大部分内核 API;
  • work_cancel() 取消。周期场景在工作函数尾部再次 work_queue() 自身。
4.8.4 Tickless 支持

CONFIG_SCHED_TICKLESS 开启后,NuttX 在空闲时把下一个最近到期事件(含 wdog、线程延时)编程到时钟硬件,停止周期节拍;CONFIG_SCHED_TICKLESS_ALARM 还可对接 RTC 闹钟类唤醒源。


4.9 LiteOS(Huawei LiteOS)

4.9.1 机制概述

LiteOS 的软件定时器(swtmr)模块与内核 tick 解耦:

  • 开启条件:LOSCFG_BASE_CORE_SWTMR=y,内核初始化时创建 swtmr 软件定时器任务(优先级 LOSCFG_BASE_CORE_TSK_SWTMR_CONFIG)与定时器命令队列;
  • 定时器按到期 tick 挂在有序链表上;接口调用(create/start/stop)同样以命令投递方式由 swtmr 任务统一处理,回调在 swtmr 任务上下文执行
  • 三种模式:LOS_SWTMR_MODE_ONCE(一次性)、LOS_SWTMR_MODE_PERIOD(周期)、LOS_SWTMR_MODE_NO_SELFDELETE(一次性且到期后不自动回收,需手工删除);
  • 特色配置 LOSCFG_BASE_CORE_SWTMR_ALIGN定时器对齐——把相近到期时间的定时器合并到同一时刻唤醒,减少低功耗场景下的唤醒次数(IoT 设备省电利器)。
4.9.2 核心接口
接口 说明
LOS_SwtmrCreate(interval, mode, cb, &id, arg) 创建;interval 以 tick 为单位(LOS_MS2Tick() 换算),模式见上
LOS_SwtmrStart(id) 启动
LOS_SwtmrStop(id) 停止
LOS_SwtmrDelete(id) 删除
LOS_SwtmrTimeGet(id, &tick) 查询剩余时间
UINT16 id;
LOS_SwtmrCreate(LOS_MS2Tick(500), LOS_SWTMR_MODE_PERIOD, my_cb, &id, NULL);
LOS_SwtmrStart(id);

低功耗:开启 LOSCFG_KERNEL_TICKLESS 后,空闲时停节拍进入低功耗,swtmr 到期时间参与唤醒时刻计算;ALIGN 配置进一步聚合唤醒点。


4.10 AliOS Things(Rhino 内核)

4.10.1 机制概述

AliOS Things 提供两层定时器接口:

  • 内核层(Rhino ktimer)ktimer_t,由内核**定时器任务(timer task)**统一驱动——tick 中断只更新链表头计数,实际回调在定时器任务上下文执行;定时器按到期 tick 组织在有序链表上;
  • OSAL 层(aos API):在 ktimer 之上封装的 aos_timer_*,AliOS 应用开发的推荐入口,语义对齐主流 RTOS。

低功耗方面 Rhino 提供 tickless(RHINO_CONFIG_TICKLESS),配合 krhino_tick_sleep 等接口进入低功耗并补偿 tick。

4.10.2 OSAL 层接口(推荐)
接口 说明
aos_timer_new(timer, fn, arg, ms, repeat) 创建;repeat=1 周期、0 一次性,创建后需手动 start
aos_timer_new_ext(timer, fn, arg, ms, repeat, auto_run) auto_run=1 创建即运行
aos_timer_start(timer) / aos_timer_stop(timer) 启动 / 停止
aos_timer_free(timer) 释放
aos_timer_change(timer, ms) / aos_timer_change_once(timer, ms) 修改周期 / 修改一次性延时
4.10.3 Rhino 内核层接口
接口 说明
krhino_timer_create(timer, name, cb, first, round, arg, auto_run) 创建;first 首次延时(tick),round 周期(0 为一次性)
krhino_timer_del(timer) 删除
krhino_timer_start / krhino_timer_stop(timer) 启动 / 停止
krhino_timer_change(timer, first, round) 修改参数
static aos_timer_t g_t;
aos_timer_new(&g_t, my_cb, NULL, 500, 1);  /* 500ms 周期,回调在定时器任务上下文 */
aos_timer_start(&g_t);

与 FreeRTOS 守护任务模型相同:Rhino 定时器回调共享定时器任务,禁止阻塞,耗时处理应再派发给其他任务(如 aos_post_delayed_action,AliOS 提供的延迟动作投递,底层也是定时器+任务模型)。


5. 横向对比

5.1 接口对照总表

操作系统 创建 启动 停止 删除 改参数 一次性/周期表达
Linux (timer_list) timer_setup add_timer del_timer[_sync] 同停止 mod_timer 回调内重装载实现周期
Linux (hrtimer) hrtimer_init hrtimer_start hrtimer_cancel 同停止 重新 start 回调返回 HRTIMER_RESTART
FreeRTOS xTimerCreate xTimerStart xTimerStop xTimerDelete xTimerChangePeriod autoReload 参数
μC/OS-II OSTmrCreate OSTmrStart OSTmrStop OSTmrDel —(需删后重建) opt + period 参数
μC/OS-III OSTmrCreate OSTmrStart OSTmrStop OSTmrDel OSTmrSet OS_OPT_TMR_*
RT-Thread rt_timer_create/init rt_timer_start rt_timer_stop rt_timer_delete/detach rt_timer_control RT_TIMER_FLAG_*
Zephyr k_timer_init/K_TIMER_DEFINE k_timer_start k_timer_stop —(静态/无删除语义) 重新 start period=0 一次性
ThreadX tx_timer_create tx_timer_activate tx_timer_deactivate tx_timer_delete tx_timer_change resched_ticks=0 一次性
NuttX (wdog) struct wdog_s wd_start wd_cancel 重新 start 回调内重启实现周期
NuttX (POSIX) timer_create timer_settime timer_settime 置零 timer_delete 重新 settime it_interval 非零即周期
LiteOS LOS_SwtmrCreate LOS_SwtmrStart LOS_SwtmrStop LOS_SwtmrDelete —(删后重建) LOS_SWTMR_MODE_*
AliOS Things aos_timer_new / krhino_timer_create aos_timer_start aos_timer_stop aos_timer_free / krhino_timer_del aos_timer_change / krhino_timer_change repeat/round 参数

5.2 机制与上下文对照表

操作系统 时间管理结构 回调执行上下文 tickless / 低功耗 精度上限
Linux timer_list 多层时间轮 软中断 NO_HZ_IDLE/FULL 1 jiffy(1~10 ms)
Linux hrtimer 红黑树 硬中断 / 软中断 事件驱动(天然无节拍) ns 级(受硬件约束)
FreeRTOS 双有序链表(当前+溢出) 守护任务 tickless idle 1 tick
μC/OS-II 单层时间轮 定时器任务 —(无内建 tickless) 定时器任务时基(默认 100 ms)
μC/OS-III 增量链表 + tick list 定时器任务 —(无内建 tickless) 定时器任务速率(可配)
RT-Thread 跳表 中断(HARD)/ timer 线程(SOFT) PM 组件 tickless 1 tick
Zephyr 内核超时增量链表 时钟 ISR 默认 tickless 1 tick(默认 100 μs 单位)
ThreadX 有序链表 定时器 ISR(可选定时器线程) 低功耗由应用对接 1 tick
NuttX 增量链表(wdog) 中断 / 工作队列 / POSIX 信号线程 CONFIG_SCHED_TICKLESS 1 tick(受系统节拍限制)
LiteOS 有序链表 swtmr 任务 tickless + 定时器对齐 1 tick
AliOS Things 有序链表 定时器任务 Rhino tickless 1 tick

5.3 按需求选型速查

需求场景 推荐做法
回调里只做置标志/发消息,要求最低延迟 ISR 上下文型:Zephyr k_timer、RT-Thread HARD_TIMER、ThreadX、NuttX wdog
回调逻辑较复杂、要调用 OS IPC 任务上下文型:FreeRTOS、μC/OS、LiteOS、Rhino、RT-Thread SOFT_TIMER
回调中需要睡眠(I2C/SPI、mutex、内存分配) Linux delayed_work、NuttX 工作队列、Zephyr k_work_delayable,或任务上下文型里再转发
ns/μs 级高精度定时 Linux hrtimer(用户态 POSIX timer)
电池设备超长待机 tickless 支持 + 对齐聚合(Zephyr 默认、LiteOS SWTMR_ALIGN、FreeRTOS tickless idle)
大量并发定时器(数百+) 时间轮/跳表/红黑树型实现开销更可控(Linux、RT-Thread 跳表层数调高)

6. 使用建议与常见陷阱

6.1 回调写法的通用纪律

  1. 先搞清楚回调在哪个上下文执行,再决定里面能调用什么 API——这是所有定时器问题的源头。ISR 上下文禁止阻塞;任务上下文禁止长时间占用。
  2. 回调只做"标记、投递、唤醒":置标志位、xQueueSendFromISRtx_event_flags_set、投递 work item…… 把重处理交给专门任务。这几乎在每个 OS 的官方文档里都是原话级建议。
  3. 不要在守护任务/定时器线程的回调里调用阻塞 API(如 vTaskDelay、阻塞式队列读取)——堵的是全系统所有定时器。

6.2 生命周期管理

  1. 删除前确保回调不在执行中:Linux 用 del_timer_sync/hrtimer_cancel/cancel_delayed_work_sync 等"sync"系接口;RT-Thread 静态定时器用 detach、动态用 delete;μC/OS 删除前用 OSTmrStateGet 确认。
  2. 回调与删除的竞态:回调正在访问的对象被另一上下文释放,是经典 use-after-free。通用对策:同步型删除接口、引用计数、或在回调入口检查对象有效性。
  3. 一次性定时器到期不等于释放:多数系统(μC/OS、ThreadX、FreeRTOS 非自动删除语义)需要显式删除;LiteOS 的 NO_SELFDELETE 模式尤其容易漏删。

6.3 时序语义

  1. 周期定时器的漂移:确认所用 OS 的重装载基准是"理论到期点"还是"回调完成点";Linux hrtimer 用 hrtimer_forward_now 显式对齐理论点,是无漂移的标准写法。
  2. tick 换算:永远用官方换算宏(pdMS_TO_TICKSmsecs_to_jiffiesrt_tick_from_millisecondK_MSECLOS_MS2TickMSEC2TICK),不要手写 ms * HZ / 1000,节拍率变化时会静默出错。
  3. tick 回绕:32 位 tick 计数会回绕(1000 Hz 约 49.7 天,16 位 tick 仅约 65.5 秒)。比较时间必须用"差值法",禁止直接比较绝对值——各 OS 的具体处理机制与应用层纪律详见 3.7 节的专门分析。

6.4 低功耗配合

  1. tickless 下定时器精度会受睡眠深度影响:进入深度睡眠时系统 tick 停摆,唤醒后补偿——补偿误差、外设时钟丢失都可能带来偏差;对唤醒时刻敏感的场景应使用 RTC 闹钟类机制兜底。
  2. 聚合唤醒点:LiteOS 的 SWTMR_ALIGN 思路(把多个定时器对齐到同一时刻触发)在低功耗产品上能显著减少唤醒次数;其他系统可在应用层做类似设计(统一节拍、倍数关系周期)。

7. 参考资料

  • Linux Kernel Documentation:timershrtimersNO_HZ 系列文档(kernel.org)
  • FreeRTOS Kernel Developer Docs:Software Timers(freertos.org)
  • μC/OS-II / μC/OS-III 用户手册(Micrium/Weston-embedded)
  • RT-Thread 内核文档:定时器章节(rt-thread.org/document)
  • Zephyr Project Documentation:Kernel Services — Timers(docs.zephyrproject.org)
  • Eclipse ThreadX User Guide:Chapter 4 — Application Timers
  • Apache NuttX Documentation:OS Interfaces — Timers / Work Queues(nuttx.apache.org)
  • Huawei LiteOS 内核开发指南:软件定时器(SWTMR)章节
  • AliOS Things 技术文档:aos_timer 与 Rhino ktimer(alios-things.io)
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐