# 引言:从阻塞到非阻塞的思维跃迁 Arduino 生态的简易性常让开发者忽视其背后的实时性挑战。传统 `delay()` 会暂停整个 CPU,导致 LED 闪烁与按键扫描无法并行。而状态机 + 非阻塞调度是嵌入式系统的经典解法。本文提出一种**模板化封装**,将状态机的通用逻辑抽象为可复用组件,让您只需关注业务状态转换,而无需重复编写调度代码。 # 核心原理:状态机与时间片轮转 ## 状态机基础 状态机由**状态**、**事件**和**转移**组成。在非阻塞场景下,每个任务维护一个当前状态,并在 `loop()` 中周期性检查时间戳,决定是否执行状态转移。例如,LED 闪烁任务可拆分为 `ON` 和 `OFF` 两个状态,每个状态持续固定时间后切换。 ## 模板化封装思路 模板允许我们定义**类型无关**的任务基类。核心思想: - 每个任务继承自 `Task` 基类,重写 `update()` 方法。 - 基类管理 `lastRunTime` 和 `interval`,通过 `millis()` 比较实现非阻塞延时。 - 使用模板指定任务数量,避免动态内存分配,适合资源受限的 MCU。 # 配置步骤:构建模板化任务调度器 ## 1. 定义任务基类 ```cpp class Task { public: virtual void update() = 0; // 纯虚函数,子类实现具体逻辑 bool shouldRun() { return (millis() - lastRun >= interval); } void run() { if (shouldRun()) { lastRun = millis(); update(); } } protected: unsigned long lastRun = 0; unsigned long interval = 0; }; ``` ## 2. 模板化调度器 ```cpp template class TaskScheduler { public: void addTask(Task* task, int index) { if (index < N) tasks[index] = task; } void runAll() { for (int i = 0; i < N; i++) { if (tasks[i]) tasks[i]->run(); } } private: Task* tasks[N] = {nullptr}; }; ``` ## 3. 实现具体状态机任务 以双 LED 交替闪烁为例: ```cpp class BlinkTask : public Task { public: BlinkTask(int pin, unsigned long interval) : pin(pin) { this->interval = interval; pinMode(pin, OUTPUT); state = LOW; } void update() override { state = !state; digitalWrite(pin, state); } private: int pin; bool state; }; ``` # 完整示例:多任务协同 ```cpp #include // 基类与调度器定义(如上)... // 按键扫描任务(状态机:等待->去抖->确认) class ButtonTask : public Task { public: ButtonTask(int pin) : pin(pin) { this->interval = 10; // 10ms 扫描 pinMode(pin, INPUT_PULLUP); state = 0; } void update() override { bool reading = digitalRead(pin); switch (state) { case 0: // 等待按下 if (reading == LOW) { state = 1; lastDebounce = millis(); } break; case 1: // 去抖 if (reading == LOW && (millis() - lastDebounce > 50)) { state = 2; Serial.println("Pressed!"); } else if (reading == HIGH) { state = 0; } break; case 2: // 等待释放 if (reading == HIGH) state = 0; break; } } private: int pin; int state; unsigned long lastDebounce; }; TaskScheduler<3> scheduler; BlinkTask led1(13, 500); BlinkTask led2(12, 250); ButtonTask btn(2); void setup() { Serial.begin(9600); scheduler.addTask(&led1, 0); scheduler.addTask(&led2, 1); scheduler.addTask(&btn, 2); } void loop() { scheduler.runAll(); } ``` # 进阶技巧:状态机与模板的深度融合 ## 使用模板参数传递状态类型 若状态较多,可定义 `enum class` 状态,并通过模板参数传入任务类,实现类型安全: ```cpp template class StateMachineTask : public Task { public: StateMachineTask(StateType initState) : currentState(initState) {} protected: StateType currentState; virtual void onStateChange(StateType newState) = 0; }; ``` ## 时间片轮转的精度优化 - 使用 `micros()` 替代 `millis()` 获得微秒级精度,但注意溢出周期更短。 - 对于高频任务,可考虑使用定时器中断,但模板方法更简单且足够应对大多数场景。 # 注意事项与常见陷阱 - **避免在 `update()` 中使用 `delay()`**:否则会阻塞整个调度器。 - **注意 `millis()` 溢出**:约 49 天后归零,但 `unsigned long` 减法仍正确,无需特殊处理。 - **任务数量固定**:模板参数需在编译期确定,若需动态增减,可改用链表或 `std::vector`(但避免在 MCU 上使用动态内存)。 - **状态机内避免长循环**:若某状态需要耗时操作,应拆分为多个子状态,逐步执行。 - **共享资源访问**:多任务间共享变量时,需考虑原子性,必要时使用 `volatile` 或临界区。 # 总结 通过模板化封装,我们构建了一个轻量级、可扩展的非阻塞任务调度器。它让 Arduino 程序从线性阻塞中解放,实现真正的并发处理。这种模式不仅适用于 LED 闪烁、按键扫描,还可扩展到传感器读取、通信协议解析等复杂场景。掌握它,您将迈入嵌入式实时编程的门槛,为更复杂的系统设计打下坚实基础。