# 引言 在嵌入式开发中,Arduino 以其简单易用著称,但面对多任务并发需求时,传统的 `loop()` 轮询方式往往显得力不从心。引入 RTOS(如 FreeRTOS)虽能解决问题,但在 Flash/RAM 有限的芯片上(如 ATmega328P),其开销不容忽视。本文介绍一种基于 Timer1 的协作式优先级调度器,它通过硬件定时器中断驱动任务切换,实现无操作系统的轻量级多任务管理,特别适合对实时性要求不极端、但需要结构化任务管理的场景。 # 原理:协作式调度与优先级 协作式调度(Cooperative Scheduling)的核心是:任务主动让出 CPU(通过 `yield()` 或 `delay()`),而非被系统强制抢占。我们利用 Timer1 产生周期性中断,在中断服务程序(ISR)中检查当前任务是否超时,若超时则保存上下文并切换到下一个就绪的高优先级任务。 优先级体现在任务就绪队列的排序上:我们维护一个任务数组,按优先级从高到低排列,调度器总是选择最高优先级的就绪任务执行。由于是协作式,高优先级任务不会被低优先级任务打断(除非低优先级任务主动让出),但高优先级任务可以通过设置更短的时间片来获得更频繁的 CPU 时间。 ## 关键组件:任务控制块(TCB) 每个任务由一个 TCB 描述,包含: - 任务函数指针 - 任务状态(就绪/阻塞) - 优先级 - 时间片(以毫秒为单位) - 剩余时间计数 - 上下文(保存的寄存器,这里简化处理,因为协作式切换发生在任务函数内部,不需要完整上下文切换) 注意:在 AVR 上,由于中断会保存大部分寄存器,我们只需在任务切换时保存/恢复栈指针和程序计数器(PC),但为了简化,我们采用“任务函数返回后调度”的方式,即每个任务是一个无限循环,但通过 `yield()` 主动让出。 # 硬件配置:Timer1 设置 ATmega328P 的 Timer1 是 16 位定时器,适合产生较长周期中断。我们配置为 CTC 模式(Clear Timer on Compare Match),产生 1ms 的节拍。 ## 寄存器配置步骤 1. **设置比较值**:`OCR1A = 15999;`(16MHz 时钟,256 分频,1ms 中断:16000000/256/1000 = 62.5,取整 62,但实际用 15999 对应 1ms?计算:16000000/256 = 62500 Hz,1ms 需要 62.5 个计数,所以 OCR1A = 62-1 = 61?不对,我们使用 CTC 模式,计数到 OCR1A 后清零,所以周期 = (OCR1A+1)/62500 秒,要 1ms,则 OCR1A+1 = 62.5,取 62,则周期约 0.992ms。为精确,可调整。这里为了示例,我们设定 1ms 节拍,OCR1A = 62-1 = 61。但实际常用 1ms 用 15999 是因为 256 分频下 16MHz/256=62500Hz,1ms 需要 62.5 个计数,所以 OCR1A=61。但很多库用 15999 是因为 1024 分频?我们明确:使用 256 分频,OCR1A = 61。 ```c TCCR1B = (1 << WGM12) | (1 << CS12); // CTC 模式,256 分频 OCR1A = 61; // 1ms 中断 TIMSK1 = (1 << OCIE1A); // 使能比较匹配中断 ``` 2. **中断服务程序**:在 ISR 中调用调度器 `scheduler_tick()`。 # 调度器实现 ## 数据结构 ```c #define MAX_TASKS 4 typedef void (*TaskFunc)(); typedef struct { TaskFunc func; uint8_t priority; // 数值越大优先级越高 uint8_t state; // 0: 阻塞, 1: 就绪 uint16_t time_slice; // 时间片 ms uint16_t remaining; // 剩余时间 ms } TCB; TCB tasks[MAX_TASKS]; volatile uint8_t current_task = 0; // 当前运行任务索引 volatile uint8_t task_count = 0; ``` ## 任务注册与调度逻辑 ```c void scheduler_init() { // 初始化 Timer1 noInterrupts(); TCCR1B = (1 << WGM12) | (1 << CS12); OCR1A = 61; TIMSK1 = (1 << OCIE1A); interrupts(); } int8_t task_create(TaskFunc func, uint8_t priority, uint16_t time_slice) { if (task_count >= MAX_TASKS) return -1; tasks[task_count].func = func; tasks[task_count].priority = priority; tasks[task_count].time_slice = time_slice; tasks[task_count].remaining = time_slice; tasks[task_count].state = 1; // 就绪 // 简单按优先级插入排序,保持数组降序 int8_t i = task_count - 1; while (i >= 0 && tasks[i].priority < priority) { tasks[i+1] = tasks[i]; i--; } tasks[i+1] = tasks[task_count]; // 注意:这里逻辑有误,需修正 // 正确做法:先复制原任务到临时,然后移动,最后插入 // 为简化,我们直接使用冒泡排序,但这里省略,见完整代码 task_count++; return 0; } void scheduler_tick() { // 递减当前任务的剩余时间 if (tasks[current_task].remaining > 0) { tasks[current_task].remaining--; } // 如果当前任务时间片用完,则切换到下一个就绪任务(最高优先级) if (tasks[current_task].remaining == 0) { // 寻找最高优先级就绪任务 uint8_t next = 0; for (uint8_t i = 0; i < task_count; i++) { if (tasks[i].state == 1) { next = i; break; } } // 重置当前任务时间片(但仅当它仍就绪) if (tasks[current_task].state == 1) { tasks[current_task].remaining = tasks[current_task].time_slice; } current_task = next; tasks[current_task].remaining = tasks[current_task].time_slice; } } void yield() { // 主动让出,立即触发调度(通过设置 remaining=0) tasks[current_task].remaining = 0; // 等待下一个 tick 进行切换,或者直接调用 scheduler_tick()(但需注意中断嵌套) // 这里简单调用 scheduler_tick(),但需关中断 noInterrupts(); scheduler_tick(); interrupts(); } ``` ## 任务函数示例 ```c void task_high() { while (1) { digitalWrite(13, HIGH); delay(100); // 内部会调用 yield digitalWrite(13, LOW); delay(100); } } void task_low() { while (1) { // 低优先级任务 Serial.println("Low"); yield(); } } ``` 注意:`delay()` 函数在 Arduino 中会阻塞,但我们可以重写一个基于调度的 `delay()`,或者让任务使用 `yield()` 配合 `millis()` 实现非阻塞延时。这里为了演示,我们使用 `yield()` 手动让出。 # 完整代码示例 ```c #include #define MAX_TASKS 4 typedef void (*TaskFunc)(); typedef struct { TaskFunc func; uint8_t priority; uint8_t state; // 1: ready, 0: blocked uint16_t time_slice; uint16_t remaining; } TCB; TCB tasks[MAX_TASKS]; volatile uint8_t task_count = 0; volatile uint8_t current_task = 0; // 简单排序插入(按优先级降序) void task_create(TaskFunc func, uint8_t priority, uint16_t slice) { if (task_count >= MAX_TASKS) return; // 找到插入位置 int8_t pos = task_count; for (int8_t i = task_count - 1; i >= 0; i--) { if (tasks[i].priority < priority) { pos = i; } else break; } // 移动元素 for (int8_t i = task_count; i > pos; i--) { tasks[i] = tasks[i-1]; } // 插入新任务 tasks[pos].func = func; tasks[pos].priority = priority; tasks[pos].time_slice = slice; tasks[pos].remaining = slice; tasks[pos].state = 1; task_count++; } void scheduler_tick() { if (task_count == 0) return; // 递减当前任务剩余时间 if (tasks[current_task].remaining > 0) { tasks[current_task].remaining--; } // 如果时间片用完,切换 if (tasks[current_task].remaining == 0) { // 寻找最高优先级就绪任务(数组已按优先级降序) uint8_t next = current_task; for (uint8_t i = 0; i < task_count; i++) { if (tasks[i].state == 1) { next = i; break; } } // 重置当前任务时间片(如果它仍就绪) if (tasks[current_task].state == 1) { tasks[current_task].remaining = tasks[current_task].time_slice; } current_task = next; tasks[current_task].remaining = tasks[current_task].time_slice; } } void yield() { noInterrupts(); tasks[current_task].remaining = 0; scheduler_tick(); interrupts(); } // 任务函数 void task_led() { pinMode(13, OUTPUT); while (1) { digitalWrite(13, HIGH); delay(200); // 注意:delay会阻塞,但这里我们简单使用,实际应替换为基于调度的延时 digitalWrite(13, LOW); delay(200); } } void task_print() { Serial.begin(9600); while (1) { Serial.println("Low priority task"); yield(); // 主动让出 } } void setup() { // 初始化调度器 cli(); TCCR1B = (1 << WGM12) | (1 << CS12); // CTC, 256分频 OCR1A = 61; // 1ms TIMSK1 = (1 << OCIE1A); sei(); // 创建任务:高优先级 LED 闪烁,低优先级串口打印 task_create(task_led, 2, 10); // 10ms 时间片 task_create(task_print, 1, 20); // 20ms 时间片 // 启动第一个任务 current_task = 0; tasks[current_task].remaining = tasks[current_task].time_slice; } void loop() { // 调度器由中断驱动,loop 中执行当前任务 tasks[current_task].func(); } ISR(TIMER1_COMPA_vect) { scheduler_tick(); } ``` # 注意事项 - **中断安全**:在 `yield()` 和调度器内部操作共享变量时,必须关中断(`noInterrupts()`/`interrupts()`),防止数据竞争。 - **任务函数不得阻塞**:协作式调度要求任务不能长时间占用 CPU,否则低优先级任务会饿死。应使用非阻塞延时(如基于 `millis()` 的状态机)或频繁调用 `yield()`。 - **栈空间**:每个任务共享同一个栈(因为协作式切换不切换栈),所以任务函数内局部变量不宜过大,避免溢出。 - **优先级反转**:由于是协作式,高优先级任务必须主动让出,否则低优先级任务永远无法执行。设计时需确保高优先级任务有 `yield()` 或延时。 - **时间片精度**:OCR1A 的计算需根据实际晶振频率调整,示例中 61 对应 1ms 是基于 16MHz 和 256 分频,若使用其他芯片需重新计算。 - **任务切换开销**:每次切换仅需几个周期,但频繁切换会增加开销,建议时间片不小于 5ms。 # 总结 本文展示了如何在 Arduino 上利用 Timer1 实现一个轻量级协作式优先级调度器。通过硬件定时器中断驱动任务切换,我们避免了 RTOS 的复杂性和资源占用,同时获得了多任务管理能力。此方法适用于中小型嵌入式项目,尤其适合 I/O 密集型任务。掌握这一技巧,能帮助你更高效地组织代码,提升系统实时性。