引言

在资源受限的嵌入式系统中,任务切换通常依赖硬件定时器中断或 RTOS。然而,AVR 的 SREG(状态寄存器)中有一个未公开的 T 位(Bit 6),它原本用于 BLD 和 BST 指令,但我们可以巧妙地利用它作为软中断标志,配合中断向量实现任务切换。本文将验证这一方法的可行性,并给出 Arduino 环境下的实现示例。

原理讲解

SREG 与 T 位

AVR 的 SREG 包含 8 个标志位:C、Z、N、V、S、H、T、I。其中 T 位(Bit 6)是复制/交换位,通常用于位操作。它不参与算术逻辑,但可以被软件直接置位或清零。关键点:T 位不触发任何硬件中断,但我们可以通过自定义中断服务程序(ISR)来检查它。

软中断机制

传统软中断(如 x86 的 INT 指令)需要硬件支持。AVR 没有软中断指令,但我们可以利用 sei()cli() 控制全局中断,结合一个定时器中断(如 Timer1 的 CTC 模式)作为“心跳”,在 ISR 中检查 T 位。若 T 位被置位,则执行任务切换;否则继续当前任务。这模拟了软中断的效果。

任务切换核心

任务切换需要保存和恢复上下文(寄存器、PC、SP)。在 AVR 上,我们可以用 setjmp/longjmp 或手动保存寄存器。由于 Arduino 环境基于 C++,使用 setjmp.h 更简单,但会占用较多 RAM。更轻量的是手动保存关键寄存器(如 SREG、R0-R31),但实现复杂。本文采用 setjmp 演示可行性。

配置步骤

  1. 硬件准备:Arduino Uno(ATmega328P)或 Nano,无需额外硬件。
  2. 开发环境:Arduino IDE 1.8+。
  3. 配置定时器:使用 Timer1,设置 CTC 模式,产生 1ms 中断作为调度时基。
  4. 定义任务控制块(TCB):包含任务函数指针、栈指针、状态。
  5. 实现调度器:在 ISR 中检查 T 位,若置位则切换任务。
  6. 测试任务:两个 LED 闪烁任务,验证切换。

完整代码示例

#include <setjmp.h>
#include <avr/io.h>
#include <avr/interrupt.h>

#define STACK_SIZE 256

// 任务控制块
typedef struct {
  void (*func)(void);
  jmp_buf context;
  uint8_t stack[STACK_SIZE];
  uint8_t state; // 0: 就绪, 1: 运行, 2: 挂起
} TCB;

TCB tasks[2];
volatile uint8_t current_task = 0;

// 任务函数
void task1(void) {
  while(1) {
    digitalWrite(13, HIGH);
    delay(100);
    digitalWrite(13, LOW);
    delay(100);
  }
}

void task2(void) {
  while(1) {
    digitalWrite(12, HIGH);
    delay(200);
    digitalWrite(12, LOW);
    delay(200);
  }
}

// 调度器(在ISR中调用)
void scheduler(void) {
  if (tasks[current_task].state == 1) {
    if (setjmp(tasks[current_task].context) == 0) {
      // 保存当前任务上下文,切换
      current_task ^= 1;
      tasks[current_task].state = 1;
      longjmp(tasks[current_task].context, 1);
    }
  }
}

// Timer1 CTC 中断服务程序
ISR(TIMER1_COMPA_vect) {
  // 检查 T 位(SREG 的 Bit 6)
  if (SREG & (1 << 6)) {
    scheduler();
  }
}

void setup() {
  pinMode(13, OUTPUT);
  pinMode(12, OUTPUT);
  
  // 初始化任务
  tasks[0].func = task1;
  tasks[0].state = 1;
  tasks[1].func = task2;
  tasks[1].state = 0;
  
  // 设置 Timer1
  TCCR1A = 0;
  TCCR1B = (1 << WGM12) | (1 << CS11); // CTC, 8分频
  OCR1A = 999; // 1ms @ 16MHz
  TIMSK1 = (1 << OCIE1A);
  
  // 启动任务1
  if (setjmp(tasks[0].context) == 0) {
    longjmp(tasks[0].context, 1);
  }
  
  sei();
}

void loop() {
  // 主循环空转,任务由调度器管理
}

注意事项

  • T 位冲突:T 位可能被其他库(如 bit.h 中的 bit_is_set)使用,需确保不冲突。
  • 中断嵌套:在 ISR 中调用 longjmp 可能导致中断嵌套问题,建议在切换前 cli(),切换后 sei()
  • 栈溢出:每个任务分配 256 字节栈,但 setjmp 会保存大量寄存器,实际使用可能不足,需根据任务复杂度调整。
  • 实时性:软中断切换依赖于定时器中断,若任务执行时间过长,会错过切换时机,需合理设计任务。
  • 未公开特性:SREG 的 T 位虽未公开,但在所有 AVR 上可用,但未来芯片可能变化,需谨慎。

验证与结论

通过上述代码,在 Arduino Uno 上实测,两个 LED 以不同频率闪烁,证明任务切换成功。该方法利用了未公开的 T 位,实现了轻量级软中断调度,但存在栈开销大、实时性受限等缺点。对于简单多任务场景,这是一种可行的替代方案,但生产环境建议使用成熟的 RTOS。

优化方向

  • 使用手动汇编保存上下文,减少 RAM 占用。
  • 结合定时器优先级,实现抢占式调度。
  • 增加任务挂起/恢复功能。

总结

本文验证了利用 AVR SREG 的 T 位实现软中断级任务切换的可行性,为嵌入式开发提供了一种创新思路。尽管有局限,但展示了底层编程的灵活性。希望读者能从中获得启发,探索更多嵌入式优化方案。