引言

在嵌入式开发中,多任务处理是常态。然而,对于 Arduino 这类资源紧张(如 ATmega328P 仅 2KB RAM)的 MCU,移植 RTOS(如 FreeRTOS)往往得不偿失。RTOS 需要额外的任务栈、内核对象,且调度开销大。更轻量、更可控的方案是:非阻塞式状态机 + 协作式调度器。它利用 CPU 的循环执行特性,通过时间片轮转和状态切换,实现“伪并发”,足以应对大多数传感器读取、按键扫描、LED 闪烁等场景。

核心原理:从阻塞到非阻塞

传统 Arduino 代码常使用 delay() 阻塞等待,导致 CPU 空转,无法处理其他任务。非阻塞式编程的核心是:将任务分解为多个状态,每个状态只执行非阻塞操作,并通过时间戳(millis())判断是否进入下一个状态

状态机模型包含:

  • 状态变量:记录当前状态(如 enum 类型)。
  • 状态转移条件:基于时间、事件或外部输入。
  • 状态动作:每个状态下执行的代码,必须快速返回,不能阻塞。

调度器则是一个超级循环,依次调用每个任务的状态机函数,确保每个任务都有机会运行。

实现步骤

1. 定义任务状态机结构

我们以两个典型任务为例:LED 闪烁(周期 500ms)和串口打印(周期 1s)。

// 任务状态枚举
typedef enum {
    LED_OFF,
    LED_ON
} LedState;

typedef enum {
    PRINT_IDLE,
    PRINT_SEND
} PrintState;

// 任务控制块(TCB)
typedef struct {
    uint32_t lastTick;      // 上次更新时间
    uint32_t interval;      // 任务周期
    uint8_t state;          // 当前状态
    void (*taskFunc)(void); // 任务函数指针
} Task_t;

2. 实现任务函数(非阻塞)

// LED 闪烁任务
void ledTask() {
    static LedState state = LED_OFF;
    static uint32_t lastTick = 0;
    uint32_t now = millis();
    
    switch (state) {
        case LED_OFF:
            if (now - lastTick >= 500) {
                digitalWrite(LED_BUILTIN, HIGH);
                state = LED_ON;
                lastTick = now;
            }
            break;
        case LED_ON:
            if (now - lastTick >= 500) {
                digitalWrite(LED_BUILTIN, LOW);
                state = LED_OFF;
                lastTick = now;
            }
            break;
    }
}

// 串口打印任务
void printTask() {
    static uint32_t lastPrint = 0;
    uint32_t now = millis();
    if (now - lastPrint >= 1000) {
        Serial.println("Hello from task!");
        lastPrint = now;
    }
}

3. 构建调度器

// 任务列表
Task_t tasks[] = {
    {0, 0, 0, ledTask},
    {0, 0, 0, printTask}
};
#define NUM_TASKS (sizeof(tasks) / sizeof(tasks[0]))

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

void loop() {
    // 协作式调度:依次执行每个任务
    for (uint8_t i = 0; i < NUM_TASKS; i++) {
        tasks[i].taskFunc();
    }
    // 可选:执行其他非任务代码
}

4. 扩展:支持时间片轮转(可选)

若任务需要更精细的优先级或时间片,可引入简单的时间片计数:

uint32_t tick = 0;
void loop() {
    tick++;
    if (tick % 10 == 0) ledTask();      // 每 10ms 执行一次
    if (tick % 20 == 0) printTask();    // 每 20ms 执行一次
    // 注意:tick 溢出需处理,但通常足够
}

完整示例:多任务状态机

以下是一个综合示例,包含按键消抖(状态机)和 LED 呼吸灯效果(非阻塞 PWM 模拟)。

// 按键消抖状态机
enum {BTN_IDLE, BTN_DEBOUNCE, BTN_PRESSED} btnState;
uint32_t btnLastDebounce = 0;
const int btnPin = 2;

void buttonTask() {
    static int lastReading = HIGH;
    int reading = digitalRead(btnPin);
    uint32_t now = millis();
    
    switch (btnState) {
        case BTN_IDLE:
            if (reading != lastReading) {
                btnState = BTN_DEBOUNCE;
                btnLastDebounce = now;
            }
            break;
        case BTN_DEBOUNCE:
            if (now - btnLastDebounce > 20) {
                if (reading == LOW) {
                    btnState = BTN_PRESSED;
                    Serial.println("Button pressed!");
                } else {
                    btnState = BTN_IDLE;
                }
            }
            break;
        case BTN_PRESSED:
            if (reading == HIGH) {
                btnState = BTN_IDLE;
            }
            break;
    }
    lastReading = reading;
}

// 呼吸灯任务(非阻塞)
void breatheTask() {
    static uint8_t brightness = 0;
    static int8_t step = 1;
    static uint32_t lastUpdate = 0;
    uint32_t now = millis();
    
    if (now - lastUpdate >= 10) {
        brightness += step;
        if (brightness >= 255 || brightness == 0) step = -step;
        analogWrite(9, brightness);
        lastUpdate = now;
    }
}

void setup() {
    pinMode(btnPin, INPUT_PULLUP);
    pinMode(9, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    buttonTask();
    breatheTask();
}

注意事项

  • 避免阻塞操作:状态机内严禁使用 delay()while 等待硬件等,否则会阻塞整个调度器。
  • 时间基准:使用 millis() 而非 micros() 以节省资源,注意 millis() 溢出(约 49 天),需用无符号减法处理(如 now - lastTick 自动处理)。
  • 任务划分粒度:任务应尽量短小,每次执行时间 < 1ms,否则影响其他任务响应。
  • 共享资源保护:若任务间共享变量,需用 volatile 或关中断保护,避免数据竞争。
  • 状态机可读性:使用 enumswitch-case 清晰表达状态,避免魔法数字。
  • 调试技巧:在关键状态切换处添加串口打印,但注意打印本身耗时,可加调试开关。
  • 扩展性:若任务数量增多,可改用数组+函数指针,或引入优先级调度(如简单的时间片轮转)。

总结

非阻塞式状态机与协作式调度器是嵌入式开发的基石技能。它无需额外资源,代码透明可控,非常适合 Arduino 等小型平台。通过将任务分解为状态,并利用时间戳驱动,我们实现了多任务并发,同时保持了代码的可维护性和实时性。当项目复杂度上升时,可平滑过渡到更高级的调度算法,但掌握这种轻量级技巧,将让你在资源受限的环境中游刃有余。