中断系统:NVIC配置、中断优先级、外部中断EXTI、中断服务函数编写
中断,说白了就是CPU正在干一件事,突然被更紧急的事打断,跑去处理完再回来接着干。我刚开始学嵌入式时,觉得中断就是个“高级点的轮询”。后来踩了坑才明白——中断用不好,系统直接崩给你看。
这一章,咱们把中断系统拆开揉碎了讲。从NVIC怎么配,到优先级怎么设,再到外部中断EXTI和中断服务函数怎么写。嗯,都是干货。
1. NVIC:中断的“调度中心”
NVIC,全称Nested Vectored Interrupt Controller,嵌套向量中断控制器。名字挺长,功能其实就两个:管理中断的使能与屏蔽,以及分配中断优先级。
在Cortex-M内核里,NVIC是内核的一部分。你写BSP时,第一步就是跟它打交道。
1.1 NVIC的使能与屏蔽
每个中断源都有一个对应的使能位。你把它置1,中断才能被CPU响应。反之,清零就屏蔽。
我习惯用标准库的NVIC_EnableIRQ()和NVIC_DisableIRQ()。直接操作寄存器也行,但容易写错位号。
// 使能USART1中断
NVIC_EnableIRQ(USART1_IRQn);
// 屏蔽USART1中断
NVIC_DisableIRQ(USART1_IRQn);
注意:IRQn这个枚举值,每个芯片都不一样。你得查芯片手册,别想当然。
1.2 中断优先级分组
Cortex-M支持最多256级优先级,但具体芯片会裁剪。比如STM32F1只支持4位优先级,也就是16级。
这4位还能拆成“抢占优先级”和“子优先级”。怎么拆?靠NVIC_PriorityGroupConfig()。
| 分组 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| 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. 中断优先级:别让“抢”变成“乱”
优先级设置,核心就两条规则:
- 抢占优先级高的中断,可以打断抢占优先级低的。
- 抢占优先级相同的中断,谁先来谁先服务,不能互相打断。
子优先级只在抢占优先级相同时才起作用——决定谁先执行。
举个例子:
// 配置分组为Group2:4级抢占,4级子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 中断A:抢占优先级1,子优先级0
NVIC_SetPriority(TIM2_IRQn, 1 << 2 | 0); // 实际值 = 0x04
// 中断B:抢占优先级0,子优先级3
NVIC_SetPriority(USART1_IRQn, 0 << 2 | 3); // 实际值 = 0x03
这里中断B的抢占优先级更高(0比1高),所以B可以打断A。如果A和B抢占优先级相同,才看子优先级。
3. 外部中断EXTI:从引脚到CPU的“快车道”
EXTI,External Interrupt,外部中断。它负责把GPIO引脚上的电平变化,转成中断信号送给NVIC。
配置EXTI,一般分三步:
- 配置GPIO为输入模式,并选择触发边沿(上升沿、下降沿或双边沿)。
- 配置EXTI线,把GPIO引脚映射到对应的EXTI线。
- 配置NVIC,使能对应的中断通道。
以STM32F1为例,PA0映射到EXTI0线:
// 1. 配置GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 2. 配置EXTI线(HAL库自动完成映射)
// 3. 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
注意:EXTI0线只能对应PA0、PB0、PC0……同一时间只能有一个引脚映射到同一根EXTI线。你想想看,如果PA0和PB0都配成EXTI0,那到底谁触发?芯片会乱掉的。
4. 中断服务函数:别在里面“磨洋工”
中断服务函数,ISR(Interrupt Service Routine),是中断的“终点站”。CPU跳进来,处理完,再跳回去。
写ISR,我有几条铁律:
- 快进快出:ISR里别做复杂计算,别调printf,别等延时。你想想看,ISR执行时间越长,其他中断被堵的时间就越长。
- 清除中断标志:不清标志,中断会反复触发,系统直接卡死。我见过有人忘了清EXTI的挂起位,结果CPU一直在ISR里打转。
- 尽量只设标志位:真正的处理逻辑放到主循环或任务里。ISR只负责“通知”,不负责“干活”。
一个典型的EXTI中断服务函数:
void EXTI0_IRQHandler(void)
{
// 检查中断标志
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 设置标志位,通知主循环处理
g_button_pressed = 1;
// 清除中断标志
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
注意:函数名是固定的,不能自己乱起。比如EXTI0的中断服务函数就叫EXTI0_IRQHandler。你写错了,编译器不会报错,但中断永远进不去。
5. 中断系统的整体流程
说了这么多,咱们用一张图把整个流程串起来:
从图里能看出来,整个中断链路是串行的。任何一个环节出问题,中断就废了。我调试时,习惯用逻辑分析仪抓GPIO引脚,看中断有没有被正确触发。嗯,这招比看代码管用多了。
6. 总结一下
中断系统,说白了就是三个东西:NVIC管调度,EXTI管检测,ISR管处理。你把这三点理清楚,BSP的中断部分就稳了。
最后再啰嗦一句:中断优先级不是越高越好。你把所有中断都设成最高优先级,那跟没设优先级一样。合理的做法是:关键中断(比如系统滴答定时器)优先级最高,普通外设中断次之,非关键中断(比如按键)优先级最低。