4. 中断系统与NVIC:中断优先级分组、外部中断EXTI配置、中断服务函数编写规范、中断嵌套与抢占优先级实战
中断,说白了就是CPU正在干活,突然被一个紧急事件叫停,转去处理这个事件,处理完再回来接着干。我刚开始学MCU时,觉得中断就是个“高级点的轮询”。后来在项目中吃过亏才明白——中断用不好,系统随时可能崩。
这一章,咱们把中断系统彻底讲透。从NVIC的优先级分组,到外部中断EXTI的配置,再到中断服务函数的写法规范,最后用实战代码演示中断嵌套。嗯,内容不少,但都是硬货。
4.1 NVIC与中断优先级分组
NVIC(Nested Vectored Interrupt Controller),嵌套向量中断控制器。它是Cortex-M内核的中断管家。所有中断的使能、挂起、优先级,都由它管。
我个人习惯,拿到一个新MCU,第一件事就是看它的NVIC支持多少中断源。STM32F103有68个可屏蔽中断通道,F407有82个。这个数字决定了你的系统能挂多少外设中断。
4.1.1 优先级分组:抢占优先级 vs 子优先级
ARM Cortex-M的中断优先级分两组:抢占优先级和子优先级。这个设计,我当年刚接触时觉得挺绕。后来用多了,发现它其实很巧妙。
| 概念 | 作用 | 类比 |
|---|---|---|
| 抢占优先级 | 决定能否打断正在执行的中断 | 领导级别:总经理可以打断部门经理 |
| 子优先级 | 同抢占优先级下的响应顺序 | 同级别领导:按资历排队 |
抢占优先级高的中断,可以打断抢占优先级低的中断。子优先级只在同抢占优先级时起作用——谁的数字小,谁先响应。
核心规则:抢占优先级决定嵌套能力,子优先级决定同级的排队顺序。两者互不干扰。
4.1.2 优先级分组配置
STM32用NVIC_SetPriorityGrouping()函数来配置分组。它决定了抢占优先级和子优先级各占多少位。以STM32F4为例,优先级寄存器共4位(0-3),分组方式如下:
| 分组编号 | 抢占优先级位数 | 子优先级位数 | 抢占优先级取值范围 | 子优先级取值范围 |
|---|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 无 | 0-15 |
| NVIC_PriorityGroup_1 | 1 | 3 | 0-1 | 0-7 |
| NVIC_PriorityGroup_2 | 2 | 2 | 0-3 | 0-3 |
| NVIC_PriorityGroup_3 | 3 | 1 | 0-7 | 0-1 |
| NVIC_PriorityGroup_4 | 4 | 0 | 0-15 | 无 |
我建议,大多数项目用NVIC_PriorityGroup_2。为什么?4个抢占优先级够用,4个子优先级也能满足同级的排队需求。我在一个电机控制项目里用过Group_4,结果发现16个抢占优先级根本用不完,反而把子优先级砍没了,调试时很被动。
我的习惯:系统初始化时只调用一次NVIC_SetPriorityGrouping(),整个项目周期不改。中途改分组,所有中断的优先级含义都会变,容易出bug。
4.2 外部中断EXTI配置
EXTI(External Interrupt/Event Controller),外部中断/事件控制器。它负责把GPIO引脚上的电平变化,转化成中断请求送给NVIC。
配置EXTI,说白了就是三步:选引脚、配触发方式、连NVIC。我见过不少新手在这三步里踩坑,尤其是引脚映射那一步。
4.2.1 EXTI配置步骤
- 使能GPIO时钟和SYSCFG时钟——EXTI的引脚映射依赖SYSCFG。
- 配置GPIO为输入模式——上拉或下拉根据外部电路定。
- 将GPIO引脚连接到EXTI线——通过SYSCFG_EXTILineConfig()函数。
- 配置EXTI触发方式——上升沿、下降沿或双边沿。
- 使能EXTI中断线——EXTI_IMR寄存器对应位置1。
- 配置NVIC优先级并使能中断——NVIC_EnableIRQ()。
这里有个坑:同一EXTI线只能映射一个GPIO引脚。比如EXTI0线只能映射PA0、PB0、PC0中的一个。我曾经在一个项目里,把PA0和PB0都配成了EXTI0,结果两个引脚的中断互相干扰,查了两天才发现。
注意:EXTI线0-15对应GPIO引脚0-15。PA0用EXTI0,PB1用EXTI1,以此类推。同一个EXTI线号,只能选一个引脚。
4.2.2 代码示例:配置PA0为下降沿触发中断
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
// 2. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 连接EXTI线
SYSCFG_EXTILineConfig(EXTI_PortSource_GPIOA, EXTI_PinSource_0);
// 4. 配置EXTI
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line0;
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStruct.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStruct);
// 5. 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStruct);
这段代码,我几乎每个项目都会写一遍。你把它封装成一个函数,比如EXTI_PA0_Init(),以后复用就方便了。
4.3 中断服务函数编写规范
中断服务函数(ISR),是中断系统的灵魂。写得好,系统稳如泰山;写得烂,系统随时死机。我见过最离谱的ISR,里面跑了个while(1)等待外部事件——结果中断再也回不来了。
4.3.1 ISR的黄金法则
- 快进快出——ISR里不要做耗时操作。我一般控制在几十微秒以内。
- 不要调用printf()——printf()可能重入,而且太慢。
- 不要用延时函数——delay()会阻塞整个系统。
- 标志位+主循环——ISR只置标志位,具体处理放主循环。
- 清除中断标志——不清标志,中断会反复触发。
ISR设计原则:ISR只做最紧急的事——读取数据、置标志位、清除中断标志。其余一切,交给主循环或任务调度器。
4.3.2 标准ISR模板
// 中断标志位
volatile uint8_t g_exti0_flag = 0;
void EXTI0_IRQHandler(void)
{
// 1. 检查中断源
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 2. 置标志位(只做最轻量的操作)
g_exti0_flag = 1;
// 3. 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
// 主循环中处理
int main(void)
{
while (1)
{
if (g_exti0_flag)
{
g_exti0_flag = 0;
// 在这里处理按键逻辑、通信等耗时操作
}
}
}
volatile关键字不能少。编译器可能会优化掉对g_exti0_flag的读取,加上volatile告诉编译器“这个变量随时可能被中断修改”。
4.4 中断嵌套与抢占优先级实战
中断嵌套,就是高优先级中断打断低优先级中断。这个机制用好了,系统响应极快;用不好,低优先级中断可能永远得不到执行。
4.4.1 中断嵌套的条件
- 两个中断的抢占优先级不同
- 高抢占优先级的中断触发时,低优先级中断正在执行
- 高优先级中断执行完后,CPU回到低优先级中断继续执行
如果两个中断的抢占优先级相同,即使子优先级不同,也不会发生嵌套。子优先级只在它们同时挂起时决定谁先响应。
4.4.2 实战:演示中断嵌套
我们配两个中断:EXTI0(按键)抢占优先级1,EXTI1(定时器)抢占优先级2。按键中断可以打断定时器中断。
// EXTI0配置(抢占优先级1)
NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
// EXTI1配置(抢占优先级2)
NVIC_InitStruct.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);
当EXTI1正在执行时,如果EXTI0触发,CPU会暂停EXTI1,跳转到EXTI0。EXTI0执行完,再回来继续执行EXTI1。这就是嵌套。
调试技巧:在ISR里翻转GPIO引脚,用示波器看波形。你能直观看到中断嵌套的时序——高优先级中断的脉冲插在低优先级中断的脉冲中间。
4.5 本章知识结构图
下面这张图,把中断系统的核心逻辑串起来了。从外设触发,到NVIC仲裁,再到ISR执行,一目了然。
4.6 避坑指南
中断系统用久了,总会遇到一些奇奇怪怪的问题。我把自己踩过的坑列出来,你遇到了可以直接对照。
坑1:中断标志没清干净
有些外设的中断标志,需要先读状态寄存器再清除。比如USART,要先读SR再写DR,否则标志清不掉。我曾经因为这个,串口中断只进一次就卡死了。
坑2:优先级分组中途修改
项目运行到一半,调用NVIC_SetPriorityGrouping()改分组。所有已配置的中断优先级含义全变了。我建议:上电初始化时一次配好,后面别动。
坑3:ISR里调用可重入函数
printf()、malloc()这些函数,内部可能用了全局变量。中断里调用它们,如果主循环也在用,数据就乱了。我一般只用标志位和环形缓冲区。
我的调试三板斧:1) 示波器看GPIO翻转波形;2) 串口打印中断进入次数;3) 在ISR里累加一个计数器,主循环定期读取。这三个方法,能解决90%的中断问题。
中断系统,是MCU开发的基石。你把它吃透了,后面学定时器、串口、ADC都会轻松很多。嗯,这一章的内容就到这儿。代码该写的都写了,坑该说的都说了。剩下的,就是你自己动手去试了。