Arduino 上实现无操作系统优先级调度:基于 Timer1 的协作式任务切换技巧
👁 1 阅读 · 2026-08-27 · 嵌入式
在资源受限的 Arduino 平台上,引入完整 RTOS 往往过于沉重。本文深入探讨如何利用 Timer1 中断与自定义任务控制块(TCB),在无操作系统环境下实现轻量级协作式优先级调度。通过精确的时间片轮转与优先级抢占(协作式),开发者可以优雅地管理多个实时任务,提升系统响应性与代码可维护性。文章包含原理剖析、寄存器级配置、完整代码示例及关键注意事项,助你掌握嵌入式调度的核心技巧。
# 引言
在嵌入式开发中,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 密集型任务。掌握这一技巧,能帮助你更高效地组织代码,提升系统实时性。