# 引言 在嵌入式开发中,多任务处理是常态。然而,对于 Arduino 这类资源紧张(如 ATmega328P 仅 2KB RAM)的 MCU,移植 RTOS(如 FreeRTOS)往往得不偿失。RTOS 需要额外的任务栈、内核对象,且调度开销大。更轻量、更可控的方案是:**非阻塞式状态机 + 协作式调度器**。它利用 CPU 的循环执行特性,通过时间片轮转和状态切换,实现“伪并发”,足以应对大多数传感器读取、按键扫描、LED 闪烁等场景。 # 核心原理:从阻塞到非阻塞 传统 Arduino 代码常使用 `delay()` 阻塞等待,导致 CPU 空转,无法处理其他任务。非阻塞式编程的核心是:**将任务分解为多个状态,每个状态只执行非阻塞操作,并通过时间戳(`millis()`)判断是否进入下一个状态**。 状态机模型包含: - **状态变量**:记录当前状态(如 `enum` 类型)。 - **状态转移条件**:基于时间、事件或外部输入。 - **状态动作**:每个状态下执行的代码,必须快速返回,不能阻塞。 调度器则是一个超级循环,依次调用每个任务的状态机函数,确保每个任务都有机会运行。 # 实现步骤 ## 1. 定义任务状态机结构 我们以两个典型任务为例:LED 闪烁(周期 500ms)和串口打印(周期 1s)。 ```c // 任务状态枚举 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. 实现任务函数(非阻塞) ```c // 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. 构建调度器 ```c // 任务列表 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. 扩展:支持时间片轮转(可选) 若任务需要更精细的优先级或时间片,可引入简单的时间片计数: ```c uint32_t tick = 0; void loop() { tick++; if (tick % 10 == 0) ledTask(); // 每 10ms 执行一次 if (tick % 20 == 0) printTask(); // 每 20ms 执行一次 // 注意:tick 溢出需处理,但通常足够 } ``` # 完整示例:多任务状态机 以下是一个综合示例,包含按键消抖(状态机)和 LED 呼吸灯效果(非阻塞 PWM 模拟)。 ```c // 按键消抖状态机 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` 或关中断保护,避免数据竞争。 - **状态机可读性**:使用 `enum` 和 `switch-case` 清晰表达状态,避免魔法数字。 - **调试技巧**:在关键状态切换处添加串口打印,但注意打印本身耗时,可加调试开关。 - **扩展性**:若任务数量增多,可改用数组+函数指针,或引入优先级调度(如简单的时间片轮转)。 # 总结 非阻塞式状态机与协作式调度器是嵌入式开发的基石技能。它无需额外资源,代码透明可控,非常适合 Arduino 等小型平台。通过将任务分解为状态,并利用时间戳驱动,我们实现了多任务并发,同时保持了代码的可维护性和实时性。当项目复杂度上升时,可平滑过渡到更高级的调度算法,但掌握这种轻量级技巧,将让你在资源受限的环境中游刃有余。