Arduino 多优先级软定时器:基于 SysTick 的调度优化实战

一、为什么需要软定时器?

Arduino 生态中,delay() 是新手最爱,但也是实时性杀手。它阻塞 CPU,导致无法响应外部事件。millis() 虽非阻塞,但仅提供时间戳,仍需手动轮询比较,且无法处理多个定时任务优先级。

嵌入式系统常需同时管理多个周期任务(如按键扫描、LED 闪烁、传感器采样),且要求高优先级任务(如通信协议解析)不被低优先级任务延迟。此时,基于硬件定时器(如 SysTick)的软定时器调度器成为必然选择。

二、SysTick 与软定时器原理

2.1 SysTick 是什么?

SysTick 是 Cortex-M 内核自带的 24 位递减计数器,常用于操作系统节拍。Arduino 核心库已将其配置为 1ms 中断(HAL_IncTickmillis 的底层)。我们可复用该中断,但需注意避免冲突。

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 平台。

扩展思考:如何将定时器改为动态创建?如何加入任务挂起/恢复功能?欢迎在评论区讨论。