C语言interrupt中断如何正确使用与处理?

99ANYc3cd6
预计阅读时长 19 分钟
位置: 首页 C语言 正文

中断是计算机体系结构中一个非常重要的概念,它允许硬件或软件打断 CPU 正在执行的程序流,转而去处理一个更紧急的事件,处理完毕后,CPU 会返回到原来的断点继续执行。

interrupt c语言
(图片来源网络,侵删)

在 C 语言层面,我们通常不直接处理硬件中断的底层细节(如设置中断向量表、开关中断等),因为这些操作通常是与特定硬件平台(如 ARM, AVR, x86)相关的,并且需要使用汇编语言或特定平台的寄存器操作,C 语言为我们提供了编写中断服务程序的标准框架,以及一些在 C 中与中断相关的关键字和概念。


中断的基本概念

中断源

引发中断的事件来源:

  • 外部中断: 来自外部硬件设备。
    • 例子: 按键按下、定时器溢出、网络数据到达、ADC 转换完成。
  • 内部中断: 由 CPU 内部事件引起。
    • 例子: 除零错误、指令执行(如 INT 3 用于调试)、系统调用。

中断处理流程

  1. 中断请求: 中断源向 CPU 发出请求信号。
  2. 中断响应: CPU 在当前指令执行完毕后,如果允许中断,则响应中断请求。
  3. 现场保护: CPU 自动(或通过程序)将当前程序的关键信息(如程序计数器 PC、程序状态字 PSW/寄存器)保存到堆栈中,以便之后能正确返回。
  4. 中断处理: CPU 跳转到预先设定好的中断服务程序 并执行。
  5. 现场恢复: ISR 执行完毕后,CPU 从堆栈中恢复之前保存的现场信息。
  6. 中断返回: CPU 回到被中断程序的断点处,继续执行。

在 C 语言中编写中断服务程序

虽然中断的响应由硬件触发,但编写处理中断的代码(即 ISR)通常使用 C 语言,为了告诉编译器一个函数是中断服务程序,编译器需要对其进行特殊处理,

  • 使用特定的函数调用/返回指令。
  • 自动保存和恢复被中断程序使用的寄存器。
  • 禁用某些中断,防止嵌套或重入。

不同的编译器有不同的关键字来声明 ISR。

interrupt c语言
(图片来源网络,侵删)

示例 1:针对 AVR (Arduino) 的 ISR

AVR 微控制器(如 Arduino Uno 上的 ATmega328P)使用的 avr-libc 提供了 <avr/interrupt.h> 头文件,其中定义了 ISR 宏。

代码示例:配置并响应外部中断 0 (INT0)

#include <avr/io.h>
#include <avr/interrupt.h>
// 定义一个 LED 引脚,假设连接在 PB5 (Arduino Uno 的 onboard LED)
#define LED_PIN PB5
// 中断服务程序
// 当 INT0 (PD2 引脚) 上发生电平变化时,此函数将被调用
ISR(INT0_vect) 
{
    // 在中断服务程序中,尽量保持代码简短!
    // 切换 LED 状态
    if (PORTB & (1 << LED_PIN)) {
        PORTB &= ~(1 << LED_PIN); // LED off
    } else {
        PORTB |= (1 << LED_PIN);  // LED on
    }
}
int main(void)
{
    // 1. 设置 LED 引脚为输出
    DDRB |= (1 << LED_PIN);
    // 2. 设置 INT0 (PD2) 引脚为输入
    DDRD &= ~(1 << PD2);
    // 3. 配置中断控制寄存器
    //    ISC01 = 1, ISC00 = 1 表示 INT0 的下降沿触发
    EICRA |= (1 << ISC01) | (1 << ISC00);
    // 4. 开启 INT0 中断
    EIMSK |= (1 << INT0);
    // 5. 全局开启中断
    sei(); // Set Enable Interrupts
    // 主循环可以执行其他任务,或者保持空转
    while (1) 
    {
        // 主循环中的代码可能会被中断打断
        // ...
    }
    return 0;
}

代码解释:

  • #include <avr/interrupt.h>:包含了 ISR 宏的定义。
  • ISR(INT0_vect):这是声明中断服务程序的核心。INT0_vect 是中断向量表的名称,代表外部中断 0 的中断向量,编译器看到这个宏,就会生成适合 AVR 架构的中断入口代码。
  • sei():一个宏,用于设置全局中断使能位 I,在 sei() 之后,CPU 才会响应可屏蔽的中断。
  • cli():与 sei() 相对,用于关闭全局中断,在关键代码段前后使用 cli()sei() 可以实现临界区,防止代码被中断破坏。

示例 2:针对 ARM Cortex-M 的 __irq__attribute__

ARM Cortex-M 系列微控制器(如 STM32, nRF52, ESP32-S2/S3)使用向量表中断控制器,其 ISR 声明方式通常如下:

interrupt c语言
(图片来源网络,侵删)
// 对于 Keil MDK (ARMCC/ARMCLANG) 编译器
void EXTI0_IRQHandler(void) __irq
{
    // ... 中断处理代码 ...
    // 必须手动清除中断挂起标志位
    EXTI->PR |= EXTI_PR_PR0; // 清除线0的中断挂起位
}
// 对于 GCC 编译器
void EXTI0_IRQHandler(void) __attribute__((interrupt("IRQ")))
{
    // ... 中断处理代码 ...
    // 必须手动清除中断挂起标志位
    EXTI->PR |= EXTI_PR_PR0; // 清除线0的中断挂起位
}

