引言
在嵌入式开发中,多任务处理是常见需求。然而,对于Arduino这类资源受限的平台,引入实时操作系统(RTOS)可能带来额外的内存开销和复杂性。协作式调度器提供了一种轻量级替代方案:通过定时器中断触发任务切换,实现看似并发的多任务执行,而无需复杂的上下文切换和优先级抢占。本文将带你从零构建一个基于定时器中断的协作式调度器,并深入解析其工作原理。
协作式调度器原理
协作式调度(Cooperative Scheduling)的核心思想是:任务主动让出CPU(通过yield或delay),调度器在后台(定时器中断)记录时间片,并在任务让出时切换到下一个就绪任务。与抢占式调度不同,协作式调度不强制中断正在运行的任务,因此任务必须自行控制执行时间,避免长时间占用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的复杂性和内存开销,同时保证了任务调度的实时性。本文的调度器设计可扩展,例如增加优先级、信号量等机制,以满足更复杂的需求。掌握这一技术,你将能更高效地组织嵌入式代码,提升系统响应能力。