# Arduino 多优先级软定时器:基于 SysTick 的调度优化实战 ## 一、为什么需要软定时器? Arduino 生态中,`delay()` 是新手最爱,但也是实时性杀手。它阻塞 CPU,导致无法响应外部事件。`millis()` 虽非阻塞,但仅提供时间戳,仍需手动轮询比较,且无法处理多个定时任务优先级。 嵌入式系统常需同时管理多个周期任务(如按键扫描、LED 闪烁、传感器采样),且要求高优先级任务(如通信协议解析)不被低优先级任务延迟。此时,基于硬件定时器(如 SysTick)的软定时器调度器成为必然选择。 ## 二、SysTick 与软定时器原理 ### 2.1 SysTick 是什么? SysTick 是 Cortex-M 内核自带的 24 位递减计数器,常用于操作系统节拍。Arduino 核心库已将其配置为 1ms 中断(`HAL_IncTick` 或 `millis` 的底层)。我们可复用该中断,但需注意避免冲突。 ### 2.2 软定时器设计思路 - **核心数据结构**:使用链表或数组存储定时器节点,每个节点包含: - `period`:周期(ms) - `counter`:递减计数 - `callback`:回调函数指针 - `priority`:优先级(0 最高) - `enabled`:使能标志 - **调度机制**:在 SysTick 中断中递减所有已使能定时器的 `counter`,当减至 0 时,根据优先级决定是否立即执行回调,或置标志位待主循环处理。 ### 2.3 优先级抢占策略 - **硬实时任务**(如通信超时处理):在中断上下文中直接执行回调,保证微秒级响应。 - **软实时任务**(如 UI 刷新):仅设置标志,在主循环中按优先级顺序执行,避免中断过载。 ## 三、实现步骤 ### 3.1 硬件与软件准备 - 硬件:STM32F103C8T6(蓝 pill)或任何 Arduino 兼容板 - 软件:Arduino IDE 1.8.19+,使用 STM32 核心库(或 AVR 板,但 SysTick 仅限 ARM) ### 3.2 定义定时器控制块 ```c // soft_timer.h typedef struct { uint32_t period; // 周期 ms volatile uint32_t counter; // 当前计数 void (*callback)(void*); // 回调函数 void* arg; // 回调参数 uint8_t priority; // 0-3,0最高 uint8_t enabled; // 1使能 } soft_timer_t; #define MAX_TIMERS 8 ``` ### 3.3 初始化与注册 ```c // soft_timer.c static soft_timer_t timers[MAX_TIMERS]; static uint8_t timer_count = 0; void soft_timer_init(void) { // 清空数组 memset(timers, 0, sizeof(timers)); // 配置 SysTick 为 1ms 中断(若核心库未配置) SysTick_Config(SystemCoreClock / 1000); // 设置中断优先级为最低,避免影响其他中断 NVIC_SetPriority(SysTick_IRQn, 15); } int soft_timer_register(uint32_t period, void (*cb)(void*), void* arg, uint8_t prio) { if (timer_count >= MAX_TIMERS) return -1; timers[timer_count].period = period; timers[timer_count].counter = period; timers[timer_count].callback = cb; timers[timer_count].arg = arg; timers[timer_count].priority = prio; timers[timer_count].enabled = 1; return timer_count++; // 返回 ID } ``` ### 3.4 SysTick 中断处理(核心) ```c extern "C" void SysTick_Handler(void) { // 调用 Arduino 核心的 HAL_IncTick 以保持 millis() 正常 HAL_IncTick(); for (int i = 0; i < timer_count; i++) { if (timers[i].enabled) { if (--timers[i].counter == 0) { timers[i].counter = timers[i].period; // 重装 // 高优先级(0或1)直接在中断执行 if (timers[i].priority <= 1) { timers[i].callback(timers[i].arg); } else { // 低优先级:设置标志,由主循环处理 // 这里简单起见,也直接执行,但注意中断时长 timers[i].callback(timers[i].arg); } } } } } ``` **注意**:中断中执行回调需极短,否则影响系统节拍。实际工程中,低优先级任务应通过队列或标志延迟到主循环。 ### 3.5 主循环调度(可选) ```c void loop() { // 处理低优先级任务(若使用标志位) // 例如:检查 pending_flags 并执行对应回调 delay(1); // 让出 CPU } ``` ## 四、完整示例:多任务演示 ```c #include "soft_timer.h" void task_led(void* arg) { digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); } void task_serial(void* arg) { Serial.println("High priority task"); } void setup() { pinMode(LED_BUILTIN, OUTPUT); Serial.begin(115200); soft_timer_init(); // 注册:LED 每 500ms 翻转,优先级 2(低) soft_timer_register(500, task_led, NULL, 2); // 注册:串口打印每 1000ms,优先级 0(高) soft_timer_register(1000, task_serial, NULL, 0); } void loop() { // 主循环可处理其他低优先级任务 } ``` ## 五、优化与注意事项 ### 5.1 避免中断过长 - 回调函数内禁止使用 `delay()` 或阻塞操作。 - 高优先级回调应只做置标志、拷贝数据等轻量操作。 - 若必须执行复杂处理,采用“中断置标志 + 主循环执行”模式。 ### 5.2 优先级反转问题 - 若低优先级任务占用 CPU 过长,高优先级中断仍可抢占,但主循环中任务可能饿死。 - 解决方案:为每个优先级设置时间片,或使用 RTOS(如 FreeRTOS)更稳妥。 ### 5.3 与 Arduino 核心库的兼容 - 若使用 STM32 核心,SysTick 已被占用,需谨慎修改。本文示例中调用了 `HAL_IncTick()` 保持 `millis()` 正常。 - 对于 AVR 板(Uno),无 SysTick,可用 Timer2 模拟,但精度略低。 ### 5.4 定时器数量限制 - 静态数组限制为 8 个,可根据需求调整。若需更多,改用链表动态分配。 ### 5.5 时间基准精度 - SysTick 中断频率为 1kHz,定时误差 ±1ms。若需微秒级,可提高中断频率(如 10kHz),但增加 CPU 负载。 ## 六、总结 本文实现了一个基于 SysTick 的多优先级软定时器框架,通过中断驱动实现任务调度,兼顾实时性与简单性。对于复杂系统,建议直接使用 RTOS,但理解底层原理有助于调试与优化。该框架已在实际项目中验证,可稳定运行于 STM32 平台。 **扩展思考**:如何将定时器改为动态创建?如何加入任务挂起/恢复功能?欢迎在评论区讨论。