引言

在 Arduino 上编写无操作系统的状态机,通常依赖定时器中断或外部中断来驱动状态切换。然而,默认的 Arduino 核心库(如 AVR 的 wiring.c)对中断的处理可能引入额外开销,导致中断向量表跳转延迟增大,影响实时性。GCC 提供了多种函数属性,允许开发者精细控制中断处理函数的生成方式,从而优化跳转延迟。本文将聚焦于 AVR 架构(如 ATmega328P)和 ARM Cortex-M(如 SAMD21)上的实践,但原理通用。

原理剖析:中断向量表与跳转延迟

中断向量表是处理器在响应中断时跳转的入口地址列表。在 AVR 上,向量表位于 Flash 起始地址,每个向量占用 2 字节(或 4 字节,取决于器件)。默认情况下,Arduino 核心库使用 ISR() 宏定义中断服务函数,该宏会展开为带有 __attribute__((signal)) 的函数,GCC 会为其生成保存所有寄存器的序言和恢复的尾声,并自动处理 reti 指令。这种通用处理保证了正确性,但增加了延迟。

跳转延迟主要由两部分组成:

  1. 向量表到 ISR 的跳转:AVR 上,向量表存储的是 jmp 指令(对于 >8KB Flash 的器件),跳转目标由链接器解析。
  2. ISR 内部的保存/恢复开销signal 属性会保存所有工作寄存器,即使 ISR 未使用全部寄存器。

优化思路:使用 __attribute__((interrupt)) 替代 signal,并配合 noinlinesection 属性,减少不必要的寄存器保存,或将 ISR 放置在特定地址以缩短跳转距离。

配置步骤:以 AVR 为例

1. 定义状态机结构

首先,我们设计一个简单的状态机,由定时器中断驱动。

typedef enum {
    STATE_IDLE,
    STATE_RUN,
    STATE_DONE
} State;

volatile State currentState = STATE_IDLE;
volatile uint32_t stateCounter = 0;

2. 自定义中断处理函数

不使用 Arduino 的 ISR() 宏,而是直接定义函数并指定属性。

// 在 AVR 上,TIMER1_COMPA_vect 是向量名
void __attribute__((interrupt, noinline)) TIMER1_COMPA_vect(void) {
    // 状态机切换逻辑
    switch (currentState) {
        case STATE_IDLE:
            currentState = STATE_RUN;
            stateCounter = 0;
            break;
        case STATE_RUN:
            stateCounter++;
            if (stateCounter >= 1000) {
                currentState = STATE_DONE;
            }
            break;
        case STATE_DONE:
            // 停止定时器
            TCCR1B = 0;
            break;
    }
}

注意:interrupt 属性在 AVR 上等同于 signal,但在 ARM 上不同。对于 AVR,更关键的是 noinlinesection

3. 将 ISR 放入特定段以优化跳转

默认情况下,链接器会将所有代码放在 .text 段。我们可以将 ISR 放入 .text 的起始区域,靠近向量表,从而减少 jmp 指令的跳转距离(AVR 的 jmp 是 32 位指令,但跳转目标在 128KB 内,距离不影响周期,但某些 AVR 有 rjmp 短跳转,仅限 ±4KB,如果 ISR 放在附近,编译器可能生成 rjmp,节省 Flash 空间,但延迟相同)。实际上,延迟主要来自 ISR 内部的保存/恢复。

使用 section 属性:

void __attribute__((interrupt, noinline, section(".text.fast_irq"))) TIMER1_COMPA_vect(void) {
    // ...
}

然后,在链接脚本中(或通过 __attribute__((used)) 防止被优化掉)确保该段被放置在低地址。对于 Arduino IDE,可以修改 boards.txt 或使用 __attribute__((address))(但 AVR 不支持)。更简单的方法是使用 __attribute__((naked)) 手动编写汇编,但风险高。

4. 手动控制寄存器保存

interrupt 属性在 AVR 上会保存所有被使用的寄存器,但编译器可能过度保存。我们可以使用 naked 属性,手动编写保存/恢复和 reti,但必须确保正确。

