引言

在嵌入式开发中,多任务处理是常见需求。然而,对于Arduino这类资源受限的平台,引入实时操作系统(RTOS)可能带来额外的内存开销和复杂性。协作式调度器提供了一种轻量级替代方案:通过定时器中断触发任务切换,实现看似并发的多任务执行,而无需复杂的上下文切换和优先级抢占。本文将带你从零构建一个基于定时器中断的协作式调度器,并深入解析其工作原理。

协作式调度器原理

协作式调度(Cooperative Scheduling)的核心思想是:任务主动让出CPU(通过yielddelay),调度器在后台(定时器中断)记录时间片,并在任务让出时切换到下一个就绪任务。与抢占式调度不同,协作式调度不强制中断正在运行的任务,因此任务必须自行控制执行时间,避免长时间占用CPU。

定时器中断的角色

定时器中断(如Arduino的Timer1)作为调度器的“心跳”,周期性触发中断服务程序(ISR)。在ISR中,我们更新系统时钟(tick计数),并检查是否有任务需要唤醒。当任务调用delay()yield()时,调度器将当前任务挂起,并选择下一个就绪任务执行。

任务状态机

每个任务具有以下状态:

  • 就绪(Ready):等待执行,但未到启动时间。
  • 运行(Running):当前正在执行的任务。
  • 等待(Waiting):调用了delay(),等待指定时间后唤醒。

调度器维护一个任务控制块(TCB)数组,每个TCB包含任务函数指针、状态、下次运行时间等。

调度器设计步骤

1. 定义任务控制块和调度器数据结构

#define MAX_TASKS 4

typedef struct {
  void (*taskFunc)(void);  // 任务函数指针
  uint8_t state;           // 任务状态:0-就绪,1-运行,2-等待
  uint32_t delay_ticks;    // 剩余等待的tick数
  uint32_t last_run;       // 上次运行时的tick计数
} TCB;

static TCB tasks[MAX_TASKS];
static uint8_t task_count = 0;
static uint8_t current_task = 0;
static volatile uint32_t tick = 0;  // 系统tick计数

2. 初始化定时器中断

使用Arduino的Timer1,配置为1ms中断一次。

void scheduler_init() {
  // 设置Timer1为CTC模式,1ms中断
  TCCR1A = 0;
  TCCR1B = (1 << WGM12) | (1 << CS11);  // 8分频
  OCR1A = 1999;  // 16MHz/8/2000 = 1kHz
  TIMSK1 = (1 << OCIE1A);
  sei();
}

ISR(TIMER1_COMPA_vect) {
  tick++;
  // 遍历任务,更新等待状态
  for (uint8_t i = 0; i < task_count; i++) {
    if (tasks[i].state == 2) {  // 等待状态
      if (--tasks[i].delay_ticks == 0) {
        tasks[i].state = 0;  // 变为就绪
      }
    }
  }
}

3. 任务注册与调度核心

void scheduler_add_task(void (*func)()) {
  if (task_count < MAX_TASKS) {
    tasks[task_count].taskFunc = func;
    tasks[task_count].state = 0;
    tasks[task_count].delay_ticks = 0;
    tasks[task_count].last_run = 0;
    task_count++;
  }
}

void scheduler_dispatch() {
  while (1) {
    // 寻找下一个就绪任务
    uint8_t found = 0;
    for (uint8_t i = 0; i < task_count; i++) {
      uint8_t idx = (current_task + i + 1) % task_count;
      if (tasks[idx].state == 0) {
        current_task = idx;
        tasks[idx].state = 1;  // 运行
        tasks[idx].last_run = tick;
        tasks[idx].taskFunc();  // 执行任务
        tasks[idx].state = 0;   // 任务返回后,设为就绪(若未主动等待)
        found = 1;
        break;
      }
    }
    if (!found) {
      // 无任务就绪,进入低功耗或空转
      delay(1);
    }
  }
}

4. 任务内的时间管理

任务需要调用task_delay()来让出CPU并等待指定时间。

void task_delay(uint32_t ms) {
  uint32_t ticks_to_wait = ms;  // 假设1ms一个tick
  tasks[current_task].state = 2;  // 设置为等待
  tasks[current_task].delay_ticks = ticks_to_wait;
  // 主动让出CPU,但调度器会继续循环,因此这里直接返回即可
}

注意:在协作式调度中,任务函数必须定期调用task_delay()yield(),否则其他任务无法执行。

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

以下示例创建两个任务:一个LED闪烁,一个串口打印计数。

#include <Arduino.h>

// 调度器代码(如上)
// ...

void task_led() {
  static uint8_t led_state = 0;
  digitalWrite(13, led_state);
  led_state = !led_state;
  task_delay(500);  // 每500ms切换
}

void task_serial() {
  static uint32_t count = 0;
  Serial.println(count++);
  task_delay(1000);  // 每1000ms打印
}

void setup() {
  pinMode(13, OUTPUT);
  Serial.begin(9600);
  scheduler_init();
  scheduler_add_task(task_led);
  scheduler_add_task(task_serial);
  scheduler_dispatch();  // 启动调度器(永不返回)
}

void loop() {
  // 不会执行到这里
}

注意事项

  • 任务函数不能阻塞:避免使用delay()(Arduino内置)或长时间循环,应使用task_delay()
  • 共享数据保护:由于中断可能随时发生,访问全局变量时需考虑原子性,必要时关闭中断。
  • 任务数量限制:MAX_TASKS根据内存调整,每个TCB占用约10字节,Arduino Uno可支持10-20个任务。
  • 定时器精度:1ms tick足够大多数应用,若需更高精度可调整OCR1A值。
  • 中断优先级:定时器中断应保持简短,避免在ISR中做复杂操作。

总结

通过定时器中断实现的协作式调度器,为Arduino提供了一种轻量级多任务方案。它避免了RTOS的复杂性和内存开销,同时保证了任务调度的实时性。本文的调度器设计可扩展,例如增加优先级、信号量等机制,以满足更复杂的需求。掌握这一技术,你将能更高效地组织嵌入式代码,提升系统响应能力。