2. 中断控制器基础:中断向量表、中断优先级、中断嵌套、中断屏蔽与使能、中断响应流程
好,咱们进入中断控制器的核心内容。说实话,中断控制器这玩意儿,我刚开始做嵌入式那会儿,总觉得它就是个“信号中转站”,没啥技术含量。直到有一次,我在一个工业控制项目里,因为中断优先级没配好,导致系统在高负载下频繁死机……嗯,从那以后,我再也不敢小看它了。
今天咱们就把中断控制器的几个关键概念掰开揉碎了讲。你想想看,CPU 是个“单线程”的家伙,它一次只能干一件事。但外设们可不管这些,随时都可能“喊”它。这时候,中断控制器就是那个“秘书”,帮 CPU 排好队、分好轻重缓急。
2.1 中断向量表
中断向量表,说白了就是一张“地址映射表”。每个中断源都有一个唯一的编号,叫中断号。CPU 收到中断后,会根据这个中断号,去向量表里找到对应的处理函数地址,然后跳过去执行。
我个人习惯把向量表理解成一个函数指针数组。比如在 ARM Cortex-M 系列里,向量表通常放在 Flash 的起始地址(0x00000000)。
// 伪代码示意:中断向量表
typedef void (*isr_func_t)(void);
isr_func_t vector_table[256] = {
&Reset_Handler, // 0: 复位
&NMI_Handler, // 1: 不可屏蔽中断
&HardFault_Handler, // 2: 硬件错误
&MemManage_Handler, // 3: 内存管理
&BusFault_Handler, // 4: 总线错误
&UsageFault_Handler, // 5: 用法错误
// ... 后面是外设中断
&TIM2_IRQHandler, // 28: 定时器2
&USART1_IRQHandler, // 37: 串口1
};
这里有个坑,我必须要提醒你:向量表的地址必须对齐。Cortex-M 要求向量表按 512 字节对齐(也就是地址的低 9 位为 0)。我曾经见过一个同事,把向量表放在 RAM 里做动态重映射,结果忘了对齐,系统一进中断就跑飞了。
2.2 中断优先级
中断优先级,就是给中断排个“座次”。高优先级的中断可以打断低优先级的中断。这个机制在实时系统中至关重要。
不同的芯片对优先级的实现不太一样。我拿最常见的 ARM Cortex-M 来举例。它支持 8~256 级优先级(具体看芯片实现),而且有个很巧妙的设计:抢占优先级和子优先级。
| 优先级分组 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 只有子优先级,无抢占 |
| NVIC_PriorityGroup_1 | 1 | 3 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16级抢占,无子优先级 |
你可能会问:“为什么要分这么细?” 其实就是为了灵活。比如,你可以把定时器中断设为最高抢占优先级,保证它永远不被其他中断打断。而把串口中断设为低优先级,允许它被其他更紧急的中断打断。
2.3 中断嵌套
中断嵌套,就是高优先级中断打断低优先级中断的执行。这个机制让系统能及时响应紧急事件。
举个例子:假设系统正在处理串口接收中断(优先级 5),这时候定时器中断(优先级 1)来了。因为定时器优先级更高,CPU 会暂停串口中断处理,先去执行定时器中断。等定时器中断处理完了,再回来继续处理串口中断。
但这里有个问题:嵌套深度。如果中断嵌套太深,栈空间可能会被耗尽。我记得在一个项目中,我用了 3 层嵌套,结果栈溢出导致系统崩溃。排查了好久才发现是栈大小没配够。
// 中断嵌套的栈使用示意
void USART1_IRQHandler(void) {
// 进入时,栈已使用 100 字节
// 处理中...
// 此时 TIM2 中断发生,CPU 自动压栈
// 栈使用增加到 200 字节
// TIM2 处理完返回,栈恢复到 100 字节
// 继续处理 USART1
}
2.4 中断屏蔽与使能
中断屏蔽,就是暂时“关掉”某个中断。使能就是“打开”它。这个操作在驱动开发中太常见了。
在 Cortex-M 上,中断使能/屏蔽是通过 NVIC 的寄存器来控制的。每个中断对应一个 bit。
// 使能中断
NVIC_EnableIRQ(USART1_IRQn); // 设置 NVIC->ISER[0] 的对应位
// 屏蔽中断
NVIC_DisableIRQ(USART1_IRQn); // 设置 NVIC->ICER[0] 的对应位
// 设置挂起(软件触发中断)
NVIC_SetPendingIRQ(USART1_IRQn); // 设置 NVIC->ISPR[0]
// 清除挂起
NVIC_ClearPendingIRQ(USART1_IRQn); // 设置 NVIC->ICPR[0]
这里有个细节:中断屏蔽和中断挂起是两回事。屏蔽只是不让 CPU 响应,但中断请求信号仍然会被记录下来(挂起位会置 1)。等你重新使能中断后,CPU 会立即响应这个挂起的中断。
我曾经犯过一个错误:在临界区里屏蔽了中断,但忘了清除挂起位。结果退出临界区后,中断一下子涌进来,系统直接卡死。所以,在操作中断屏蔽时,一定要考虑挂起位的处理。
uint32_t primask = __get_PRIMASK(); // 保存当前中断状态
__disable_irq(); // 屏蔽所有中断
// ... 临界区代码 ...
__set_PRIMASK(primask); // 恢复之前的状态
这样能避免因为中断状态改变而导致的 bug。
2.5 中断响应流程
最后,咱们把整个流程串起来。一个完整的中断响应,大致分这么几步:
- 中断请求:外设产生中断信号,发送给中断控制器。
- 中断仲裁:中断控制器根据优先级,决定哪个中断先被响应。
- CPU 响应:CPU 完成当前指令后,检测到中断请求。
- 自动压栈:CPU 自动将当前程序的上下文(PC、LR、PSR、R0-R3、R12)压入栈中。
- 向量读取:CPU 从中断向量表中读取对应中断号的处理函数地址。
- 跳转执行:CPU 跳转到中断服务函数(ISR)执行。
- 中断返回:ISR 执行完毕后,执行中断返回指令(如 BX lr),CPU 自动出栈恢复上下文。
在 Cortex-M 上,这个过程是硬件自动完成的,非常高效。但有一点要注意:中断延迟。从外设发出中断请求,到 CPU 开始执行 ISR 的第一条指令,这个时间就是中断延迟。
影响中断延迟的因素有:
- 当前指令的执行时间(比如正在执行除法或浮点运算)
- 中断屏蔽状态
- 更高优先级中断的存在
- 总线访问延迟(比如正在访问低速外设)
好了,中断控制器的基础就讲到这里。这些东西看起来简单,但实际项目中处处是坑。下一节咱们会深入具体的中断控制器驱动配置,到时候我会拿实际代码来演示。记住一句话:中断配置无小事,一个优先级设错,可能整个系统就崩了。