引言
在嵌入式开发中,多任务处理是常见需求。然而,对于 Arduino 这类资源有限的平台,完整 RTOS(如 FreeRTOS)可能占用过多 RAM/Flash,且引入额外复杂度。协作式调度器(Cooperative Scheduler)提供了一种轻量级替代方案:任务主动让出 CPU,由调度器按策略切换。结合状态机(State Machine),我们可以实现非阻塞的任务切换,让系统看起来“同时”运行多个任务。
原理讲解
协作式调度 vs 抢占式调度
- 抢占式调度:由定时器中断强制切换任务,需要保存/恢复上下文,对硬件要求高。
-
协作式调度:任务自行决定何时让出 CPU(如调用
yield()),无需硬件上下文切换,实现简单,适合事件驱动或周期性任务。
状态机任务模型
每个任务被建模为一个有限状态机(FSM)。任务在每次被调度时执行一个“步骤”,根据当前状态执行相应操作,然后转移到下一个状态。通过状态转移,任务可以分阶段完成长操作,避免阻塞。
调度策略
最简单的协作式调度器采用轮询(Round-Robin):按顺序依次调用每个任务的 update() 函数。任务内部通过状态机控制执行节奏,若某任务需等待外部事件,则直接返回,不占用 CPU。
实现步骤
1. 定义任务结构
我们定义一个 Task 结构体,包含状态变量、周期、上次执行时间等。
typedef struct {
void (*update)(void); // 任务函数指针
uint32_t period; // 执行周期(ms)
uint32_t last_run; // 上次执行时间
uint8_t state; // 当前状态(用户自定义)
} Task;
2. 实现调度器核心
调度器维护一个任务数组,并在 loop() 中遍历调用。
#define MAX_TASKS 5
Task tasks[MAX_TASKS];
uint8_t task_count = 0;
void scheduler_init() {
task_count = 0;
}
bool add_task(void (*update)(void), uint32_t period) {
if (task_count >= MAX_TASKS) return false;
tasks[task_count].update = update;
tasks[task_count].period = period;
tasks[task_count].last_run = 0;
tasks[task_count].state = 0;
task_count++;
return true;
}
void scheduler_run() {
uint32_t now = millis();
for (uint8_t i = 0; i < task_count; i++) {
if (now - tasks[i].last_run >= tasks[i].period) {
tasks[i].last_run = now;
tasks[i].update(); // 调用任务函数
}
}
}
3. 编写状态机任务示例
以 LED 闪烁和串口打印为例,展示状态机写法。
// 任务1:LED 闪烁(状态机)
#define LED_PIN 13
#define STATE_LED_ON 0
#define STATE_LED_OFF 1
void task_led_update() {
switch (tasks[0].state) { // 假设任务索引为0
case STATE_LED_ON:
digitalWrite(LED_PIN, HIGH);
tasks[0].state = STATE_LED_OFF;
break;
case STATE_LED_OFF:
digitalWrite(LED_PIN, LOW);
tasks[0].state = STATE_LED_ON;
break;
}
}
// 任务2:串口打印计数(状态机)
#define STATE_PRINT 0
void task_print_update() {
static int count = 0;
switch (tasks[1].state) {
case STATE_PRINT:
Serial.print("Count: ");
Serial.println(count++);
tasks[1].state = STATE_PRINT; // 保持状态
break;
}
}
4. 在 Arduino 中集成
void setup() {
Serial.begin(9600);
pinMode(LED_PIN, OUTPUT);
scheduler_init();
add_task(task_led_update, 500); // 每500ms切换LED
add_task(task_print_update, 1000); // 每1000ms打印
}
void loop() {
scheduler_run();
}
注意事项
-
任务函数必须非阻塞:避免使用
delay(),改用状态机或记录时间戳。 -
状态管理:每个任务的状态变量应独立,建议使用
task[i].state而非全局变量,以便调度器管理。 -
周期精度:
millis()基于定时器,若任务执行时间过长,可能影响其他任务,需确保每个任务执行时间远小于周期。 -
任务数量限制:
MAX_TASKS根据 RAM 调整,避免浪费。 - 可扩展性:可增加优先级或时间片,但保持简单是协作式调度的优势。
总结
通过状态机和协作式调度,我们在 Arduino 上实现了轻量级多任务,无需 RTOS 即可满足大部分场景。这种方法不仅节省资源,还让代码结构清晰,易于调试。掌握这一技巧,你将能更高效地开发复杂的嵌入式应用。