引言

在嵌入式开发中,多任务处理是常见需求。然而,对于 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 即可满足大部分场景。这种方法不仅节省资源,还让代码结构清晰,易于调试。掌握这一技巧,你将能更高效地开发复杂的嵌入式应用。