# 引言 Arduino因其简单易用而广受欢迎,但许多开发者在使用过程中会遇到一个痛点:`delay()`函数会阻塞整个程序,导致无法同时处理多个任务。例如,在LED闪烁的同时读取传感器或响应按键,传统写法往往顾此失彼。本文介绍一种基于状态机的非阻塞式多任务调度框架,让Arduino能够高效地并发执行多个任务,且无需引入RTOS,代码轻量、可移植性强。 # 原理讲解 ## 为什么需要非阻塞? `delay()`本质是忙等待,CPU在延时期间空转,无法执行其他代码。在嵌入式系统中,CPU资源宝贵,阻塞意味着浪费。非阻塞编程的核心是:**将任务分解为多个小步骤,每个步骤在短时间内完成,然后立即返回主循环,让出CPU给其他任务**。 ## 状态机思想 状态机(State Machine)是一种数学模型,由状态、事件和转移组成。在任务调度中,每个任务可视为一个状态机: - **状态(State)**:任务当前所处的阶段,如“等待”、“执行”、“完成”。 - **事件(Event)**:触发状态转移的条件,如“时间到”、“数据就绪”。 - **转移(Transition)**:从一个状态到另一个状态的动作。 通过状态机,任务不再需要连续占用CPU,而是在每个状态中执行少量操作,然后根据条件跳转到下一个状态。 ## 时间片轮转 结合`millis()`函数,我们可以实现基于时间的调度。每个任务拥有自己的时间戳,主循环不断检查是否到达该任务的执行时间,若到达则执行一次状态机推进。这样,多个任务在宏观上“同时”运行,微观上分时复用CPU。 # 框架设计 ## 任务结构体 我们定义一个任务结构体,包含状态、时间戳、间隔和函数指针: ```c typedef struct { uint8_t state; // 当前状态 unsigned long lastTime; // 上次执行时间 unsigned long interval; // 执行间隔(ms) void (*taskFunc)(void); // 任务函数指针 } Task; ``` ## 调度器核心 调度器维护一个任务数组,在`loop()`中遍历所有任务,检查时间是否到期,若到期则调用对应函数。 ```c #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闪烁为例,使用状态机实现非阻塞: ```c // 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)。 ```c #include // 任务结构体定义 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控制、按键扫描等。希望读者能掌握这一思想,并将其应用到实际项目中,提升开发效率与系统性能。