引言
在嵌入式开发中,当需要同时处理多个任务(如按键扫描、LED闪烁、传感器读取)时,初学者常陷入delay()阻塞的困境。协作式调度器(Cooperative Scheduler)提供了一种轻量级解决方案,无需操作系统即可实现任务并发。本文将基于Arduino平台,剖析其设计核心,助你掌握这一关键技术。
协作式调度器原理
协作式调度器基于非抢占式多任务模型:任务主动让出CPU(通过yield()或delay()),调度器才切换至下一任务。与抢占式RTOS不同,协作式调度器无需复杂上下文切换,内存占用极低,适合资源受限的MCU(如ATmega328P)。
核心思想:
- 任务以函数形式注册,每个任务具有独立状态(就绪、运行、等待)。
- 调度器维护任务列表,按优先级或时间片轮转执行。
- 任务通过
delay()或yield()释放CPU,实现协作。
设计要点
1. 任务控制块(TCB)
每个任务需一个控制块,存储函数指针、周期、上次执行时间、状态等。
typedef struct {
void (*func)(void); // 任务函数
unsigned long period; // 执行周期(ms)
unsigned long lastRun; // 上次执行时间戳
uint8_t enabled; // 使能标志
} Task;
2. 调度策略
- 时间片轮转:所有任务按顺序执行,每个任务运行至主动让出。
-
周期调度:基于
millis()实现定时触发,适合周期性任务(如每10ms扫描按键)。 - 优先级调度:高优先级任务先执行,但需防止低优先级饿死,可结合时间片。
推荐采用周期调度+时间片轮转结合,兼顾实时性与公平性。
3. 定时管理
使用millis()而非delay(),避免阻塞。核心是记录上次执行时间,判断是否到达周期。
unsigned long now = millis();
if (now - task.lastRun >= task.period) {
task.func();
task.lastRun = now;
}
注意millis()溢出问题(约49天),需使用无符号减法处理。
4. 任务状态与协作
任务内部需主动让出CPU,常用方式:
-
delay(0):仅让出当前时间片,不等待。 -
delay(ms):让出并等待指定时间,期间调度器可运行其他任务。 - 自定义
yield():调用调度器切换。
示例:
void taskBlink() {
digitalWrite(LED, !digitalRead(LED));
delay(100); // 让出CPU 100ms
}
5. 内存与栈管理
Arduino Uno仅有2KB SRAM,任务函数需避免大数组和递归。每个任务独立栈?协作式调度器通常共享栈,因此任务内局部变量需谨慎,避免深度调用。建议:
- 使用
static或全局变量保存状态。 - 避免在任务内定义大数组。
- 可设置栈检查(如
StackHighMark)监控使用量。
完整代码示例
以下实现一个简单的协作式调度器,包含两个任务:LED闪烁和串口打印。
// 协作式调度器示例
#include <Arduino.h>
#define MAX_TASKS 5
typedef struct {
void (*func)(void);
unsigned long period;
unsigned long lastRun;
uint8_t enabled;
} Task;
Task tasks[MAX_TASKS];
uint8_t taskCount = 0;
// 注册任务
bool addTask(void (*f)(void), unsigned long period) {
if (taskCount >= MAX_TASKS) return false;
tasks[taskCount].func = f;
tasks[taskCount].period = period;
tasks[taskCount].lastRun = 0;
tasks[taskCount].enabled = 1;
taskCount++;
return true;
}
// 调度器主循环
void schedulerRun() {
unsigned long now = millis();
for (uint8_t i = 0; i < taskCount; i++) {
if (tasks[i].enabled && (now - tasks[i].lastRun >= tasks[i].period)) {
tasks[i].func();
tasks[i].lastRun = now;
}
}
}
// 任务1:LED闪烁
void taskLED() {
static uint8_t state = 0;
digitalWrite(13, state);
state = !state;
// 无需delay,调度器自动按周期调用
}
// 任务2:串口打印
void taskPrint() {
Serial.println("Task Print Running");
}
void setup() {
pinMode(13, OUTPUT);
Serial.begin(9600);
addTask(taskLED, 500); // 每500ms切换
addTask(taskPrint, 1000); // 每1s打印
}
void loop() {
schedulerRun();
}
注意事项
- 任务执行时间:任务函数应快速返回,避免长时间占用CPU,否则影响其他任务实时性。
- 共享资源保护:协作式调度器无互斥锁,若任务间共享变量,需在任务内关闭中断或使用原子操作。
- 优先级反转:若采用优先级调度,需注意低优先级任务持锁导致高优先级等待。
-
调试技巧:使用
Serial打印任务执行时间,或利用micros()测量任务耗时。 - 扩展性:可增加任务删除、暂停功能,或支持动态优先级。
总结
协作式调度器是嵌入式多任务编程的基石,其设计简单、高效,适合大多数Arduino应用。通过合理设计任务结构、调度策略和内存管理,可显著提升系统响应性。掌握其要点,为后续学习RTOS打下坚实基础。