引言

在嵌入式开发中,多任务调度是提升系统响应能力的关键。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 内核的理解,还能在特定场景下发挥奇效。希望本文能激发你的创造力,在嵌入式开发中探索更多底层奥秘。