Arduino 无 OS 协作式调度器:基于定时器中断的 10 行核心代码深度剖析
👁 1 阅读 · 2026-08-27 · 嵌入式
在资源受限的嵌入式系统中,协作式调度器是替代 RTOS 的轻量级方案。本文以 Arduino 平台为例,通过一个基于定时器中断的 10 行核心代码,剖析其原理、实现步骤与注意事项。你将理解如何用极简代码实现多任务时间片轮转,并掌握在无 OS 环境下管理任务调度的精髓。
# 引言
在 Arduino 这类资源有限的嵌入式平台上,直接运行 RTOS 往往显得笨重。协作式调度器(Cooperative Scheduler)通过任务主动让出 CPU 的方式,实现多任务并发,代码量极小且无上下文切换开销。本文的核心是一个基于定时器中断的调度器,其核心代码仅 10 行,却足以支撑多任务轮转。
# 原理讲解
## 协作式 vs 抢占式
- **协作式**:任务必须显式调用 `yield()` 或 `delay()` 来让出 CPU,否则会阻塞其他任务。
- **抢占式**:由定时器中断强制切换任务,无需任务主动配合,但需要保存/恢复上下文。
本文采用协作式,但利用定时器中断来产生时间基准,驱动任务调度。
## 调度器核心机制
调度器维护一个任务数组,每个任务包含:
- 函数指针 `func`
- 执行周期 `period`(毫秒)
- 上次执行时间 `lastRun`
定时器中断(如 `Timer1`)每 1ms 触发一次,在中断服务程序(ISR)中更新一个全局 tick 计数。主循环中,调度器遍历任务列表,检查当前 tick 是否达到任务的执行周期,若达到则调用任务函数。
## 为什么是 10 行?
核心调度逻辑可以浓缩为以下 10 行(不含任务定义和初始化):
```c
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. 定义任务结构体
```c
typedef struct {
void (*func)(void);
unsigned long period;
unsigned long lastRun;
} Task;
```
## 2. 初始化定时器中断
在 Arduino 上,我们使用 `Timer1` 产生 1ms 中断:
```c
#include
void setup() {
Timer1.initialize(1000); // 1000us = 1ms
Timer1.attachInterrupt(timerISR);
}
volatile unsigned long tick = 0;
void timerISR() {
tick++;
}
```
注意:`millis()` 本身也是基于定时器中断,但为了演示,我们自定义 tick。实际中可直接使用 `millis()`,但需注意其精度和溢出。
## 3. 实现调度器
```c
#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. 主循环与任务定义
```c
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();
// 其他非阻塞操作
}
```
# 完整代码示例
```c
#include
#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 的协作式调度器实现。它利用定时器中断提供时间基准,主循环中轮询任务,实现了轻量级多任务调度。这种方法在资源受限的嵌入式系统中非常实用,且易于扩展。理解其原理后,你可以根据需求增加优先级、任务删除等功能,构建更强大的调度器。