# 引言 嵌入式开发中,多任务需求无处不在:同时处理按键扫描、LED 闪烁、传感器读取和串口通信。然而,Arduino Uno 仅有 2KB RAM,运行 RTOS 往往力不从心。协作式状态机(Cooperative State Machine)提供了一种优雅的解决方案:通过非阻塞延时和状态切换,在单线程中模拟并发,实现高效、可预测的多任务调度。 # 核心原理 ## 协作式 vs 抢占式 - **抢占式(RTOS)**:由内核强制切换任务,需要上下文保存、优先级管理等,开销大。 - **协作式**:任务主动让出 CPU(通过 `delay()` 或 `yield()`),代码简单,内存占用极低。 ## 状态机模型 每个任务是一个有限状态机(FSM),包含: - **状态**:用枚举或常量表示当前阶段。 - **事件**:如时间到达、外部信号。 - **转移**:根据事件切换状态。 核心思想:在 `loop()` 中轮询所有任务,每个任务检查自身状态和条件,若满足则执行动作并更新状态,否则立即返回。 ## 非阻塞延时 使用 `millis()` 代替 `delay()`,记录上次执行时间,通过时间差判断是否到达执行时刻。 ```c unsigned long previousMillis = 0; const long interval = 1000; void task() { unsigned long currentMillis = millis(); if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // 执行任务 } } ``` # 框架设计 ## 任务结构体 定义统一的任务接口,便于管理。 ```c typedef struct { void (*init)(void); // 初始化函数 void (*update)(void); // 状态更新函数 unsigned long period; // 执行周期(ms) unsigned long lastRun; // 上次执行时间 } Task; ``` ## 调度器 维护任务数组,在 `loop()` 中遍历并执行到期任务。 ```c #define MAX_TASKS 5 Task tasks[MAX_TASKS]; int taskCount = 0; void scheduler_init() { taskCount = 0; } bool scheduler_add(Task t) { if (taskCount >= MAX_TASKS) return false; t.lastRun = 0; tasks[taskCount++] = t; return true; } void scheduler_run() { unsigned long now = millis(); for (int i = 0; i < taskCount; i++) { if (now - tasks[i].lastRun >= tasks[i].period) { tasks[i].lastRun = now; tasks[i].update(); } } } ``` ## 状态机实现示例 以 LED 闪烁和按键扫描为例,展示如何将任务转化为状态机。 ```c // LED 闪烁任务 enum LEDState { LED_OFF, LED_ON }; LEDState ledState = LED_OFF; unsigned long ledTimer = 0; void led_init() { pinMode(13, OUTPUT); } void led_update() { unsigned long now = millis(); switch (ledState) { case LED_OFF: if (now - ledTimer >= 500) { digitalWrite(13, HIGH); ledState = LED_ON; ledTimer = now; } break; case LED_ON: if (now - ledTimer >= 300) { digitalWrite(13, LOW); ledState = LED_OFF; ledTimer = now; } break; } } // 按键扫描任务 enum KeyState { KEY_IDLE, KEY_DEBOUNCE, KEY_PRESSED }; KeyState keyState = KEY_IDLE; unsigned long keyTimer = 0; const int keyPin = 2; void key_init() { pinMode(keyPin, INPUT_PULLUP); } void key_update() { unsigned long now = millis(); bool pressed = (digitalRead(keyPin) == LOW); switch (keyState) { case KEY_IDLE: if (pressed) { keyState = KEY_DEBOUNCE; keyTimer = now; } break; case KEY_DEBOUNCE: if (now - keyTimer >= 20) { if (pressed) { keyState = KEY_PRESSED; Serial.println("Key pressed!"); } else { keyState = KEY_IDLE; } } break; case KEY_PRESSED: if (!pressed) { keyState = KEY_IDLE; } break; } } ``` # 配置步骤 1. **定义任务**:为每个任务编写 `init` 和 `update` 函数,内部使用状态机。 2. **创建任务结构体**:设置周期(如 LED 任务 10ms,按键 5ms)。 3. **初始化调度器**:在 `setup()` 中调用 `scheduler_init()`。 4. **注册任务**:使用 `scheduler_add()` 添加任务。 5. **运行调度器**:在 `loop()` 中调用 `scheduler_run()`。 ```c void setup() { Serial.begin(9600); scheduler_init(); Task ledTask = { led_init, led_update, 10, 0 }; Task keyTask = { key_init, key_update, 5, 0 }; scheduler_add(ledTask); scheduler_add(keyTask); // 初始化所有任务 for (int i = 0; i < taskCount; i++) { tasks[i].init(); } } void loop() { scheduler_run(); } ``` # 完整代码示例 将上述片段整合,形成可直接运行的完整程序。 ```c // 协作式状态机框架示例 #include // 任务结构体定义...(如前) // LED 任务实现...(如前) // 按键任务实现...(如前) void setup() { Serial.begin(9600); scheduler_init(); Task ledTask = { led_init, led_update, 10, 0 }; Task keyTask = { key_init, key_update, 5, 0 }; scheduler_add(ledTask); scheduler_add(keyTask); for (int i = 0; i < taskCount; i++) { tasks[i].init(); } } void loop() { scheduler_run(); } ``` # 注意事项 - **避免阻塞**:任务内严禁使用 `delay()` 或长循环,否则会阻塞其他任务。 - **时间精度**:`millis()` 在 50 天后溢出,需使用无符号长整型差值比较(如 `(now - lastRun) >= period`),可自动处理溢出。 - **任务周期**:周期不宜过短,否则调度器频繁轮询,浪费 CPU;建议最小 1ms。 - **共享资源**:多任务访问全局变量时,需考虑原子性,必要时使用 `noInterrupts()` 保护。 - **状态机复杂度**:状态过多时,可考虑使用函数指针表优化,但保持简单优先。 - **调试**:利用串口打印状态切换,便于定位逻辑错误。 # 总结 协作式状态机框架在 Arduino 上实现了轻量级多任务调度,无需 RTOS 即可满足大多数应用需求。通过非阻塞延时和状态切换,我们获得了确定性的时序和极低的内存开销。此框架可轻松扩展至更多任务,并适用于其他单片机平台。掌握这一技术,你将能更高效地构建复杂嵌入式系统。