引言

Arduino因其简单易用而广受欢迎,但许多开发者在使用过程中会遇到一个痛点:delay()函数会阻塞整个程序,导致无法同时处理多个任务。例如,在LED闪烁的同时读取传感器或响应按键,传统写法往往顾此失彼。本文介绍一种基于状态机的非阻塞式多任务调度框架,让Arduino能够高效地并发执行多个任务,且无需引入RTOS,代码轻量、可移植性强。

原理讲解

为什么需要非阻塞?

delay()本质是忙等待,CPU在延时期间空转,无法执行其他代码。在嵌入式系统中,CPU资源宝贵,阻塞意味着浪费。非阻塞编程的核心是:将任务分解为多个小步骤,每个步骤在短时间内完成,然后立即返回主循环,让出CPU给其他任务

状态机思想

状态机(State Machine)是一种数学模型,由状态、事件和转移组成。在任务调度中,每个任务可视为一个状态机:

  • 状态(State):任务当前所处的阶段,如“等待”、“执行”、“完成”。
  • 事件(Event):触发状态转移的条件,如“时间到”、“数据就绪”。
  • 转移(Transition):从一个状态到另一个状态的动作。

通过状态机,任务不再需要连续占用CPU,而是在每个状态中执行少量操作,然后根据条件跳转到下一个状态。

时间片轮转

结合millis()函数,我们可以实现基于时间的调度。每个任务拥有自己的时间戳,主循环不断检查是否到达该任务的执行时间,若到达则执行一次状态机推进。这样,多个任务在宏观上“同时”运行,微观上分时复用CPU。

框架设计

任务结构体

我们定义一个任务结构体,包含状态、时间戳、间隔和函数指针:

typedef struct {
    uint8_t state;          // 当前状态
    unsigned long lastTime; // 上次执行时间
    unsigned long interval; // 执行间隔(ms)
    void (*taskFunc)(void); // 任务函数指针
} Task;

调度器核心

调度器维护一个任务数组,在loop()中遍历所有任务,检查时间是否到期,若到期则调用对应函数。

#define MAX_TASKS 5
Task tasks[MAX_TASKS];
uint8_t taskCount = 0;

void addTask(void (*func)(void), unsigned long interval) {
    if (taskCount < MAX_TASKS) {
        tasks[taskCount].state = 0;
        tasks[taskCount].lastTime = 0;
        tasks[taskCount].interval = interval;
        tasks[taskCount].taskFunc = func;
        taskCount++;
    }
}

void scheduler() {
    unsigned long now = millis();
    for (uint8_t i = 0; i < taskCount; i++) {
        if (now - tasks[i].lastTime >= tasks[i].interval) {
            tasks[i].lastTime = now;
            tasks[i].taskFunc();
        }
    }
}

任务实现示例

以LED闪烁为例,使用状态机实现非阻塞:

// LED任务状态
#define LED_OFF 0
#define LED_ON  1

void ledTask() {
    static uint8_t state = LED_OFF;
    switch (state) {
        case LED_OFF:
            digitalWrite(13, HIGH);
            state = LED_ON;
            break;
        case LED_ON:
            digitalWrite(13, LOW);
            state = LED_OFF;
            break;
    }
}

注意,这里没有使用delay,而是通过调度器控制执行间隔。

完整代码示例

下面是一个完整示例,实现两个任务:LED闪烁(间隔500ms)和串口打印(间隔1s)。

#include <Arduino.h>

// 任务结构体定义
typedef struct {
    uint8_t state;
    unsigned long lastTime;
    unsigned long interval;
    void (*taskFunc)(void);
} Task;

#define MAX_TASKS 5
Task tasks[MAX_TASKS];
uint8_t taskCount = 0;

void addTask(void (*func)(void), unsigned long interval) {
    if (taskCount < MAX_TASKS) {
        tasks[taskCount].state = 0;
        tasks[taskCount].lastTime = 0;
        tasks[taskCount].interval = interval;
        tasks[taskCount].taskFunc = func;
        taskCount++;
    }
}

void scheduler() {
    unsigned long now = millis();
    for (uint8_t i = 0; i < taskCount; i++) {
        if (now - tasks[i].lastTime >= tasks[i].interval) {
            tasks[i].lastTime = now;
            tasks[i].taskFunc();
        }
    }
}

// 任务1:LED闪烁
void ledTask() {
    static uint8_t state = 0;
    switch (state) {
        case 0:
            digitalWrite(13, HIGH);
            state = 1;
            break;
        case 1:
            digitalWrite(13, LOW);
            state = 0;
            break;
    }
}

// 任务2:串口打印
void printTask() {
    static int count = 0;
    Serial.print("Count: ");
    Serial.println(count++);
}

void setup() {
    pinMode(13, OUTPUT);
    Serial.begin(9600);
    addTask(ledTask, 500);   // 每500ms执行一次
    addTask(printTask, 1000); // 每1000ms执行一次
}

void loop() {
    scheduler();
}

注意事项

  • 避免阻塞操作:任务函数内不要使用delay()或长时间循环,否则会阻塞调度器。若需等待外部事件,应使用状态机等待。
  • 时间戳溢出millis()约49天后溢出,但使用unsigned long减法(如now - lastTime)可正确处理溢出,无需额外处理。
  • 任务数量限制MAX_TASKS根据内存调整,Arduino Uno仅有2KB RAM,任务过多会耗尽内存。
  • 状态变量作用域:任务函数内的static变量保留状态,但若任务被删除或重置,需手动复位。
  • 优先级问题:此框架为协作式调度,任务按顺序执行,若某任务执行时间过长,会影响后续任务。因此任务应尽量短小。

扩展与优化

  • 动态间隔:可在任务函数中修改tasks[i].interval,实现可变周期。
  • 任务暂停/恢复:增加active标志,调度时跳过非活动任务。
  • 事件驱动:结合中断设置标志位,任务在轮询时检查标志,实现事件触发。

结语

本文提出的基于状态机的轻量级调度框架,让Arduino能够轻松处理多任务,且代码简洁、易于维护。它适用于大多数非实时嵌入式场景,如传感器采集、LED控制、按键扫描等。希望读者能掌握这一思想,并将其应用到实际项目中,提升开发效率与系统性能。