中断系统: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级抢占,无子优先级
我的习惯:项目初期就定好分组,整个工程只调用一次NVIC_PriorityGroupConfig()。中途改分组,所有中断的优先级都得重算,容易出bug。

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抢占优先级相同,才看子优先级。

我曾经踩过的坑:把抢占优先级设得一模一样,结果两个中断同时触发时,系统响应时间变得不可预测。后来我强制要求:关键中断(比如定时器、DMA)的抢占优先级必须不同。

3. 外部中断EXTI:从引脚到CPU的“快车道”

EXTI,External Interrupt,外部中断。它负责把GPIO引脚上的电平变化,转成中断信号送给NVIC。

配置EXTI,一般分三步:

  1. 配置GPIO为输入模式,并选择触发边沿(上升沿、下降沿或双边沿)。
  2. 配置EXTI线,把GPIO引脚映射到对应的EXTI线。
  3. 配置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,那到底谁触发?芯片会乱掉的。

关键点:EXTI支持“事件”和“中断”两种模式。事件只产生脉冲,不触发CPU中断,适合唤醒外设。中断则直接进CPU。我一般用中断模式,事件模式用得少。

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。你写错了,编译器不会报错,但中断永远进不去。

我的习惯:在ISR开头加一个计数器,每次进入加1。调试时通过串口或调试器查看这个计数器,就能知道中断有没有漏掉。

5. 中断系统的整体流程

说了这么多,咱们用一张图把整个流程串起来:

中断系统整体流程 外部事件(按键/传感器) EXTI(边沿检测) NVIC(优先级仲裁) CPU(执行ISR) 返回主程序 关键点 1. 外部事件触发EXTI 2. EXTI产生中断请求 3. NVIC根据优先级裁决 4. CPU执行ISR并清标志 避坑提醒 ISR里别做复杂操作 一定要清中断标志

从图里能看出来,整个中断链路是串行的。任何一个环节出问题,中断就废了。我调试时,习惯用逻辑分析仪抓GPIO引脚,看中断有没有被正确触发。嗯,这招比看代码管用多了。

6. 总结一下

中断系统,说白了就是三个东西:NVIC管调度,EXTI管检测,ISR管处理。你把这三点理清楚,BSP的中断部分就稳了。

最后再啰嗦一句:中断优先级不是越高越好。你把所有中断都设成最高优先级,那跟没设优先级一样。合理的做法是:关键中断(比如系统滴答定时器)优先级最高,普通外设中断次之,非关键中断(比如按键)优先级最低。