引言
在 Arduino 这类资源有限的嵌入式平台上,直接运行 RTOS 往往显得笨重。协作式调度器(Cooperative Scheduler)通过任务主动让出 CPU 的方式,实现多任务并发,代码量极小且无上下文切换开销。本文的核心是一个基于定时器中断的调度器,其核心代码仅 10 行,却足以支撑多任务轮转。
原理讲解
协作式 vs 抢占式
-
协作式:任务必须显式调用
yield()或delay()来让出 CPU,否则会阻塞其他任务。 - 抢占式:由定时器中断强制切换任务,无需任务主动配合,但需要保存/恢复上下文。
本文采用协作式,但利用定时器中断来产生时间基准,驱动任务调度。
调度器核心机制
调度器维护一个任务数组,每个任务包含:
- 函数指针
func - 执行周期
period(毫秒) - 上次执行时间
lastRun
定时器中断(如 Timer1)每 1ms 触发一次,在中断服务程序(ISR)中更新一个全局 tick 计数。主循环中,调度器遍历任务列表,检查当前 tick 是否达到任务的执行周期,若达到则调用任务函数。
为什么是 10 行?
核心调度逻辑可以浓缩为以下 10 行(不含任务定义和初始化):
void scheduler_run() {
for (int i = 0; i < taskCount; i++) {
if (millis() - tasks[i].lastRun >= tasks[i].period) {
tasks[i].lastRun = millis();
tasks[i].func();
}
}
}
这里 millis() 由定时器中断维护,每 1ms 递增。这 10 行代码实现了非阻塞的周期任务调度。
配置步骤
1. 定义任务结构体
typedef struct {
void (*func)(void);
unsigned long period;
unsigned long lastRun;
} Task;
2. 初始化定时器中断
在 Arduino 上,我们使用 Timer1 产生 1ms 中断:
#include <TimerOne.h>
void setup() {
Timer1.initialize(1000); // 1000us = 1ms
Timer1.attachInterrupt(timerISR);
}
volatile unsigned long tick = 0;
void timerISR() {
tick++;
}
注意:millis() 本身也是基于定时器中断,但为了演示,我们自定义 tick。实际中可直接使用 millis(),但需注意其精度和溢出。
3. 实现调度器
#define MAX_TASKS 5
Task tasks[MAX_TASKS];
int taskCount = 0;
void scheduler_init() {
taskCount = 0;
}
int scheduler_add(void (*func)(), unsigned long period) {
if (taskCount >= MAX_TASKS) return -1;
tasks[taskCount].func = func;
tasks[taskCount].period = period;
tasks[taskCount].lastRun = 0;
return taskCount++;
}
void scheduler_run() {
for (int i = 0; i < taskCount; i++) {
if (millis() - tasks[i].lastRun >= tasks[i].period) {
tasks[i].lastRun = millis();
tasks[i].func();
}
}
}
4. 主循环与任务定义
void task1() {
digitalWrite(13, !digitalRead(13)); // 翻转 LED
}
void task2() {
Serial.println("Task2 running");
}
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
scheduler_init();
scheduler_add(task1, 500); // 每 500ms 执行
scheduler_add(task2, 1000); // 每 1000ms 执行
Timer1.initialize(1000);
Timer1.attachInterrupt(timerISR);
}
void loop() {
scheduler_run();
// 其他非阻塞操作
}
完整代码示例
#include <TimerOne.h>
#define MAX_TASKS 5
typedef struct {
void (*func)(void);
unsigned long period;
unsigned long lastRun;
} Task;
Task tasks[MAX_TASKS];
int taskCount = 0;
volatile unsigned long tick = 0;
void timerISR() { tick++; }
void scheduler_init() { taskCount = 0; }
int scheduler_add(void (*func)(), unsigned long period) {
if (taskCount >= MAX_TASKS) return -1;
tasks[taskCount].func = func;
tasks[taskCount].period = period;
tasks[taskCount].lastRun = 0;
return taskCount++;
}
void scheduler_run() {
for (int i = 0; i < taskCount; i++) {
if (millis() - tasks[i].lastRun >= tasks[i].period) {
tasks[i].lastRun = millis();
tasks[i].func();
}
}
}
void task1() {
digitalWrite(13, !digitalRead(13));
}
void task2() {
Serial.println("Task2 running");
}
void setup() {
Serial.begin(9600);
pinMode(13, OUTPUT);
scheduler_init();
scheduler_add(task1, 500);
scheduler_add(task2, 1000);
Timer1.initialize(1000);
Timer1.attachInterrupt(timerISR);
}
void loop() {
scheduler_run();
}
注意事项
-
任务不能阻塞:协作式调度要求任务必须快速返回,不能使用
delay()或长时间循环,否则会延迟其他任务。 -
中断与主循环共享变量:
tick是volatile,但主循环中读取millis()时,应使用原子读取(Arduino 的millis()已保证原子性)。 -
定时器精度:
Timer1的 1ms 中断精度足够大多数应用,但若需更高精度,可改用微秒级定时器。 -
任务数量限制:
MAX_TASKS需根据实际调整,避免数组越界。 -
溢出处理:
millis()约 49 天溢出,但上述代码中millis() - lastRun的差值计算可正确处理溢出(无符号减法)。
总结
本文通过 10 行核心代码展示了 Arduino 上无 OS 的协作式调度器实现。它利用定时器中断提供时间基准,主循环中轮询任务,实现了轻量级多任务调度。这种方法在资源受限的嵌入式系统中非常实用,且易于扩展。理解其原理后,你可以根据需求增加优先级、任务删除等功能,构建更强大的调度器。