引言
在 Arduino 开发中,我们常面临多任务需求:同时处理按键扫描、LED 闪烁、传感器读取和串口通信。若使用 delay() 阻塞,系统将失去实时性;若引入 RTOS,又可能超出硬件资源。协作式状态机(Cooperative State Machine)提供了一种折中方案:通过任务主动让出 CPU,实现非抢占式调度,无需操作系统,即可高效管理多个任务。
原理:协作式多任务与状态机
协作式多任务
协作式调度依赖任务主动让出控制权。每个任务在完成一个“步骤”后,必须返回主循环,让其他任务运行。这避免了抢占式调度带来的竞态条件,但要求任务不能长时间阻塞。
状态机
状态机将任务分解为有限个状态,每个状态对应一段代码,通过事件或条件触发状态转移。将状态机与协作式调度结合,每个任务就是一个状态机,在每次调度时执行当前状态的动作,然后转移到下一个状态。
模板设计
我们使用 C++ 模板实现一个通用的 StateMachine 类,通过函数指针或 std::function 存储状态处理函数,并维护一个状态变量。调度器则是一个简单的循环,依次调用每个任务的状态机实例。
配置步骤
-
定义状态枚举:为每个任务定义状态,如
IDLE,RUNNING,DONE。 - 实现状态处理函数:每个状态对应一个函数,返回下一个状态。
- 创建状态机实例:使用模板实例化,传入初始状态和处理函数表。
-
注册任务:在
setup()中创建任务对象,并加入调度器列表。 -
运行调度器:在
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。希望本文能帮助你提升嵌入式项目的架构水平。