引言

嵌入式开发中,多任务需求无处不在:同时处理按键扫描、LED 闪烁、传感器读取和串口通信。然而,Arduino Uno 仅有 2KB RAM,运行 RTOS 往往力不从心。协作式状态机(Cooperative State Machine)提供了一种优雅的解决方案:通过非阻塞延时和状态切换,在单线程中模拟并发,实现高效、可预测的多任务调度。

核心原理

协作式 vs 抢占式

  • 抢占式(RTOS):由内核强制切换任务,需要上下文保存、优先级管理等,开销大。
  • 协作式:任务主动让出 CPU(通过 delay()yield()),代码简单,内存占用极低。

状态机模型

每个任务是一个有限状态机(FSM),包含:

  • 状态:用枚举或常量表示当前阶段。
  • 事件:如时间到达、外部信号。
  • 转移:根据事件切换状态。

核心思想:在 loop() 中轮询所有任务,每个任务检查自身状态和条件,若满足则执行动作并更新状态,否则立即返回。

非阻塞延时

使用 millis() 代替 delay(),记录上次执行时间,通过时间差判断是否到达执行时刻。

unsigned long previousMillis = 0;
const long interval = 1000;

void task() {
    unsigned long currentMillis = millis();
    if (currentMillis - previousMillis >= interval) {
        previousMillis = currentMillis;
        // 执行任务
    }
}

框架设计

任务结构体

定义统一的任务接口,便于管理。

typedef struct {
    void (*init)(void);      // 初始化函数
    void (*update)(void);    // 状态更新函数
    unsigned long period;    // 执行周期(ms)
    unsigned long lastRun;   // 上次执行时间
} Task;

调度器

维护任务数组,在 loop() 中遍历并执行到期任务。

#define MAX_TASKS 5
Task tasks[MAX_TASKS];
int taskCount = 0;

void scheduler_init() {
    taskCount = 0;
}

bool scheduler_add(Task t) {
    if (taskCount >= MAX_TASKS) return false;
    t.lastRun = 0;
    tasks[taskCount++] = t;
    return true;
}

void scheduler_run() {
    unsigned long now = millis();
    for (int i = 0; i < taskCount; i++) {
        if (now - tasks[i].lastRun >= tasks[i].period) {
            tasks[i].lastRun = now;
            tasks[i].update();
        }
    }
}

状态机实现示例

以 LED 闪烁和按键扫描为例,展示如何将任务转化为状态机。

// LED 闪烁任务
enum LEDState { LED_OFF, LED_ON };
LEDState ledState = LED_OFF;
unsigned long ledTimer = 0;

void led_init() {
    pinMode(13, OUTPUT);
}

void led_update() {
    unsigned long now = millis();
    switch (ledState) {
        case LED_OFF:
            if (now - ledTimer >= 500) {
                digitalWrite(13, HIGH);
                ledState = LED_ON;
                ledTimer = now;
            }
            break;
        case LED_ON:
            if (now - ledTimer >= 300) {
                digitalWrite(13, LOW);
                ledState = LED_OFF;
                ledTimer = now;
            }
            break;
    }
}

// 按键扫描任务
enum KeyState { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED };
KeyState keyState = KEY_IDLE;
unsigned long keyTimer = 0;
const int keyPin = 2;

void key_init() {
    pinMode(keyPin, INPUT_PULLUP);
}

void key_update() {
    unsigned long now = millis();
    bool pressed = (digitalRead(keyPin) == LOW);
    switch (keyState) {
        case KEY_IDLE:
            if (pressed) {
                keyState = KEY_DEBOUNCE;
                keyTimer = now;
            }
            break;
        case KEY_DEBOUNCE:
            if (now - keyTimer >= 20) {
                if (pressed) {
                    keyState = KEY_PRESSED;
                    Serial.println("Key pressed!");
                } else {
                    keyState = KEY_IDLE;
                }
            }
            break;
        case KEY_PRESSED:
            if (!pressed) {
                keyState = KEY_IDLE;
            }
            break;
    }
}

配置步骤

  1. 定义任务:为每个任务编写 initupdate 函数,内部使用状态机。
  2. 创建任务结构体:设置周期(如 LED 任务 10ms,按键 5ms)。
  3. 初始化调度器:在 setup() 中调用 scheduler_init()
  4. 注册任务:使用 scheduler_add() 添加任务。
  5. 运行调度器:在 loop() 中调用 scheduler_run()
void setup() {
    Serial.begin(9600);
    scheduler_init();
    
    Task ledTask = { led_init, led_update, 10, 0 };
    Task keyTask = { key_init, key_update, 5, 0 };
    scheduler_add(ledTask);
    scheduler_add(keyTask);
    
    // 初始化所有任务
    for (int i = 0; i < taskCount; i++) {
        tasks[i].init();
    }
}

void loop() {
    scheduler_run();
}

完整代码示例

将上述片段整合,形成可直接运行的完整程序。

// 协作式状态机框架示例
#include <Arduino.h>

// 任务结构体定义...(如前)

// LED 任务实现...(如前)

// 按键任务实现...(如前)

void setup() {
    Serial.begin(9600);
    scheduler_init();
    
    Task ledTask = { led_init, led_update, 10, 0 };
    Task keyTask = { key_init, key_update, 5, 0 };
    scheduler_add(ledTask);
    scheduler_add(keyTask);
    
    for (int i = 0; i < taskCount; i++) {
        tasks[i].init();
    }
}

void loop() {
    scheduler_run();
}

注意事项

  • 避免阻塞:任务内严禁使用 delay() 或长循环,否则会阻塞其他任务。
  • 时间精度millis() 在 50 天后溢出,需使用无符号长整型差值比较(如 (now - lastRun) >= period),可自动处理溢出。
  • 任务周期:周期不宜过短,否则调度器频繁轮询,浪费 CPU;建议最小 1ms。
  • 共享资源:多任务访问全局变量时,需考虑原子性,必要时使用 noInterrupts() 保护。
  • 状态机复杂度:状态过多时,可考虑使用函数指针表优化,但保持简单优先。
  • 调试:利用串口打印状态切换,便于定位逻辑错误。

总结

协作式状态机框架在 Arduino 上实现了轻量级多任务调度,无需 RTOS 即可满足大多数应用需求。通过非阻塞延时和状态切换,我们获得了确定性的时序和极低的内存开销。此框架可轻松扩展至更多任务,并适用于其他单片机平台。掌握这一技术,你将能更高效地构建复杂嵌入式系统。