# 引言 在 Arduino 开发中,`delay()` 和 `loop()` 轮询是新手标配,但面对多传感器、多执行器或复杂协议时,阻塞式代码会导致响应迟钝、逻辑混乱。引入 RTOS(如 FreeRTOS)又受限于内存和复杂度。此时,**协作式状态机框架**提供了一种轻量级、零开销的解决方案:它利用状态机建模任务,通过非抢占式调度器循环执行,实现多任务“伪并行”。 # 核心原理 ## 1. 协作式调度 vs 抢占式调度 - **抢占式**:由时钟中断强制切换任务,需保存/恢复上下文,资源开销大。 - **协作式**:任务主动让出 CPU(如 `yield()`),调度器按顺序执行就绪任务。优点是实现简单、无竞态,缺点是任务必须快速返回,否则阻塞全局。 ## 2. 状态机模型 每个任务是一个有限状态机(FSM),包含: - **状态**:用枚举或常量表示,如 `IDLE`, `RUNNING`, `DONE`。 - **事件**:触发状态迁移的条件,如超时、传感器读数、外部中断。 - **动作**:在状态内执行的非阻塞操作,如读取数据、控制引脚。 状态机通过 `switch-case` 或函数指针实现,每次调度只执行当前状态的一小段逻辑,然后返回,避免长时间占用 CPU。 ## 3. 时间片管理 协作式调度器通常配合 `millis()` 或 `micros()` 实现时间片。每个任务记录上次执行时间,当 `now - lastTime >= interval` 时,才执行该任务的状态机。这样,即使任务未完成,也能按周期推进。 # 框架设计 我们构建一个轻量级任务管理器,支持注册任务、设置周期和状态机回调。 ## 1. 数据结构 ```c typedef struct { void (*init)(void); // 初始化函数 void (*run)(void); // 状态机主函数 uint32_t interval; // 执行周期 (ms) uint32_t lastRun; // 上次执行时间戳 uint8_t state; // 当前状态 bool enabled; // 是否启用 } Task_t; ``` ## 2. 调度器核心 ```c #define MAX_TASKS 5 Task_t tasks[MAX_TASKS]; uint8_t taskCount = 0; void scheduler_init() { taskCount = 0; } bool add_task(void (*init)(void), void (*run)(void), uint32_t interval) { if (taskCount >= MAX_TASKS) return false; tasks[taskCount].init = init; tasks[taskCount].run = run; tasks[taskCount].interval = interval; tasks[taskCount].lastRun = 0; tasks[taskCount].state = 0; tasks[taskCount].enabled = true; if (init) init(); taskCount++; return true; } void scheduler_run() { uint32_t now = millis(); for (uint8_t i = 0; i < taskCount; i++) { if (tasks[i].enabled && (now - tasks[i].lastRun >= tasks[i].interval)) { tasks[i].lastRun = now; tasks[i].run(); // 非阻塞执行 } } } ``` ## 3. 状态机宏定义 为了方便定义状态,我们使用宏: ```c #define STATE(task, s) ((task)->state = (s)) #define GET_STATE(task) ((task)->state) ``` # 完整示例:LED 闪烁 + 串口打印 我们创建两个任务:LED 每 500ms 翻转,串口每 1s 打印计数。 ## 1. 任务函数 ```c // 任务1:LED 闪烁 void led_init() { pinMode(13, OUTPUT); } void led_run() { static uint8_t ledState = 0; switch (GET_STATE(&tasks[0])) { case 0: // 点亮 digitalWrite(13, HIGH); STATE(&tasks[0], 1); break; case 1: // 熄灭 digitalWrite(13, LOW); STATE(&tasks[0], 0); break; } } // 任务2:串口打印 void serial_init() { Serial.begin(9600); } void serial_run() { static uint32_t counter = 0; switch (GET_STATE(&tasks[1])) { case 0: Serial.print("Count: "); Serial.println(counter++); STATE(&tasks[1], 1); break; case 1: // 等待下一个周期,无需操作 STATE(&tasks[1], 0); break; } } ``` ## 2. 主程序 ```c void setup() { scheduler_init(); add_task(led_init, led_run, 500); // 500ms 周期 add_task(serial_init, serial_run, 1000); // 1s 周期 } void loop() { scheduler_run(); // 可添加其他非阻塞代码 } ``` # 进阶:状态机与事件驱动 实际项目中,状态机往往需要响应外部事件(如按键、传感器中断)。我们可以通过全局标志或队列传递事件。 ```c volatile bool buttonPressed = false; void buttonISR() { buttonPressed = true; } void button_task_run() { if (buttonPressed) { buttonPressed = false; // 处理事件,切换状态 STATE(&tasks[2], 1); } } ``` 注意:ISR 中只置标志,不做耗时操作,避免破坏协作式调度。 # 注意事项 - **避免阻塞**:任务内严禁使用 `delay()` 或长循环,否则会阻塞其他任务。若需等待,应使用状态机拆分。 - **共享变量**:由于非抢占,任务间共享变量无需加锁,但需注意原子性(如 8 位变量安全,32 位可能需关中断)。 - **时间精度**:`millis()` 有 1ms 误差,若需高精度,使用 `micros()` 或定时器中断。 - **任务数量**:根据 RAM 限制,通常 5-10 个任务足够,过多会增加调度开销。 - **状态机设计**:每个状态应快速执行,若需等待外部条件,可设计为“轮询-返回”模式。 # 总结 协作式状态机框架在 Arduino 上实现了轻量级多任务调度,既避免了 RTOS 的复杂度,又解决了裸机轮询的阻塞问题。通过状态机建模,代码结构清晰、易扩展,特别适合中小型嵌入式项目。掌握此框架,你将能优雅地处理并发逻辑,提升开发效率。 # 参考资料 - Arduino 官方文档 `millis()` - 《嵌入式实时操作系统 μC/OS-III》 - 开源项目:TaskScheduler 库