引言

在 Arduino 开发中,我们常面临多任务需求:同时处理按键扫描、LED 闪烁、传感器读取和串口通信。若使用 delay() 阻塞,系统将失去实时性;若引入 RTOS,又可能超出硬件资源。协作式状态机(Cooperative State Machine)提供了一种折中方案:通过任务主动让出 CPU,实现非抢占式调度,无需操作系统,即可高效管理多个任务。

原理:协作式多任务与状态机

协作式多任务

协作式调度依赖任务主动让出控制权。每个任务在完成一个“步骤”后,必须返回主循环,让其他任务运行。这避免了抢占式调度带来的竞态条件,但要求任务不能长时间阻塞。

状态机

状态机将任务分解为有限个状态,每个状态对应一段代码,通过事件或条件触发状态转移。将状态机与协作式调度结合,每个任务就是一个状态机,在每次调度时执行当前状态的动作,然后转移到下一个状态。

模板设计

我们使用 C++ 模板实现一个通用的 StateMachine 类,通过函数指针或 std::function 存储状态处理函数,并维护一个状态变量。调度器则是一个简单的循环,依次调用每个任务的状态机实例。

配置步骤

  1. 定义状态枚举:为每个任务定义状态,如 IDLE, RUNNING, DONE
  2. 实现状态处理函数:每个状态对应一个函数,返回下一个状态。
  3. 创建状态机实例:使用模板实例化,传入初始状态和处理函数表。
  4. 注册任务:在 setup() 中创建任务对象,并加入调度器列表。
  5. 运行调度器:在 loop() 中调用调度器的 run() 方法。

完整代码示例

以下示例实现两个任务:LED 闪烁(每 500ms 翻转)和串口打印(每 1s 打印一次)。

#include <Arduino.h>
#include <functional>

// 状态机模板类
template<typename StateType>
class StateMachine {
public:
    using StateHandler = std::function<StateType()>;

    StateMachine(StateType initState, const StateHandler* handlers, int numStates)
        : currentState(initState), handlers(handlers), numStates(numStates) {}

    void run() {
        if (currentState >= 0 && currentState < numStates && handlers[currentState]) {
            currentState = handlers[currentState]();
        }
    }

private:
    StateType currentState;
    const StateHandler* handlers;
    int numStates;
};

// 任务1:LED 闪烁
enum LedState { LED_OFF, LED_ON };

unsigned long lastLedToggle = 0;
const int ledPin = 13;

LedState ledOffHandler() {
    if (millis() - lastLedToggle >= 500) {
        digitalWrite(ledPin, HIGH);
        lastLedToggle = millis();
        return LED_ON;
    }
    return LED_OFF;
}

LedState ledOnHandler() {
    if (millis() - lastLedToggle >= 500) {
        digitalWrite(ledPin, LOW);
        lastLedToggle = millis();
        return LED_OFF;
    }
    return LED_ON;
}

StateMachine<LedState>::StateHandler ledHandlers[] = { ledOffHandler, ledOnHandler };
StateMachine<LedState> ledSM(LED_OFF, ledHandlers, 2);

// 任务2:串口打印
enum PrintState { PRINT_IDLE, PRINT_WAIT };

unsigned long lastPrint = 0;

PrintState printIdleHandler() {
    if (millis() - lastPrint >= 1000) {
        Serial.println("Hello from state machine!");
        lastPrint = millis();
        return PRINT_WAIT;
    }
    return PRINT_IDLE;
}

PrintState printWaitHandler() {
    // 等待1秒后回到IDLE,但这里我们直接返回IDLE,因为millis检查在IDLE中
    return PRINT_IDLE;
}

StateMachine<PrintState>::StateHandler printHandlers[] = { printIdleHandler, printWaitHandler };
StateMachine<PrintState> printSM(PRINT_IDLE, printHandlers, 2);

// 调度器:简单数组管理
StateMachineBase* tasks[] = { &ledSM, &printSM }; // 注意:需要基类,这里简化

void setup() {
    pinMode(ledPin, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    ledSM.run();
    printSM.run();
}

注意:上述代码中 StateMachineBase 未定义,实际使用时需定义基类或使用模板函数。为简化,我们直接调用两个实例。

注意事项

  • 非阻塞原则:状态处理函数内严禁使用 delay() 或长时间循环,否则会阻塞其他任务。
  • 时间管理:使用 millis() 而非 delay() 实现定时,注意 millis() 溢出问题(约49天)。
  • 状态转移:确保每个状态都有明确的转移条件,避免死循环。
  • 内存开销std::function 可能占用较多内存,在 AVR 上建议使用普通函数指针。
  • 扩展性:任务数量增多时,可考虑使用链表或动态数组管理任务,但注意内存碎片。

总结

协作式状态机模板为 Arduino 提供了一种轻量级多任务方案,它结构清晰、易于调试,且不依赖外部库。通过合理设计状态,可以应对大多数非实时性任务。对于更复杂的实时需求,可考虑升级为抢占式调度或引入 RTOS。希望本文能帮助你提升嵌入式项目的架构水平。