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 定义定时器控制块
// 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 初始化与注册
// 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 中断处理(核心)
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 主循环调度(可选)
void loop() {
// 处理低优先级任务(若使用标志位)
// 例如:检查 pending_flags 并执行对应回调
delay(1); // 让出 CPU
}
四、完整示例:多任务演示
#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 平台。
扩展思考:如何将定时器改为动态创建?如何加入任务挂起/恢复功能?欢迎在评论区讨论。