引言
Arduino凭借其简易性成为原型开发首选,但面对复杂实时任务时,其默认的delay()和millis()轮询方式往往力不从心。硬件定时器(如Timer1、Timer2)数量有限,且中断优先级固定,难以满足多任务差异化响应需求。本文面向有经验的开发者,探讨在Arduino上实现多优先级软定时器中断的替代方案,剖析其原理与陷阱,并给出一个健壮的软件实现。
为什么需要软定时器中断?
- 硬件定时器通常只有2-3个,且多被Arduino核心库占用(如
millis()使用Timer0)。 - 中断优先级由硬件决定,无法动态调整,导致高实时性任务可能被低优先级中断阻塞。
- 软定时器通过软件调度模拟多定时器,可灵活分配优先级,且不占用额外硬件资源。
替代方案概览
1. 基于millis()的非阻塞轮询
- 原理:在主循环中不断检查
millis()差值,达到设定时间则执行任务。 - 优点:简单、无额外资源。
- 缺点:无法实现真正中断,主循环阻塞时定时失效;优先级需手动管理,易出现长任务拖垮其他任务。
2. Ticker库(如Arduino Ticker)
- 原理:利用一个硬件定时器(通常为Timer2)产生周期性中断,在中断中回调注册函数。
- 优点:近似中断行为,支持多个定时任务。
- 缺点:所有回调在中断上下文中执行,若回调耗时过长会阻塞其他中断;优先级仅靠注册顺序,无法抢占;且库实现可能使用
delayMicroseconds等阻塞函数,引发不可预测延迟。
3. TimerOne/TimerThree库
- 原理:直接配置硬件定时器,允许自定义中断服务函数(ISR)。
- 优点:精确、可设置优先级(通过修改NVIC)。
- 缺点:仅限特定定时器,且需手动管理多个定时器,代码复杂度高;在Arduino Uno上仅Timer1可用,多任务需软件扩展。
4. 自研软定时器调度器(推荐)
- 原理:利用一个硬件定时器产生固定节拍(如1ms),在ISR中维护一个优先级队列,根据到期时间触发对应任务,任务在中断外或中断内执行(取决于设计)。
- 优点:可灵活配置优先级、支持抢占(若任务在中断外执行,可通过调度器切换)、可扩展性强。
- 缺点:实现复杂,需注意临界区保护。
陷阱分析
陷阱1:在ISR中执行耗时操作
- 现象:Ticker回调中调用
Serial.print()或delay(),导致系统卡顿或丢失定时。 - 原因:ISR应短小精悍,耗时操作会阻塞其他中断,且
delay()依赖中断,在ISR中调用会死锁。 - 解决:ISR仅设置标志位或写入队列,实际处理放在主循环。
陷阱2:优先级反转
- 现象:低优先级任务长时间占用CPU,高优先级任务无法及时执行。
- 原因:在非抢占式调度中,低优先级任务若在临界区,高优先级任务需等待。
- 解决:使用抢占式调度(如基于状态机切换),或确保临界区极短。
陷阱3:定时器溢出与时间比较错误
- 现象:使用
millis()时,当计数器回绕(约49天)后,直接比较current - start > interval会出错。 - 原因:无符号减法在溢出时仍正确,但若用有符号比较则失败。
- 解决:使用
(uint32_t)(current - start) < interval形式,或采用硬件定时器计数。
陷阱4:动态内存分配在ISR中
- 现象:在ISR中使用
new或malloc,导致堆碎片或崩溃。 - 原因:ISR不可重入,且动态分配非线程安全。
- 解决:预先分配静态内存或使用内存池。
实战:实现一个多优先级软定时器调度器
以下代码基于Arduino Uno,利用Timer2产生1ms节拍,维护一个固定大小的任务表,支持优先级(数值越小优先级越高)。任务在中断中仅标记到期,实际执行在主循环中,避免ISR过长。
原理
- 使用
Timer2配置为CTC模式,产生1ms中断。 - 在ISR中遍历任务表,将到期任务的状态置为
READY,并记录当前时间。 - 主循环中,选择最高优先级的
READY任务执行,执行完毕后清除状态。 - 任务函数需为非阻塞,或采用状态机分段执行。
配置步骤
- 定义任务控制块(TCB)结构体。
- 初始化定时器2,设置比较匹配寄存器。
- 注册任务(设置周期、优先级、回调函数)。
- 在主循环中调用调度器。
完整代码示例
#include <Arduino.h>
#define MAX_TASKS 5
#define TICK_MS 1
// 任务状态
typedef enum {
TASK_STOPPED,
TASK_RUNNING,
TASK_READY
} TaskState;
typedef struct {
uint32_t period; // 周期(ms)
uint32_t last_run; // 上次执行时间戳
uint8_t priority; // 优先级,0最高
void (*callback)(); // 回调函数
TaskState state;
} TCB;
static TCB tasks[MAX_TASKS];
static volatile uint32_t tick_count = 0;
// Timer2 ISR,1ms触发
ISR(TIMER2_COMPA_vect) {
tick_count++;
for (int i = 0; i < MAX_TASKS; i++) {
if (tasks[i].state != TASK_STOPPED) {
if ((uint32_t)(tick_count - tasks[i].last_run) >= tasks[i].period) {
tasks[i].state = TASK_READY;
tasks[i].last_run = tick_count;
}
}
}
}
// 初始化定时器2
void init_timer2() {
cli();
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0;
OCR2A = (F_CPU / 1024 / 1000) - 1; // 1ms @16MHz, prescaler 1024
TCCR2A |= (1 << WGM21); // CTC模式
TCCR2B |= (1 << CS22) | (1 << CS21) | (1 << CS20); // 1024分频
TIMSK2 |= (1 << OCIE2A); // 使能比较匹配中断
sei();
}
// 注册任务
bool add_task(uint8_t index, uint32_t period, uint8_t priority, void (*cb)()) {
if (index >= MAX_TASKS || cb == NULL) return false;
tasks[index].period = period;
tasks[index].priority = priority;
tasks[index].callback = cb;
tasks[index].last_run = tick_count;
tasks[index].state = TASK_RUNNING; // 初始为运行态,但实际未执行
return true;
}
// 调度器:主循环调用
void scheduler_run() {
int highest_pri = -1;
int task_index = -1;
// 查找最高优先级READY任务
for (int i = 0; i < MAX_TASKS; i++) {
if (tasks[i].state == TASK_READY) {
if (highest_pri == -1 || tasks[i].priority < highest_pri) {
highest_pri = tasks[i].priority;
task_index = i;
}
}
}
if (task_index != -1) {
tasks[task_index].state = TASK_RUNNING;
tasks[task_index].callback(); // 执行任务
// 若任务为一次性,可停止;否则保持运行态,等待下次到期
// 这里假设任务周期执行,执行后状态由ISR下次置READY
tasks[task_index].state = TASK_RUNNING; // 重新置为运行,等待下次
}
}
// 示例任务
void task_high() {
static uint8_t led_state = 0;
digitalWrite(13, led_state ^= 1); // 翻转LED
}
void task_low() {
// 模拟耗时任务,但非阻塞
static uint32_t last = 0;
if (millis() - last > 500) {
Serial.println("Low pri task");
last = millis();
}
}
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
init_timer2();
add_task(0, 100, 0, task_high); // 100ms周期,优先级0
add_task(1, 1000, 1, task_low); // 1000ms周期,优先级1
}
void loop() {
scheduler_run();
// 其他主循环工作
}
注意事项
-
临界区保护:在修改任务表或访问
tick_count时,应暂时关闭中断(cli()/sei()),防止数据竞争。 - 任务执行时间:若任务执行时间超过其周期,会错过下一次触发,需设计为可重入或状态机。
- 优先级抢占:上述实现为非抢占式,若需抢占,可在ISR中直接执行高优先级任务,但需确保任务短小。
-
定时器精度:
OCR2A计算基于16MHz,若使用其他板子需调整分频和比较值。 - 内存使用:任务表静态分配,避免动态内存。
总结
Arduino上的多优先级软定时器实现需权衡资源与实时性。Ticker库简单但易踩坑,自研调度器虽复杂但可控性强。本文提供的方案通过节拍中断与主循环调度分离,有效避免ISR过长问题,并支持优先级选择。开发者应根据实际需求,选择合适方案,并时刻警惕中断上下文陷阱,确保系统稳定可靠。