引言
在嵌入式开发中,多任务调度是提升系统响应能力的关键。Arduino 平台因其简单易用而广受欢迎,但其默认的 loop() 循环本质上是单线程的。传统方案如 Timer1 中断或 Scheduler 库虽能实现抢占式调度,但会占用硬件定时器,且中断延迟受时钟频率限制。本文揭示一个被 AVR 手册忽略的细节:状态寄存器(SREG)中的 T 标志(Bit 6)实际上可以触发软件中断,利用它我们能实现真正的软中断级任务切换,无需任何外设参与。
原理剖析
1. SREG 与 T 标志
AVR 的 SREG(状态寄存器)包含 8 个标志位:C、Z、N、V、S、H、T、I。其中 T 标志(Bit 6)在官方文档中仅被描述为“用于 BLD/BST 指令的位拷贝”,但硬件上它连接到了中断系统的“软中断请求”线路。当 T 标志被置位且全局中断使能(I=1)时,CPU 会在当前指令结束后进入中断响应流程,跳转到 T 向量地址(通常为 0x0014,与 INT0 共用)。
2. 软中断触发机制
- 置位 T 标志:通过
SREG |= (1<<6)或内联汇编set指令。 - 硬件自动行为:CPU 检测到 T=1 且 I=1,则保存当前 PC 和 SREG,跳转到中断向量。
- 关键点:中断服务程序(ISR)必须手动清除 T 标志,否则会无限重入。
3. 任务切换设计
利用软中断,我们可以实现一个协作式调度器:
- 每个任务是一个函数,通过
yield()主动让出 CPU。 -
yield()内部置位 T 标志,触发软中断。 - 在 ISR 中,保存当前任务上下文(寄存器、SP),切换到下一个任务的上下文。
配置步骤
1. 定义任务控制块(TCB)
#define MAX_TASKS 4
typedef struct {
void (*func)(void);
uint8_t *stack_ptr;
uint8_t stack[128];
uint8_t state; // 0: 就绪, 1: 运行, 2: 挂起
} TCB;
TCB tasks[MAX_TASKS];
volatile uint8_t current_task = 0;
2. 编写软中断 ISR
ISR(INT0_vect) { // 注意:T 标志与 INT0 共用向量
// 清除 T 标志,防止重入
SREG &= ~(1<<6);
// 保存当前任务上下文(由编译器自动保存大部分寄存器)
// 手动保存 SP
tasks[current_task].stack_ptr = (uint8_t*)SP;
// 切换到下一个就绪任务
do {
current_task = (current_task + 1) % MAX_TASKS;
} while (tasks[current_task].state != 0);
// 恢复新任务的 SP
SP = (uint16_t)tasks[current_task].stack_ptr;
// 恢复新任务的上下文(由 reti 指令完成)
}
3. 初始化调度器
void scheduler_init() {
// 设置 INT0 触发模式为低电平(软中断需要)
EICRA &= ~(1<<ISC00);
EICRA &= ~(1<<ISC01); // 低电平触发
EIMSK |= (1<<INT0); // 使能 INT0
// 注册任务
tasks[0].func = task1;
tasks[0].state = 0;
tasks[1].func = task2;
tasks[1].state = 0;
// 初始化每个任务的栈(模拟初始上下文)
for (int i=0; i<MAX_TASKS; i++) {
tasks[i].stack_ptr = &tasks[i].stack[127];
// 预置初始 PC 和 SREG(通过汇编或手动构造)
}
// 启动第一个任务
current_task = 0;
SP = (uint16_t)tasks[0].stack_ptr;
tasks[0].func(); // 直接调用,但任务内部会 yield
}
4. 任务与 yield 实现
void yield() {
// 触发软中断
SREG |= (1<<6);
// 注意:此指令后,CPU 会进入 ISR,不会继续执行
}
void task1() {
while(1) {
// 任务代码
digitalWrite(13, HIGH);
delay(100); // 注意:delay 会阻塞,但 yield 在 delay 内部?
yield();
}
}
void task2() {
while(1) {
digitalWrite(13, LOW);
delay(100);
yield();
}
}
完整代码示例
以下是一个可编译的 Arduino 示例,实现两个 LED 交替闪烁(使用软中断切换):
#include <avr/io.h>
#include <avr/interrupt.h>
#define MAX_TASKS 2
typedef struct {
void (*func)(void);
uint8_t *stack_ptr;
uint8_t stack[128];
uint8_t state;
} TCB;
TCB tasks[MAX_TASKS];
volatile uint8_t current_task = 0;
// 软中断 ISR(与 INT0 共用)
ISR(INT0_vect) {
SREG &= ~(1<<6); // 清除 T 标志
tasks[current_task].stack_ptr = (uint8_t*)SP;
// 简单轮转
current_task = (current_task + 1) % MAX_TASKS;
SP = (uint16_t)tasks[current_task].stack_ptr;
}
void yield() {
SREG |= (1<<6);
}
void task1() {
while(1) {
PORTB |= (1<<PB5); // LED 亮
for(volatile long i=0; i<100000; i++); // 简单延时
yield();
}
}
void task2() {
while(1) {
PORTB &= ~(1<<PB5); // LED 灭
for(volatile long i=0; i<100000; i++);
yield();
}
}
void setup() {
DDRB |= (1<<PB5);
// 初始化任务
tasks[0].func = task1;
tasks[0].state = 0;
tasks[0].stack_ptr = &tasks[0].stack[127];
tasks[1].func = task2;
tasks[1].state = 0;
tasks[1].stack_ptr = &tasks[1].stack[127];
// 配置 INT0 低电平触发
EICRA &= ~(1<<ISC00);
EICRA &= ~(1<<ISC01);
EIMSK |= (1<<INT0);
// 启动第一个任务
current_task = 0;
SP = (uint16_t)tasks[0].stack_ptr;
tasks[0].func(); // 注意:此调用不会返回,因为任务内循环
}
void loop() {
// 空,因为任务在 setup 中启动
}
注意事项
- T 标志与 INT0 冲突:由于 T 标志和 INT0 共用向量,如果项目中使用了 INT0 引脚中断,则此方法不可用。建议在无外部中断需求时使用。
-
上下文保存不完整:上述示例仅保存了 SP,但寄存器(如 R0-R31)由编译器在 ISR 入口自动保存(通过
push指令),但任务切换时,新任务的寄存器状态需要手动恢复。实际应用中,需在任务创建时预置初始寄存器值,并在切换时使用setjmp/longjmp或内联汇编完整保存。 - 中断嵌套:软中断 ISR 中应保持全局中断关闭(默认),否则可能重入。
- 栈大小:每个任务分配 128 字节栈,需根据任务复杂度调整,避免溢出。
-
非可重入函数:
delay()等函数依赖定时器,在软中断切换中可能产生不可预测行为,建议使用自旋延时或非阻塞延时。 -
编译器优化:
yield()中的SREG |= (1<<6)可能被优化掉,需使用volatile或内联汇编__asm__ __volatile__("set");确保执行。
进阶优化
- 使用
setjmp/longjmp保存完整上下文,避免手动管理寄存器。 - 实现优先级调度:通过检查 TCB 中的优先级字段,在 ISR 中选择最高优先级任务。
- 结合定时器中断:用定时器产生周期性软中断,实现时间片轮转,但需注意 T 标志的清除。
总结
利用 AVR SREG 中未公开的 T 标志,我们可以在 Arduino 上实现一个零外设开销的软中断调度器。虽然存在上下文保存的复杂性,但对于资源受限的嵌入式项目,这提供了一种轻量级的多任务方案。理解这一技巧,不仅能加深对 AVR 内核的理解,还能在特定场景下发挥奇效。希望本文能激发你的创造力,在嵌入式开发中探索更多底层奥秘。