1. ISR 基础概念:中断向量表、中断优先级、中断嵌套
各位同学,咱们今天开篇先聊点最基础的东西。中断服务程序(ISR),说白了就是硬件在紧急情况下喊你起来干活的一种机制。我做了十几年嵌入式,见过太多因为中断没处理好导致系统崩溃的案例。嗯,咱们先把地基打牢。
1.1 中断向量表——硬件的“紧急通讯录”
中断向量表是什么?我习惯把它想象成一本电话簿。每个中断源都有一个唯一的编号,叫中断号。CPU 收到中断信号后,就去查这个表,找到对应的处理函数地址,然后跳过去执行。
举个例子,ARM Cortex-M 系列的中断向量表,前 16 个是系统异常,后面才是外设中断。你想想看,如果这个表被破坏了,CPU 就不知道该往哪儿跳,系统直接死给你看。
核心要点:中断向量表必须放在正确的位置,且不能被意外修改。我在项目中遇到过,有人不小心把向量表地址写错了,结果一按按键就死机,查了两天才找到原因。
典型的向量表结构如下:
// ARM Cortex-M 向量表示例
__attribute__((section(".isr_vector")))
void (* const g_pfnVectors[])(void) = {
&__StackTop, // 0: 栈顶指针
Reset_Handler, // 1: 复位
NMI_Handler, // 2: NMI
HardFault_Handler, // 3: 硬错误
// ... 系统异常 ...
TIM2_IRQHandler, // 28: 定时器2中断
USART1_IRQHandler, // 37: 串口1中断
// ... 其他外设中断 ...
};
这里要注意,向量表的起始地址通常由链接脚本指定。我个人习惯在链接脚本里加上对齐约束,确保向量表不会跑偏。
警告:千万不要在运行时动态修改向量表!除非你明确知道自己在做什么。我曾经见过有人为了“优化性能”在运行时改向量表,结果触发了一个极其隐蔽的 race condition,调试了整整一周。
1.2 中断优先级——谁更紧急谁先上
中断优先级,说白了就是给中断排个队。高优先级的中断可以打断低优先级的中断。这个机制在实时系统中至关重要。
不同的处理器架构,优先级管理方式不一样。我拿最常见的两种来对比:
| 特性 | ARM Cortex-M (NVIC) | RISC-V (PLIC) |
|---|---|---|
| 优先级位数 | 4-8 位可配置 | 通常 3 位 |
| 优先级值含义 | 数值越小优先级越高 | 数值越大优先级越高 |
| 抢占优先级 vs 子优先级 | 支持分组配置 | 不支持分组 |
| 硬件自动压栈 | 支持 | 通常不支持 |
你可能会问,优先级设多少合适?我的经验是:时间敏感的中断设高优先级,数据量大的中断设低优先级。比如,电机控制的中断必须设最高,因为错过一个周期电机就抖了;而串口接收中断可以设低一些,反正有 FIFO 兜底。
小技巧:在 Cortex-M 上,我习惯把优先级分组设为 4 位抢占 + 4 位子优先级。这样既能保证关键中断的实时性,又能给同优先级的中断留出排队空间。
1.3 中断嵌套——高优先级打断低优先级
中断嵌套,就是高优先级中断在执行过程中,被更高优先级的中断打断。这个机制用好了是神器,用不好是灾难。
咱们画个图来理解这个过程:
从图上可以看得很清楚:高优先级中断 ISR_H 打断了正在执行的 ISR_L。等 ISR_H 跑完了,ISR_L 才继续。这就是中断嵌套的核心逻辑。
但是,嵌套不是越多越好。我见过有人把中断嵌套搞了 5 层,结果栈空间直接爆了。你想想看,每嵌套一层,CPU 就要压一次栈。如果栈深度不够,系统就挂了。
避坑指南:我曾经在一个项目中,把中断嵌套层数设到了 4 层。结果系统运行几小时后随机死机。最后发现是栈溢出。从那以后,我严格控制嵌套层数不超过 3 层,并且给每个 ISR 单独分配栈空间。
1.4 中断响应的关键时序
中断响应时间,就是从硬件拉高中断信号到 CPU 开始执行 ISR 第一条指令的时间。这个时间由两部分组成:
- 硬件延迟:CPU 完成当前指令、压栈、查向量表的时间。通常是固定的几个时钟周期。
- 软件延迟:如果 CPU 正在执行更高优先级的中断,或者全局中断被关闭了,那就得等。
我习惯用逻辑分析仪来测量中断响应时间。具体做法是:在 ISR 入口处拉高一个 GPIO,在 ISR 出口处拉低。然后看中断信号和 GPIO 信号之间的时间差。
// 测量中断响应时间的示例代码
void TIM2_IRQHandler(void) {
// 清除中断标志
TIM2->SR = ~TIM_SR_UIF;
// 测量点:拉高 GPIO
GPIOA->BSRR = GPIO_BSRR_BS0;
// 实际中断处理逻辑
// ...
// 测量点:拉低 GPIO
GPIOA->BSRR = GPIO_BSRR_BR0;
}
经验之谈:如果发现中断响应时间不稳定,多半是因为全局中断被长时间关闭了。我建议在关中断的地方加上调试输出,看看是哪个函数在“霸占”CPU。
1.5 中断优先级分组——别让配置坑了你
ARM Cortex-M 的优先级分组是个容易踩坑的地方。NVIC 支持把优先级位拆分成抢占优先级和子优先级两部分。举个例子:
| 分组方式 | 抢占优先级位数 | 子优先级位数 | 抢占优先级数量 | 子优先级数量 |
|---|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 1 | 16 |
| 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 | 1 |
这里有个坑:分组必须在系统初始化时设置一次,之后不能再改。我见过有人在不同模块里重复调用分组配置,结果后调用的覆盖了前面的,导致优先级乱套。
我的建议:在系统启动代码的最前面,统一设置优先级分组。比如我习惯用 NVIC_PriorityGroup_4,也就是 16 级抢占优先级,没有子优先级。这样逻辑最清晰,不容易出错。