引言

在 Arduino 这类资源有限的嵌入式平台上,直接运行 RTOS 往往显得笨重。协作式调度器(Cooperative Scheduler)通过任务主动让出 CPU 的方式,实现多任务并发,代码量极小且无上下文切换开销。本文的核心是一个基于定时器中断的调度器,其核心代码仅 10 行,却足以支撑多任务轮转。

原理讲解

协作式 vs 抢占式

  • 协作式:任务必须显式调用 yield()delay() 来让出 CPU,否则会阻塞其他任务。
  • 抢占式:由定时器中断强制切换任务,无需任务主动配合,但需要保存/恢复上下文。

本文采用协作式,但利用定时器中断来产生时间基准,驱动任务调度。

调度器核心机制

调度器维护一个任务数组,每个任务包含:

  • 函数指针 func
  • 执行周期 period(毫秒)
  • 上次执行时间 lastRun

定时器中断(如 Timer1)每 1ms 触发一次,在中断服务程序(ISR)中更新一个全局 tick 计数。主循环中,调度器遍历任务列表,检查当前 tick 是否达到任务的执行周期,若达到则调用任务函数。

为什么是 10 行?

核心调度逻辑可以浓缩为以下 10 行(不含任务定义和初始化):

void scheduler_run() {
  for (int i = 0; i < taskCount; i++) {
    if (millis() - tasks[i].lastRun >= tasks[i].period) {
      tasks[i].lastRun = millis();
      tasks[i].func();
    }
  }
}

这里 millis() 由定时器中断维护,每 1ms 递增。这 10 行代码实现了非阻塞的周期任务调度。

配置步骤

1. 定义任务结构体

typedef struct {
  void (*func)(void);
  unsigned long period;
  unsigned long lastRun;
} Task;

2. 初始化定时器中断

在 Arduino 上,我们使用 Timer1 产生 1ms 中断:

#include <TimerOne.h>

void setup() {
  Timer1.initialize(1000);  // 1000us = 1ms
  Timer1.attachInterrupt(timerISR);
}

volatile unsigned long tick = 0;
void timerISR() {
  tick++;
}

注意:millis() 本身也是基于定时器中断,但为了演示,我们自定义 tick。实际中可直接使用 millis(),但需注意其精度和溢出。

3. 实现调度器

#define MAX_TASKS 5
Task tasks[MAX_TASKS];
int taskCount = 0;

void scheduler_init() {
  taskCount = 0;
}

int scheduler_add(void (*func)(), unsigned long period) {
  if (taskCount >= MAX_TASKS) return -1;
  tasks[taskCount].func = func;
  tasks[taskCount].period = period;
  tasks[taskCount].lastRun = 0;
  return taskCount++;
}

void scheduler_run() {
  for (int i = 0; i < taskCount; i++) {
    if (millis() - tasks[i].lastRun >= tasks[i].period) {
      tasks[i].lastRun = millis();
      tasks[i].func();
    }
  }
}

4. 主循环与任务定义

void task1() {
  digitalWrite(13, !digitalRead(13));  // 翻转 LED
}

void task2() {
  Serial.println("Task2 running");
}

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  scheduler_init();
  scheduler_add(task1, 500);  // 每 500ms 执行
  scheduler_add(task2, 1000); // 每 1000ms 执行
  Timer1.initialize(1000);
  Timer1.attachInterrupt(timerISR);
}

void loop() {
  scheduler_run();
  // 其他非阻塞操作
}

完整代码示例

#include <TimerOne.h>

#define MAX_TASKS 5

typedef struct {
  void (*func)(void);
  unsigned long period;
  unsigned long lastRun;
} Task;

Task tasks[MAX_TASKS];
int taskCount = 0;
volatile unsigned long tick = 0;

void timerISR() { tick++; }

void scheduler_init() { taskCount = 0; }

int scheduler_add(void (*func)(), unsigned long period) {
  if (taskCount >= MAX_TASKS) return -1;
  tasks[taskCount].func = func;
  tasks[taskCount].period = period;
  tasks[taskCount].lastRun = 0;
  return taskCount++;
}

void scheduler_run() {
  for (int i = 0; i < taskCount; i++) {
    if (millis() - tasks[i].lastRun >= tasks[i].period) {
      tasks[i].lastRun = millis();
      tasks[i].func();
    }
  }
}

void task1() {
  digitalWrite(13, !digitalRead(13));
}

void task2() {
  Serial.println("Task2 running");
}

void setup() {
  Serial.begin(9600);
  pinMode(13, OUTPUT);
  scheduler_init();
  scheduler_add(task1, 500);
  scheduler_add(task2, 1000);
  Timer1.initialize(1000);
  Timer1.attachInterrupt(timerISR);
}

void loop() {
  scheduler_run();
}

注意事项

  • 任务不能阻塞:协作式调度要求任务必须快速返回,不能使用 delay() 或长时间循环,否则会延迟其他任务。
  • 中断与主循环共享变量tickvolatile,但主循环中读取 millis() 时,应使用原子读取(Arduino 的 millis() 已保证原子性)。
  • 定时器精度Timer1 的 1ms 中断精度足够大多数应用,但若需更高精度,可改用微秒级定时器。
  • 任务数量限制MAX_TASKS 需根据实际调整,避免数组越界。
  • 溢出处理millis() 约 49 天溢出,但上述代码中 millis() - lastRun 的差值计算可正确处理溢出(无符号减法)。

总结

本文通过 10 行核心代码展示了 Arduino 上无 OS 的协作式调度器实现。它利用定时器中断提供时间基准,主循环中轮询任务,实现了轻量级多任务调度。这种方法在资源受限的嵌入式系统中非常实用,且易于扩展。理解其原理后,你可以根据需求增加优先级、任务删除等功能,构建更强大的调度器。