SwTimer:裸机与RTOS通用的轻量级软件定时器

发布时间:2026/5/19 14:06:37

SwTimer:裸机与RTOS通用的轻量级软件定时器 1. 项目概述SwTimer 是一个轻量级、可移植的纯软件定时器实现专为资源受限的嵌入式系统设计。它不依赖硬件定时器外设如 STM32 的 TIMx、NXP 的 PIT 或 ESP32 的 LEDC也不强制要求实时操作系统RTOS支持可在裸机Bare-Metal环境下直接运行同时亦能无缝集成于 FreeRTOS、RT-Thread、Zephyr 等主流 RTOS 中作为其原生定时器机制的补充或替代方案。其核心设计哲学是「极简、确定、可控」极简整个实现仅由单个 C 文件sw_timer.c和头文件sw_timer.h构成无外部依赖代码行数通常低于 300 行不含注释确定所有定时器操作启动、停止、重载、回调触发均在调用上下文内完成无中断延迟抖动无任务切换开销时间误差严格限定在主循环周期tick interval内可控用户完全掌握 tick 源头——可由 SysTick、GPT、RTC 任意硬件中断驱动也可由主循环中while(1)内的HAL_Delay(1)或osDelay(1)主动注入调度时机与粒度完全由开发者定义。该库并非对 HAL_TIM_Base_Start_IT 或 xTimerCreate 的封装而是一种独立的、基于链表轮询的软件时基抽象层。它解决的是嵌入式开发中高频出现的共性问题单片机仅有 2~4 个通用定时器但项目需管理 10 个不同周期的后台任务LED 呼吸、传感器采样、通信超时、看门狗喂狗、状态机超时跳转RTOS 定时器资源有限FreeRTOS 默认仅支持 16 个定时器且每个定时器占用约 128 字节 RAM而裸机项目又无法承担完整 RTOS 的内存与调度开销需要高精度短周期定时如 5ms 心跳检测但硬件定时器已全部被 PWM、编码器接口或输入捕获占用。SwTimer 的本质是一个「时间片驱动的状态机调度器」它将物理时间离散化为固定 tick例如 1ms在每个 tick 到来时遍历所有注册的软件定时器检查其剩余计数值是否归零并执行相应动作。这种设计使其天然具备确定性响应特性特别适用于工业控制、汽车电子诊断、医疗设备状态监控等对时序行为有强约束的场景。2. 核心架构与工作原理2.1 整体结构SwTimer 采用单向链表组织所有活动定时器链表头指针g_sw_timer_head全局可见所有定时器节点通过next指针串联。每个定时器节点为sw_timer_t类型其定义如下typedef struct { uint32_t period_ms; // 定时周期毫秒0 表示单次触发 uint32_t remain_ms; // 当前剩余计数值毫秒 uint8_t is_running; // 运行状态标志0停止1运行 uint8_t is_autoreload; // 是否自动重载0单次1周期 void (*callback)(void*); // 回调函数指针 void* user_data; // 用户私有数据指针 struct sw_timer_s* next; // 指向下一个定时器节点 } sw_timer_t;该结构体总大小为 24 字节ARM Cortex-M4 编译含 4 字节对齐填充内存占用极低。所有字段均为直接访问无隐藏状态或间接引用确保编译器可生成最优汇编指令。2.2 时间推进机制SwTimer 不主动产生时间而是被动响应外部 tick 注入。时间推进由用户调用sw_timer_tick()函数触发该函数必须以固定间隔被调用例如每 1ms 调用一次。其内部逻辑高度精简遍历链表中每个sw_timer_t节点若is_running 1则remain_ms--若remain_ms 0则执行callback(user_data)若is_autoreload 1则remain_ms period_ms否则置is_running 0定时器进入停止态继续处理下一个节点。关键点在于sw_timer_tick()是纯同步函数无任何阻塞、无任何中断使能/禁用操作可安全地在中断服务程序ISR或任务上下文中调用。例如在 STM32 的 SysTick_Handler 中直接调用void SysTick_Handler(void) { HAL_IncTick(); // 在此处注入 SwTimer tick sw_timer_tick(); // 1ms tick }或在 FreeRTOS 任务中以固定周期调用void timer_task(void *pvParameters) { const TickType_t xDelay pdMS_TO_TICKS(1); // 1ms for(;;) { vTaskDelay(xDelay); sw_timer_tick(); } }2.3 链表管理与内存模型SwTimer 不进行动态内存分配即不调用malloc/free所有定时器节点必须由用户静态声明或在栈上分配。典型用法如下// 静态全局定时器推荐用于长期运行的定时任务 static sw_timer_t led_blink_timer; static sw_timer_t sensor_read_timer; // 栈上临时定时器适用于一次性超时等待 void wait_for_response(uint32_t timeout_ms) { sw_timer_t timeout_timer; sw_timer_init(timeout_timer, timeout_ms, SW_TIMER_ONCE, timeout_callback, NULL); sw_timer_start(timeout_timer); while(timeout_timer.is_running) { sw_timer_tick(); // 主循环内推进时间 // 检查通信响应... } }sw_timer_init()函数仅初始化结构体字段不涉及链表插入sw_timer_start()才将节点挂入全局链表头部O(1) 复杂度sw_timer_stop()则从链表中摘除节点需遍历查找前驱节点最坏 O(n)。因此频繁启停的定时器应尽量避免建议通过is_running标志位控制启停逻辑而非反复调用 start/stop。链表操作全部使用指针运算无数组索引规避了边界检查开销。GCC 编译下sw_timer_tick()函数在 Cortex-M4 上典型执行时间为 80~120 个 CPU 周期72MHz处理 10 个定时器约耗时 1.5μs远低于 1ms tick 间隔不会成为系统瓶颈。3. API 接口详解SwTimer 提供 6 个核心 API全部为内联或普通 C 函数无宏封装便于调试与性能分析。函数名原型功能说明sw_timer_initvoid sw_timer_init(sw_timer_t *timer, uint32_t period_ms, sw_timer_mode_t mode, void (*callback)(void*), void *user_data)初始化定时器结构体。mode取值为SW_TIMER_ONCE单次或SW_TIMER_PERIODIC周期sw_timer_startvoid sw_timer_start(sw_timer_t *timer)启动定时器设置is_running1remain_msperiod_ms并将其插入全局链表sw_timer_stopvoid sw_timer_stop(sw_timer_t *timer)停止定时器置is_running0并从全局链表中移除该节点sw_timer_restartvoid sw_timer_restart(sw_timer_t *timer)重启定时器等效于先stop再start但避免链表重复操作sw_timer_is_runninguint8_t sw_timer_is_running(const sw_timer_t *timer)查询定时器当前运行状态返回 0 或 1sw_timer_tickvoid sw_timer_tick(void)推进全局时间遍历链表递减remain_ms并触发到期回调3.1sw_timer_init参数深度解析void sw_timer_init(sw_timer_t *timer, uint32_t period_ms, sw_timer_mode_t mode, void (*callback)(void*), void *user_data);timer指向待初始化的sw_timer_t实例。必须确保该内存区域生命周期覆盖定时器使用全程。若在函数栈上声明绝不可返回其地址给回调函数使用。period_ms定时周期单位毫秒。取值范围为1 ~ UINT32_MAX。当设为0时库内部会自动修正为1防止无限循环因remain_ms--永不归零。mode枚举类型sw_timer_mode_t定义为typedef enum { SW_TIMER_ONCE 0, SW_TIMER_PERIODIC 1 } sw_timer_mode_t;该参数直接映射到is_autoreload字段决定定时器到期后是否自动重载period_ms。callback非空函数指针必须为void func(void*)原型。SwTimer 不做任何函数签名检查错误声明将导致未定义行为UB。回调函数应在 100μs 内完成避免阻塞 tick 推进。user_data任意用户数据指针将在回调时原样传入。常用于传递结构体地址如struct sensor_ctx*、句柄如UART_HandleTypeDef*或简单整型需强制转换。3.2sw_timer_start与链表插入策略sw_timer_start()的链表插入采用头插法Head Insertion即新定时器总位于链表最前端。此设计基于两个工程考量最新启动的定时器最可能被优先关注例如通信协议栈中最新发起的请求超时应最先被检测避免遍历开销头插法为 O(1)而尾插法需遍历至末尾O(n)在频繁创建定时器的场景下优势显著。插入后链表顺序为new_timer → old_head → ...。sw_timer_tick()遍历时按此顺序执行回调因此多个定时器在同一 tick 到期时回调执行顺序与启动顺序相反LIFO。若业务逻辑存在依赖关系如 A 必须在 B 之后执行应通过user_data传递同步信号量而非依赖调用顺序。3.3sw_timer_stop的线性查找代价sw_timer_stop()需从链表头开始逐个比对next指针定位目标节点的前驱节点以便执行prev-next target-next操作。其时间复杂度为 O(n)n 为链表长度。对于 20 个定时器平均查找需 10 次指针解引用耗时约 0.3μs仍属可接受范围。为优化高频启停场景可扩展sw_timer_t结构体增加prev指针实现双向链表但会增加 4 字节内存开销及插入/删除逻辑复杂度。SwTimer 默认选择空间换时间策略符合其“轻量”定位。4. 典型应用示例与工程实践4.1 裸机环境下的多任务模拟在无 RTOS 的 STM32F103 项目中利用 SwTimer 管理 LED 控制、传感器轮询与通信超时#include sw_timer.h static sw_timer_t led_timer; static sw_timer_t sensor_timer; static sw_timer_t uart_timeout; void led_callback(void *p) { HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // Toggle LED } void sensor_poll_callback(void *p) { static uint8_t sensor_id 0; read_sensor_data(sensor_id); if (sensor_id 4) sensor_id 0; } void uart_timeout_callback(void *p) { HAL_UART_Abort(huart1); // Abort stuck UART transfer error_flag 1; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); // 初始化三个定时器 sw_timer_init(led_timer, 500, SW_TIMER_PERIODIC, led_callback, NULL); sw_timer_init(sensor_timer, 200, SW_TIMER_PERIODIC, sensor_poll_callback, NULL); sw_timer_init(uart_timeout, 1000, SW_TIMER_ONCE, uart_timeout_callback, NULL); // 启动定时器 sw_timer_start(led_timer); sw_timer_start(sensor_timer); while (1) { // 主循环处理事件 process_uart_rx(); // 推进 SwTimer 时间假设 SysTick 已配置为 1ms // 此处无需显式调用 sw_timer_tick()因已在 SysTick_Handler 中完成 // 其他业务逻辑... } }此例中三个周期性任务被解耦为独立定时器主循环不再需要if (HAL_GetTick() % 500 0)类型的硬编码轮询代码可维护性与可测试性大幅提升。4.2 FreeRTOS 集成替代 xTimerCreateFreeRTOS 定时器需占用configTIMER_TASK_PRIORITY优先级的任务栈空间且每个定时器对象消耗约 128 字节 RAM。SwTimer 可作为轻量替代方案#include FreeRTOS.h #include task.h #include sw_timer.h // 全局定时器链表与 FreeRTOS 任务 static sw_timer_t rtos_timer1; static sw_timer_t rtos_timer2; void rtos_timer1_callback(void *p) { BaseType_t xHigherPriorityTaskWoken pdFALSE; // 发送消息到高优先级任务 xQueueSendFromISR(msg_queue, msg, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } void sw_timer_task(void *pvParameters) { const TickType_t xDelay pdMS_TO_TICKS(1); for(;;) { vTaskDelay(xDelay); sw_timer_tick(); // 每 1ms 推进一次 } } // 创建 SwTimer 任务优先级低于关键任务高于 idle xTaskCreate(sw_timer_task, SwTimer, 128, NULL, 2, NULL); // 在应用初始化中启动定时器 sw_timer_init(rtos_timer1, 10, SW_TIMER_PERIODIC, rtos_timer1_callback, NULL); sw_timer_start(rtos_timer1);对比xTimerCreate(..., 10, pdTRUE, ...)SwTimer 版本节省约 100 字节 RAM且回调直接在sw_timer_task上下文中执行避免了 FreeRTOS 定时器任务的额外上下文切换。4.3 高精度超时控制I2C 通信健壮性增强I2C 总线易受干扰导致 SCL 时钟拉低硬件超时难以覆盖所有异常。SwTimer 可实现软件级超时保护#define I2C_TIMEOUT_MS 100 static sw_timer_t i2c_timeout_timer; static volatile uint8_t i2c_busy 0; void i2c_timeout_handler(void *p) { __disable_irq(); // 禁用全局中断确保原子性 if (i2c_busy) { LL_I2C_DeInit(I2C1); // 强制复位 I2C 外设 LL_I2C_Init(I2C1, I2C_InitStruct); i2c_error_count; } __enable_irq(); } bool i2c_write_with_timeout(I2C_TypeDef *I2Cx, uint8_t dev_addr, uint8_t *data, uint8_t len) { i2c_busy 1; sw_timer_init(i2c_timeout_timer, I2C_TIMEOUT_MS, SW_TIMER_ONCE, i2c_timeout_handler, NULL); sw_timer_start(i2c_timeout_timer); bool ret HAL_I2C_Master_Transmit(I2Cx, dev_addr, data, len, HAL_MAX_DELAY); sw_timer_stop(i2c_timeout_timer); // 成功则立即停止 i2c_busy 0; return ret; }此方案将 I2C 操作从“无限等待”变为“确定性超时”极大提升系统鲁棒性且不增加额外硬件资源消耗。5. 配置选项与移植指南SwTimer 本身无编译期配置宏但其行为可通过以下方式定制5.1 Tick 精度与分辨率sw_timer_tick()的调用频率直接决定最小定时分辨率。若需 10ms 分辨率可每 10ms 调用一次若需 100μs则需更高频 tick 源如 10kHz SysTick。注意过高的 tick 频率会增加 CPU 占用需权衡。公式如下CPU 占用率 ≈ (单次 sw_timer_tick() 耗时 × 定时器数量) × tick 频率例如20 个定时器、10kHz tick、单次耗时 100ns → 占用率 ≈ 0.02%。5.2 中断安全与临界区SwTimer 的链表操作start/stop非原子若在 ISR 与主循环中并发调用需加临界区保护。推荐使用 CMSIS__disable_irq()/__enable_irq()void sw_timer_start_safe(sw_timer_t *timer) { uint32_t primask __get_PRIMASK(); __disable_irq(); sw_timer_start(timer); __set_PRIMASK(primask); }sw_timer_tick()本身是只读遍历无需保护可安全在 ISR 中调用。5.3 跨平台移植要点SwTimer 仅依赖 C99 标准库stdint.h和stdbool.h移植至新平台仅需确认uint32_t等类型定义正确通过stdint.h__disable_irq()/__enable_irq()可替换为对应平台的关中断指令如 RISC-V 的csrrsi若目标平台无 SysTick可使用任意 GPT 定时器中断服务程序调用sw_timer_tick()。无须修改 SwTimer 源码仅需适配 tick 注入点与临界区接口。6. 性能边界与限制条件SwTimer 的适用性存在明确工程边界开发者必须清醒认知最大定时器数量受限于 RAM 容量。每个定时器 24 字节1KB RAM 可容纳约 42 个定时器最长定时周期remain_ms为uint32_t理论最大4294967295 ms ≈ 49.7 天。实际中超过 1 小时的定时器应考虑 RTC 硬件支持最小可靠周期取决于sw_timer_tick()调用频率。若 tick 为 1ms则最小周期为 1ms若 tick 为 10ms则无法实现 5ms 定时回调执行约束回调函数内禁止调用sw_timer_start/stop/restart否则引发链表遍历中修改的未定义行为。需通过消息队列或状态标志异步触发定时器操作。这些限制非缺陷而是设计取舍的结果。SwTimer 的价值正在于以清晰的边界换取极致的简洁与可控——当项目需求落入其边界内时它是最优解当需求超出边界时应果断选用硬件定时器或完整 RTOS而非强行扩展 SwTimer。在某工业 PLC 项目中团队曾用 SwTimer 管理 17 个现场总线状态监控定时器周期 100ms~2s连续运行 18 个月无一例定时偏差验证了其在严苛环境下的可靠性。这印证了一个嵌入式底层工程师的朴素信条最可靠的代码是那些你完全理解其每一行汇编指令的代码。

相关新闻