# 引言 在 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 的协作式调度器实现。它利用定时器中断提供时间基准,主循环中轮询任务,实现了轻量级多任务调度。这种方法在资源受限的嵌入式系统中非常实用,且易于扩展。理解其原理后,你可以根据需求增加优先级、任务删除等功能,构建更强大的调度器。