引言

在 Arduino 开发中,delay()loop() 轮询是新手标配,但面对多传感器、多执行器或复杂协议时,阻塞式代码会导致响应迟钝、逻辑混乱。引入 RTOS(如 FreeRTOS)又受限于内存和复杂度。此时,协作式状态机框架提供了一种轻量级、零开销的解决方案:它利用状态机建模任务,通过非抢占式调度器循环执行,实现多任务“伪并行”。

核心原理

1. 协作式调度 vs 抢占式调度

  • 抢占式:由时钟中断强制切换任务,需保存/恢复上下文,资源开销大。
  • 协作式:任务主动让出 CPU(如 yield()),调度器按顺序执行就绪任务。优点是实现简单、无竞态,缺点是任务必须快速返回,否则阻塞全局。

2. 状态机模型

每个任务是一个有限状态机(FSM),包含:

  • 状态:用枚举或常量表示,如 IDLE, RUNNING, DONE
  • 事件:触发状态迁移的条件,如超时、传感器读数、外部中断。
  • 动作:在状态内执行的非阻塞操作,如读取数据、控制引脚。

状态机通过 switch-case 或函数指针实现,每次调度只执行当前状态的一小段逻辑,然后返回,避免长时间占用 CPU。

3. 时间片管理

协作式调度器通常配合 millis()micros() 实现时间片。每个任务记录上次执行时间,当 now - lastTime >= interval 时,才执行该任务的状态机。这样,即使任务未完成,也能按周期推进。

框架设计

我们构建一个轻量级任务管理器,支持注册任务、设置周期和状态机回调。

1. 数据结构

typedef struct {
  void (*init)(void);       // 初始化函数
  void (*run)(void);        // 状态机主函数
  uint32_t interval;        // 执行周期 (ms)
  uint32_t lastRun;         // 上次执行时间戳
  uint8_t state;            // 当前状态
  bool enabled;             // 是否启用
} Task_t;

2. 调度器核心

#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. 状态机宏定义

为了方便定义状态,我们使用宏:

#define STATE(task, s) ((task)->state = (s))
#define GET_STATE(task) ((task)->state)

完整示例:LED 闪烁 + 串口打印

我们创建两个任务:LED 每 500ms 翻转,串口每 1s 打印计数。

1. 任务函数

// 任务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. 主程序

void setup() {
  scheduler_init();
  add_task(led_init, led_run, 500);   // 500ms 周期
  add_task(serial_init, serial_run, 1000); // 1s 周期
}

void loop() {
  scheduler_run();
  // 可添加其他非阻塞代码
}

进阶:状态机与事件驱动

实际项目中,状态机往往需要响应外部事件(如按键、传感器中断)。我们可以通过全局标志或队列传递事件。

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 库