引言

在嵌入式开发中,当需要同时处理多个任务(如按键扫描、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打下坚实基础。