引言

Arduino凭借其简单易用广受欢迎,但许多开发者习惯在loop()中使用delay()实现延时,这会导致CPU空转、外设响应迟钝,尤其在多任务场景(如同时处理按键、LED闪烁、传感器读取)中捉襟见肘。本文提出一种基于状态机的非阻塞调度框架,通过时间片轮转和状态迁移,实现轻量级多任务并发,适用于资源受限的嵌入式环境。

非阻塞编程基础

阻塞 vs 非阻塞

  • 阻塞delay(1000)暂停整个程序,期间无法处理其他事件。
  • 非阻塞:利用millis()记录时间戳,通过比较时间差判断是否执行任务,CPU在等待期间可处理其他任务。

核心原则

  • 绝不使用delay(),改用时间戳检查。
  • 将任务拆分为多个状态,每个状态执行非阻塞操作。
  • 使用状态机管理任务生命周期。

状态机调度框架设计

架构概览

框架包含三个核心组件:

  • 任务控制块(TCB):描述任务属性(周期、状态、回调函数)。
  • 调度器:管理任务注册、轮询和状态切换。
  • 事件标志:支持外部事件触发任务。

数据结构定义

// 任务状态枚举
typedef enum {
    TASK_READY,     // 就绪,等待执行
    TASK_RUNNING,   // 运行中
    TASK_WAITING,   // 等待事件或延时
    TASK_SUSPENDED  // 挂起
} TaskState;

// 任务控制块
typedef struct {
    void (*func)(void);      // 任务函数指针
    uint32_t period;         // 执行周期(ms)
    uint32_t lastRun;        // 上次执行时间戳
    TaskState state;         // 当前状态
    uint8_t priority;        // 优先级(0-255,数值大优先)
    void *arg;               // 可选参数
} TaskCB;

调度器实现

#define MAX_TASKS 8

static TaskCB taskList[MAX_TASKS];
static uint8_t taskCount = 0;

// 注册任务
int scheduler_add_task(void (*func)(void), uint32_t period, uint8_t priority) {
    if (taskCount >= MAX_TASKS) return -1;
    taskList[taskCount].func = func;
    taskList[taskCount].period = period;
    taskList[taskCount].lastRun = 0;
    taskList[taskCount].state = TASK_READY;
    taskList[taskCount].priority = priority;
    taskList[taskCount].arg = NULL;
    return taskCount++;
}

// 调度主循环(在loop()中调用)
void scheduler_run() {
    uint32_t now = millis();
    for (uint8_t i = 0; i < taskCount; i++) {
        if (taskList[i].state == TASK_READY && 
            (now - taskList[i].lastRun >= taskList[i].period)) {
            taskList[i].lastRun = now;
            taskList[i].state = TASK_RUNNING;
            taskList[i].func();  // 执行任务
            taskList[i].state = TASK_READY;
        }
    }
}

状态机任务示例

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

// LED任务状态
typedef enum {
    LED_OFF,
    LED_ON,
    LED_WAIT
} LedState;

static LedState ledState = LED_OFF;
static uint32_t ledTimer = 0;

void led_task() {
    switch (ledState) {
        case LED_OFF:
            digitalWrite(LED_BUILTIN, LOW);
            ledTimer = millis();
            ledState = LED_WAIT;
            break;
        case LED_ON:
            digitalWrite(LED_BUILTIN, HIGH);
            ledTimer = millis();
            ledState = LED_WAIT;
            break;
        case LED_WAIT:
            if (millis() - ledTimer >= 500) {
                // 切换状态
                ledState = (digitalRead(LED_BUILTIN) == HIGH) ? LED_OFF : LED_ON;
            }
            break;
    }
}

事件驱动扩展

事件标志机制

通过位掩码实现事件触发,任务可等待特定事件:

volatile uint32_t eventFlags = 0;
#define EVENT_BUTTON (1 << 0)
#define EVENT_TIMER  (1 << 1)

// 设置事件
void event_set(uint32_t flag) {
    eventFlags |= flag;
}

// 在任务中检查事件
void button_task() {
    if (eventFlags & EVENT_BUTTON) {
        eventFlags &= ~EVENT_BUTTON;
        // 处理按键事件
    }
}

中断与调度器交互

在ISR中仅设置事件标志,避免耗时操作:

void buttonISR() {
    event_set(EVENT_BUTTON);
}

完整示例:多任务系统

需求

  • 任务1:LED以1Hz闪烁
  • 任务2:每2秒读取传感器并打印
  • 任务3:按键按下时响应(事件触发)

代码实现

#include <Arduino.h>

// 框架代码(如上)...

// 任务函数声明
void led_task();
void sensor_task();
void button_task();

void setup() {
    Serial.begin(9600);
    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(2, INPUT_PULLUP);
    attachInterrupt(digitalPinToInterrupt(2), buttonISR, FALLING);
    
    scheduler_add_task(led_task, 500, 1);      // 周期500ms
    scheduler_add_task(sensor_task, 2000, 2);  // 周期2s
    scheduler_add_task(button_task, 0, 3);     // 事件触发,周期0表示不按周期
}

void loop() {
    scheduler_run();
}

// 任务实现...

注意事项与优化

  • 任务函数应快速返回:避免在任务内进行长时间阻塞操作,如delaySerial.print大量数据。
  • 共享数据保护:多任务间共享变量需使用volatile或临界区保护。
  • 优先级调度:当前框架简单轮询,未实现抢占,高优先级任务可能被低优先级任务延迟,可增加优先级排序。
  • 内存占用:每个TCB约20字节,8个任务仅160字节,适合Arduino UNO。
  • 时间基准millis()在50天后溢出,需处理溢出情况(使用无符号减法可自动处理)。
  • 调试技巧:在任务中打印状态时,注意串口缓冲区阻塞,可改用非阻塞打印或环形缓冲区。

总结

本文设计的基于状态机的非阻塞调度框架,以极小的资源开销实现了多任务并发,避免了delay()带来的响应迟钝问题。通过任务注册、周期调度和事件驱动,开发者可以轻松构建复杂的嵌入式应用。该框架可进一步扩展为协作式调度器,甚至移植到其他MCU平台。希望本文能帮助你在嵌入式开发中更上一层楼。