Arduino 多优先级软定时器:基于 SysTick 的调度优化实战
👁 3 阅读 · 2026-08-27 · 嵌入式
在 Arduino 开发中,`delay()` 和 `millis()` 常被滥用,导致系统响应迟缓、任务阻塞。本文深入剖析基于 SysTick 的软定时器原理,设计一套支持多优先级抢占的调度框架,通过中断驱动与时间片轮转结合,实现微秒级定时精度与任务隔离。文章包含完整代码示例、配置步骤及避坑指南,帮助开发者摆脱轮询噩梦,构建实时性更强的嵌入式应用。
# 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 平台。
**扩展思考**:如何将定时器改为动态创建?如何加入任务挂起/恢复功能?欢迎在评论区讨论。