引言:当 Arduino 遇见多任务
在嵌入式开发中,多任务处理常被等同于 RTOS。然而,对于 Arduino Uno 这类仅有 2KB RAM 的 MCU,引入 RTOS 往往得不偿失。协作式状态机(Cooperative State Machine)提供了一种轻量级替代方案:它通过任务主动让出 CPU 来实现并发,无需抢占式调度,内存开销极小。本文将带你从原理到实践,构建一个通用的状态机模板。
原理:协作式调度的核心思想
协作式多任务的核心是“非阻塞”与“主动让权”。每个任务(Task)被拆分为多个状态,每个状态执行一段短小的代码,然后返回。调度器(Scheduler)按顺序轮询所有任务,每个任务检查自身状态并执行相应动作。关键在于:任务内部绝不能使用 delay() 等阻塞函数,而是通过记录时间戳来判断是否该执行下一步。
状态机通常包含:
-
状态变量:表示当前所处状态(如
LED_ON、LED_OFF)。 - 时间戳:记录上次状态切换的时间,用于非阻塞延时。
- 事件触发:如按键按下、串口数据到达,可驱动状态转移。
模板设计:可复用的 C++ 类
我们设计一个 StateMachine 基类,提供 update() 虚函数供子类实现状态逻辑,并内置 delay() 非阻塞延时方法。
// StateMachine.h
#pragma once
class StateMachine {
public:
virtual void update() = 0; // 每帧调用
bool isReady() { return _ready; }
protected:
bool _ready = true;
unsigned long _lastTime = 0;
bool delay(unsigned long ms) {
if (millis() - _lastTime >= ms) {
_lastTime = millis();
return true;
}
return false;
}
};
实战:LED 闪烁与按键扫描任务
1. LED 闪烁任务
// LedBlink.h
#include "StateMachine.h"
class LedBlink : public StateMachine {
public:
LedBlink(int pin) : _pin(pin) { pinMode(_pin, OUTPUT); }
void update() override {
switch (_state) {
case 0: // 点亮
digitalWrite(_pin, HIGH);
_state = 1;
_lastTime = millis();
break;
case 1: // 等待 500ms
if (delay(500)) _state = 2;
break;
case 2: // 熄灭
digitalWrite(_pin, LOW);
_state = 3;
_lastTime = millis();
break;
case 3: // 等待 500ms
if (delay(500)) _state = 0;
break;
}
}
private:
int _pin;
int _state = 0;
};
2. 按键扫描任务(带消抖)
// ButtonScan.h
#include "StateMachine.h"
class ButtonScan : public StateMachine {
public:
ButtonScan(int pin) : _pin(pin) { pinMode(_pin, INPUT_PULLUP); }
void update() override {
switch (_state) {
case 0: // 等待按键按下
if (digitalRead(_pin) == LOW) {
_state = 1;
_lastTime = millis();
}
break;
case 1: // 消抖延时 20ms
if (delay(20)) {
if (digitalRead(_pin) == LOW) {
_state = 2;
} else {
_state = 0;
}
}
break;
case 2: // 确认按下,触发事件
onPress();
_state = 3;
_lastTime = millis();
break;
case 3: // 等待释放
if (digitalRead(_pin) == HIGH) {
_state = 0;
}
break;
}
}
void onPress() {
Serial.println("Button pressed!");
}
private:
int _pin;
int _state = 0;
};
3. 主程序:调度器
// main.ino
#include "LedBlink.h"
#include "ButtonScan.h"
LedBlink led(13);
ButtonScan btn(2);
void setup() {
Serial.begin(9600);
}
void loop() {
led.update();
btn.update();
}
深入:状态机的扩展与优化
-
事件队列:当任务需要与其他任务通信时,可添加一个简单的环形队列,在
update()中处理事件。 - 优先级:在调度器中,可给紧急任务分配更高的调用频率(如每 2 次循环调用一次)。
-
状态枚举:使用
enum代替魔法数字,提高可读性。
注意事项
-
避免阻塞:任何
delay()、while循环都会破坏协作调度,务必使用非阻塞延时。 -
时间戳溢出:
millis()约 49 天后回绕,但基于无符号长整型的减法在回绕时仍正确,无需处理。 - 任务数量:每个任务占用少量 RAM,但状态机数量过多时,注意栈空间(Arduino Uno 默认 2KB)。
-
调试:在状态切换处添加
Serial.print可帮助追踪逻辑,但调试代码会占用时间,正式发布时移除。
总结
协作式状态机模板为 Arduino 提供了一种轻量、高效的多任务方案。它避免了 RTOS 的复杂性和资源开销,同时保持了代码的模块化和可维护性。通过本文的模板,你可以轻松扩展多个任务,实现 LED 控制、按键扫描、传感器读取等并发操作。这种模式不仅适用于 Arduino,也适用于任何裸机嵌入式环境,是嵌入式开发者必备的技能之一。