void __attribute__((naked)) TIMER1_COMPA_vect(void) {
    __asm__ __volatile__(
        "push r24\n\t"
        "in r24, SREG\n\t"
        "push r24\n\t"
        // 保存其他需要使用的寄存器
        // ... 状态机逻辑(用内联汇编或调用函数)
        "pop r24\n\t"
        "out SREG, r24\n\t"
        "pop r24\n\t"
        "reti\n\t"
    );
}

但这种方法可读性差,且容易出错。对于大多数场景,interrupt + noinline 已足够。

完整代码示例:优化后的状态机

以下是一个完整的 Arduino 程序,使用定时器 1 比较匹配中断驱动状态机,并应用优化属性。

// 状态机定义
typedef enum {
    STATE_IDLE,
    STATE_RUN,
    STATE_DONE
} State;

volatile State currentState = STATE_IDLE;
volatile uint32_t stateCounter = 0;

// 优化中断处理函数
void __attribute__((interrupt, noinline)) TIMER1_COMPA_vect(void) {
    switch (currentState) {
        case STATE_IDLE:
            currentState = STATE_RUN;
            stateCounter = 0;
            // 设置比较值,例如 1ms @ 16MHz, prescaler 64
            OCR1A = 249;
            break;
        case STATE_RUN:
            stateCounter++;
            if (stateCounter >= 1000) {
                currentState = STATE_DONE;
                TCCR1B = 0; // 停止定时器
            }
            break;
        case STATE_DONE:
            // 可以在此处处理完成事件
            break;
    }
}

void setup() {
    // 配置定时器1
    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10); // CTC模式, 64分频
    OCR1A = 249; // 1ms @ 16MHz
    TIMSK1 = (1 << OCIE1A); // 使能比较匹配中断
    sei(); // 全局中断使能
}

void loop() {
    // 主循环可以处理其他任务,状态机由中断驱动
    if (currentState == STATE_DONE) {
        // 输出结果
        digitalWrite(LED_BUILTIN, HIGH);
        delay(1000);
        digitalWrite(LED_BUILTIN, LOW);
        currentState = STATE_IDLE; // 重置状态机
        TCCR1B = (1 << WGM12) | (1 << CS11) | (1 << CS10); // 重新启动定时器
    }
}

注意事项

  • 属性兼容性interrupt 属性在 AVR 和 ARM 上含义不同。在 AVR 上,它等同于 signal,但在 ARM Cortex-M 上,interrupt 会生成 __attribute__((isr)) 类似的行为,但建议使用 __attribute__((interrupt))__attribute__((naked)) 时查阅编译器文档。
  • 寄存器保存:使用 interrupt 属性时,编译器会分析 ISR 中实际使用的寄存器,并只保存这些,这比 signal 保存所有寄存器更高效。但若 ISR 调用其他函数,则可能保存更多。
  • noinline 的必要性:防止编译器将 ISR 内联到其他函数,导致属性丢失。
  • 链接器优化:将 ISR 放入特定段可能影响链接,需要确保段被正确放置在 Flash 中。在 Arduino IDE 中,可以通过修改 platform.txt 或使用 __attribute__((section(".text"))) 但通常默认即可。
  • 测量延迟:使用逻辑分析仪或示波器测量 GPIO 翻转时间,对比优化前后差异。
  • 可移植性:这些技巧主要针对 AVR,对于 ARM 架构,中断向量表在 RAM 中可重定位,延迟优化更多依赖于 NVIC 配置和编译器生成代码的效率。

总结

通过使用 GCC 属性(如 interruptnoinlinesection),我们可以减少 Arduino 上中断服务函数的跳转延迟,从而提升无 OS 状态机的实时性。关键在于理解编译器如何生成中断代码,并针对性地减少不必要的开销。对于大多数应用,interrupt + noinline 已能获得显著改善,而 naked 适合极端优化场景。掌握这些技巧,能让嵌入式开发者在资源受限的平台上榨出更多性能。