1. 中断基础概念:中断的定义、中断向量表、中断优先级与嵌套
大家好,欢迎来到这门实战课。今天咱们先聊聊中断——这个嵌入式系统里最基础、也最容易出坑的概念。
我刚开始做嵌入式那会儿,总觉得中断就是个“后台任务”。后来在一个电机控制项目里,中断响应慢了 3 微秒,电机直接抖成了筛子。嗯,从那以后我再也不敢小看它了。
1.1 中断到底是什么?
说白了,中断就是 CPU 正在干一件事,突然被一个紧急信号打断,转去处理更紧急的事,处理完再回来接着干。
你想想看,这就像你正在写代码,领导突然喊你开会。你保存当前进度(压栈),去开会(执行 ISR),开完会回来继续写(出栈)。
中断的核心三要素:
- 触发源:谁发的中断?定时器、GPIO、UART、DMA……
- 中断服务函数(ISR):CPU 跳过去执行的那段代码
- 返回机制:执行完 ISR 后,CPU 怎么回到原来的位置
我在项目中遇到过一种情况:有人把 ISR 写得特别长,里面还调了 printf。结果中断一触发,CPU 半天出不来,主循环直接“假死”。记住,ISR 要短、快、狠——进去就干活,干完就出来。
1.2 中断向量表——CPU 的“紧急通讯录”
中断向量表,其实就是一张表。CPU 收到中断信号后,查这张表,找到对应的 ISR 入口地址,然后跳过去执行。
这张表通常放在 Flash 或 RAM 的固定位置。比如 ARM Cortex-M 系列,向量表默认从地址 0x00000000 开始。
| 向量偏移 | 中断源 | ISR 入口地址 |
|---|---|---|
| 0x0000 | 复位 | Reset_Handler |
| 0x0004 | NMI | NMI_Handler |
| 0x0008 | 硬错误 | HardFault_Handler |
| 0x002C | SysTick | SysTick_Handler |
| 0x0040 | UART1 | UART1_IRQHandler |
小技巧: 调试阶段,我习惯把未使用的中断向量指向一个“死循环”函数。这样一旦有意外中断触发,CPU 会卡住,方便定位问题。我曾经靠这个抓到一个 GPIO 引脚虚焊导致的误触发中断。
来看一段典型的向量表定义代码(以 STM32 为例):
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
&__StackTop, // 0x0000: 栈顶指针
Reset_Handler, // 0x0004: 复位
NMI_Handler, // 0x0008: NMI
HardFault_Handler, // 0x000C: 硬错误
// ... 其他中断
SysTick_Handler, // SysTick 中断
UART1_IRQHandler, // UART1 中断
// ...
};
注意看,向量表第一个条目不是中断函数,而是栈顶指针。这是 CPU 启动时用来初始化 SP 寄存器的。很多人刚学的时候会忽略这个,结果程序一跑就飞了。
1.3 中断优先级与嵌套——谁更急?谁先来?
多个中断同时来了怎么办?谁先执行?这就涉及到优先级了。
优先级高的中断可以打断优先级低的中断——这叫嵌套。但优先级相同的中断,通常不能互相打断。
注意: 中断嵌套不是越多越好。嵌套层数太多,栈空间可能溢出。我见过一个项目,嵌套了 5 层中断,结果栈溢出导致系统随机崩溃,查了整整一周才定位到。
常见的优先级模型有两种:
- 固定优先级:每个中断的优先级是固定的,不能改。比如 8051 系列。
- 可编程优先级:软件可以配置每个中断的优先级。ARM Cortex-M 的 NVIC 就是典型。
Cortex-M 的优先级配置很灵活。它把优先级分为抢占优先级和子优先级:
| 优先级分组 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 无抢占,只有子优先级 |
| NVIC_PriorityGroup_1 | 1 | 3 | 1 位抢占,3 位子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 常用配置,2 位抢占,2 位子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 3 位抢占,1 位子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 4 位抢占,无子优先级 |
配置代码也很简单:
// 设置优先级分组:2 位抢占优先级,2 位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置 UART1 中断:抢占优先级 1,子优先级 0
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
这里有个坑:优先级数值越小,优先级越高。也就是说,抢占优先级 0 比 1 更急。我刚开始总搞反,把最重要的中断配成了 15,结果它老被别的打断。
1.4 中断处理流程——一张图说清楚
下面这张图展示了从中断触发到返回的完整流程:
流程其实就五步:
- 主程序正常执行——CPU 正在跑你的 while(1) 循环
- 中断发生——外设拉高了中断请求线
- CPU 自动压栈——保存当前任务的上下文(PC、LR、寄存器等)
- 查向量表,跳转 ISR——找到对应的中断服务函数入口
- 执行 ISR,出栈返回——恢复现场,继续执行主程序
避坑指南: 我曾经在一个项目中,ISR 里修改了全局变量,但没加 volatile 修饰。结果编译器优化后,主循环永远读不到更新后的值。记住:中断和主循环共享的变量,一定要加 volatile。
1.5 中断延迟——你躲不开的性能指标
中断延迟,就是从中断触发到 CPU 开始执行 ISR 第一条指令的时间。这个时间越短,系统的实时性越好。
中断延迟的主要来源:
- CPU 正在执行高优先级中断——得等它执行完
- CPU 正在执行关中断的临界区代码——得等临界区退出
- CPU 正在访问不可打断的指令——比如多周期乘法指令
- 压栈时间——保存现场需要几个时钟周期
拿 Cortex-M4 来说,从中断触发到执行 ISR 第一条指令,通常需要 12 个时钟周期左右。如果你跑 100MHz,那就是 120ns。嗯,这个数字你要记在心里——它是你评估系统实时性的基准。
注意: 不要在 ISR 里做浮点运算!Cortex-M4 虽然支持硬件浮点,但浮点寄存器的压栈/出栈开销很大。我见过有人把 PID 计算放在 ISR 里,结果中断延迟直接飙到几十微秒。把浮点运算放到主循环或任务里去。
好了,中断的基础概念就聊到这儿。记住三个关键词:向量表、优先级、嵌套。后面几章我们会深入分析中断延迟的测量方法和优化技巧。