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

在 C 语言层面,我们通常不直接处理硬件中断的底层细节(如设置中断向量表、开关中断等),因为这些操作通常是与特定硬件平台(如 ARM, AVR, x86)相关的,并且需要使用汇编语言或特定平台的寄存器操作,C 语言为我们提供了编写中断服务程序的标准框架,以及一些在 C 中与中断相关的关键字和概念。
中断的基本概念
中断源
引发中断的事件来源:
- 外部中断: 来自外部硬件设备。
- 例子: 按键按下、定时器溢出、网络数据到达、ADC 转换完成。
- 内部中断: 由 CPU 内部事件引起。
- 例子: 除零错误、指令执行(如
INT 3用于调试)、系统调用。
- 例子: 除零错误、指令执行(如
中断处理流程
- 中断请求: 中断源向 CPU 发出请求信号。
- 中断响应: CPU 在当前指令执行完毕后,如果允许中断,则响应中断请求。
- 现场保护: CPU 自动(或通过程序)将当前程序的关键信息(如程序计数器 PC、程序状态字 PSW/寄存器)保存到堆栈中,以便之后能正确返回。
- 中断处理: CPU 跳转到预先设定好的中断服务程序 并执行。
- 现场恢复: ISR 执行完毕后,CPU 从堆栈中恢复之前保存的现场信息。
- 中断返回: CPU 回到被中断程序的断点处,继续执行。
在 C 语言中编写中断服务程序
虽然中断的响应由硬件触发,但编写处理中断的代码(即 ISR)通常使用 C 语言,为了告诉编译器一个函数是中断服务程序,编译器需要对其进行特殊处理,
- 使用特定的函数调用/返回指令。
- 自动保存和恢复被中断程序使用的寄存器。
- 禁用某些中断,防止嵌套或重入。
不同的编译器有不同的关键字来声明 ISR。

示例 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 声明方式通常如下:

// 对于 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)来实现无锁编程。
最佳实践和注意事项
- 保持 ISR 简短: ISR 的首要任务是快速响应并退出,不要在 ISR 中执行复杂的计算、I/O 操作或调用可能阻塞的库函数(如
printf,malloc)。 - 最小化临界区: 使用
cli()/sei()来保护共享数据时,临界区应尽可能短,以减少对系统实时性的影响。 - 使用
volatile: 任何被 ISR 和主程序共享的全局变量都必须声明为volatile。 - 处理中断嵌套: 某些架构支持中断嵌套(高优先级中断可以打断低优先级中断),如果需要,可以在 ISR 中再次调用
sei()来开启嵌套,但务必小心处理共享数据和寄存器保存。 - 清除中断标志: 在需要手动清除中断标志的架构(如 Cortex-M)上,ISR 的末尾必须清除相应中断源的挂起位,否则会立即再次进入中断。
- 避免在 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 语言中正确地编写和管理中断,对于构建稳定、高效的系统至关重要。
