引言:当 Arduino 遇见多任务

在嵌入式开发中,多任务处理常被等同于 RTOS。然而,对于 Arduino Uno 这类仅有 2KB RAM 的 MCU,引入 RTOS 往往得不偿失。协作式状态机(Cooperative State Machine)提供了一种轻量级替代方案:它通过任务主动让出 CPU 来实现并发,无需抢占式调度,内存开销极小。本文将带你从原理到实践,构建一个通用的状态机模板。

原理:协作式调度的核心思想

协作式多任务的核心是“非阻塞”与“主动让权”。每个任务(Task)被拆分为多个状态,每个状态执行一段短小的代码,然后返回。调度器(Scheduler)按顺序轮询所有任务,每个任务检查自身状态并执行相应动作。关键在于:任务内部绝不能使用 delay() 等阻塞函数,而是通过记录时间戳来判断是否该执行下一步。

状态机通常包含:

  • 状态变量:表示当前所处状态(如 LED_ONLED_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,也适用于任何裸机嵌入式环境,是嵌入式开发者必备的技能之一。