关键点:

  • 函数名必须与链接器脚本中的中断向量名完全匹配(如 EXTI0_IRQHandler)。
  • __irq__attribute__((interrupt("IRQ"))) 告诉编译器这是中断处理函数,需要生成正确的 prologue(入栈)和 epilogue(出栈)指令,以保存和恢复上下文。
  • 与 AVR 不同,在 Cortex-M 上,通常需要在中断服务程序中手动清除中断源的中断挂起标志位,否则中断会一直发生,导致程序进入死循环。

C 语言中与中断相关的关键字和概念

除了编译器特定的关键字,C 语言标准本身也提供了一些与中断和并发控制相关的关键字。

volatile 关键字

这是编写中断相关代码时最重要的关键字!

当一个变量可能被 ISR 修改,同时也被主程序(或其他线程)读取时,这个变量必须声明为 volatile

为什么需要 volatile 编译器为了优化性能,会假设一个变量的值在没有被明确赋值的情况下是不会改变的,如果主程序在一个循环中读取一个变量,编译器可能会“聪明”地只读取一次,然后将它缓存在寄存器中,而不会每次循环都从内存中重新读取。

问题场景:

int flag = 0;
void main() {
    while(1) {
        if (flag == 1) { // 编译器可能只读取一次 flag,然后一直循环
            do_something();
        }
    }
}
ISR(timer_handler) {
    flag = 1; // ISR 在某个时刻会修改 flag
}

如果编译器优化了 while 循环,它可能永远不会发现 flag 变成了 1,导致 do_something() 永远不会被调用。

解决方案:使用 volatile

volatile int flag = 0; // 告诉编译器:这个变量的值可能会被硬件或未知方式改变,不要做优化!
void main() {
    while(1) {
        if (flag == 1) { // 每次循环都会从内存中重新读取 flag 的值
            do_something();
        }
    }
}
ISR(timer_handler) {
    flag = 1;
}

atomic 操作 (C11 及以后)

C11 标准引入了 <stdatomic.h> 头文件,提供了原子操作,用于在多线程或中断环境下安全地访问共享数据,原子操作保证一个操作(如读取、修改、写入)不会被其他线程或中断打断。

示例:原子地递增一个计数器

#include <stdatomic.h>
#include <stdbool.h>
atomic_int g_counter = 0;
volatile bool g_flag = false;
void main() {
    // ...
}
ISR(some_handler) {
    // 原子地递增 g_counter
    atomic_fetch_add(&g_counter, 1); 
    // 原子地设置 g_flag 为 true
    atomic_store(&g_flag, true);
}

原子操作比使用 cli()/sei()volatile 更现代、更高效,因为它可以利用 CPU 提供的硬件指令(如 ARM 的 LDREX/STREX)来实现无锁编程。


最佳实践和注意事项

  1. 保持 ISR 简短: ISR 的首要任务是快速响应并退出,不要在 ISR 中执行复杂的计算、I/O 操作或调用可能阻塞的库函数(如 printf, malloc)。
  2. 最小化临界区: 使用 cli()/sei() 来保护共享数据时,临界区应尽可能短,以减少对系统实时性的影响。
  3. 使用 volatile: 任何被 ISR 和主程序共享的全局变量都必须声明为 volatile
  4. 处理中断嵌套: 某些架构支持中断嵌套(高优先级中断可以打断低优先级中断),如果需要,可以在 ISR 中再次调用 sei() 来开启嵌套,但务必小心处理共享数据和寄存器保存。
  5. 清除中断标志: 在需要手动清除中断标志的架构(如 Cortex-M)上,ISR 的末尾必须清除相应中断源的挂起位,否则会立即再次进入中断。
  6. 避免在 ISR 中调用非可重入函数: 如果一个函数使用了静态变量或全局变量,它就是不可重入的,ISR 和主线程都可能调用它,就会导致数据混乱。printf 就是一个典型的不可重入函数。
概念 描述 C 语言实现/示例
中断服务程序 处理中断事件的函数。 ISR(INT0_vect) (AVR) 或 void EXTI0_IRQHandler(void) __attribute__((interrupt("IRQ"))) (ARM)
volatile 防止编译器对变量进行优化,确保每次都从内存读取。 volatile int shared_var;
cli() / sei() 关闭/开启全局中断,用于保护临界区。 cli(); // 关闭中断
sei(); // 开启中断
atomic C11 引入,提供无锁的原子操作,安全访问共享数据。 atomic_fetch_add(&counter, 1);
中断向量 中断发生时,CPU 跳转到的地址表。 由链接器脚本和编译器共同管理,ISR 函数名需与向量名匹配。

理解中断是进行嵌入式系统开发的基础,掌握如何在 C 语言中正确地编写和管理中断,对于构建稳定、高效的系统至关重要。

-- 展开阅读全文 --
头像
C语言中getline函数如何正确使用?
« 上一篇 今天
C语言如何实现十进制转二进制?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]