跳转至

中断系统(NVIC 与 EXTI)

中断是嵌入式系统的灵魂。没有中断,CPU 只能不停地轮询检查事件是否发生,效率极低。有了中断,CPU 可以专心做别的事,当事件发生时硬件自动通知 CPU 去处理。


一、什么是中断?

生活中的类比

想象你在写作业(主程序),突然手机响了(中断请求):

  1. 你放下笔,记住写到哪了(保存现场 / 压栈)
  2. 接电话处理事情(执行中断服务函数)
  3. 挂电话,继续写作业(恢复现场 / 出栈)

这就是中断的完整过程。

中断的基本流程

graph LR
    A[主程序运行] --> B{中断请求?}
    B -- 否 --> A
    B -- 是 --> C[保存现场 压栈]
    C --> D[执行中断服务函数 ISR]
    D --> E[恢复现场 出栈]
    E --> A

中断源分类

STM32 的中断源可以分为两大类:

分类 来源 示例
内核中断(异常) Cortex-M3 内核 Reset、NMI、HardFault、SysTick
外部中断 片上外设 EXTI、TIM、USART、ADC、DMA、SPI、I2C

二、NVIC(嵌套向量中断控制器)

NVIC 是什么?

NVIC(Nested Vectored Interrupt Controller)是 Cortex-M 内核的一部分,负责管理所有中断:

  • 决定哪个中断可以打断当前程序
  • 决定多个中断同时发生时谁先执行
  • 管理中断的使能和禁止

中断优先级

STM32 使用 4 位优先级(0~15),分为两个维度:

维度 说明 特点
抢占优先级(Preemption) 高抢占优先级可以打断低抢占优先级的中断 可以嵌套
子优先级(Sub-priority) 抢占优先级相同时,决定谁先响应 不能嵌套,只影响排队顺序

数值越小 = 优先级越高

优先级 0 比优先级 15 更高!这是初学者最容易搞混的地方。

优先级分组

4 位优先级如何分配给抢占和子优先级?通过 优先级分组 来决定:

分组 抢占优先级位数 子优先级位数 抢占级范围 子优先级范围
0 0 位 4 位 0 0~15
1 1 位 3 位 0~1 0~7
2 2 位 2 位 0~3 0~3
3 3 位 1 位 0~7 0~1
4 4 位 0 位 0~15 0

推荐使用分组 2

分组 2(2 位抢占 + 2 位子优先级)是最常用的配置,提供 4 级抢占和 4 级子优先级,够用且灵活。

中断响应规则

当多个中断同时发生时,按以下规则处理:

  1. 抢占优先级不同 → 高抢占优先级先执行(可以打断低优先级的中断)
  2. 抢占优先级相同,子优先级不同 → 子优先级高的先执行(但不能打断)
  3. 两者都相同 → 按中断号(硬件编号)排序,编号小的先执行

举例

假设使用分组 2,有三个中断:

  • 中断 A:抢占 = 1,子 = 0
  • 中断 B:抢占 = 0,子 = 3
  • 中断 C:抢占 = 1,子 = 2

响应顺序:B 优先(抢占最高=0),A 和 C 不能互相打断但 A 先响应(子优先级更高=0)。
若 A 正在执行,B 可以打断 A(抢占优先级更高),但 C 不能打断 A(抢占相同)。


三、EXTI(外部中断/事件控制器)

EXTI 是什么?

EXTI(External Interrupt/Event Controller)用于检测 GPIO 引脚上的电平变化,触发中断或事件。

典型应用:按键按下检测、传感器信号触发、外部脉冲计数。

EXTI 与 GPIO 的映射关系

STM32 有 16 条 EXTI 线(EXTI0 ~ EXTI15),每条线对应一个引脚编号:

EXTI0  ← PA0 / PB0 / PC0 / PD0 ...(同一时刻只能选一个)
EXTI1  ← PA1 / PB1 / PC1 / PD1 ...
EXTI2  ← PA2 / PB2 / PC2 / PD2 ...
...
EXTI15 ← PA15 / PB15 / PC15 ...

重要限制

同一编号的引脚只能有一个连接到 EXTI。比如 PA0 和 PB0 不能同时使用外部中断,因为它们共用 EXTI0。

触发方式

触发方式 含义 使用场景
上升沿触发 电平从低变高时触发 按键释放、脉冲上升边
下降沿触发 电平从高变低时触发 按键按下(低电平有效)
双边沿触发 上升沿和下降沿都触发 编码器信号检测

EXTI 的中断服务函数

EXTI 线共用部分 IRQ(中断请求通道):

EXTI 线 对应的 IRQ Handler 说明
EXTI0 EXTI0_IRQHandler 独占
EXTI1 EXTI1_IRQHandler 独占
EXTI2 EXTI2_IRQHandler 独占
EXTI3 EXTI3_IRQHandler 独占
EXTI4 EXTI4_IRQHandler 独占
EXTI5~9 EXTI9_5_IRQHandler 5 条共用,需在函数内判断是哪条
EXTI10~15 EXTI15_10_IRQHandler 6 条共用,需在函数内判断是哪条

四、EXTI 编程实战(CubeMX + HAL)

外部中断检测按键

配置 PA0 的下降沿触发中断,按下按键翻转 PC13 LED:

CubeMX 配置步骤:

  1. PC13GPIO_Output(LED,推挽输出)
  2. PA0GPIO_EXTI0(外部中断模式)
  3. PA0 的 GPIO 配置:
    • GPIO mode: External Interrupt Mode with Falling edge trigger detection(下降沿触发)
    • GPIO Pull-up/Pull-down: Pull-up(上拉,默认高电平)
  4. NVIC 标签页中勾选 EXTI line0 interrupt 并设置优先级
  5. System Core → NVIC 中设置优先级分组为 2 bits for pre-emption
  6. 生成代码

CubeMX 会自动生成初始化代码(gpio.c 和 NVIC 配置)。你只需要实现 回调函数

/* 在 main.c 中(USER CODE 区域)编写回调函数 */

/* USER CODE BEGIN 4 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0)  // 确认是 PA0 触发的
    {
        HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);  // 翻转 LED
    }
}
/* USER CODE END 4 */

HAL 中断处理流程

HAL 库的中断处理分为两层:

  1. IRQ Handlerstm32f1xx_it.c 中,CubeMX 自动生成):调用 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0)
  2. HAL 内部处理:自动判断中断标志、清除标志位、调用回调函数
  3. 回调函数(你来实现):HAL_GPIO_EXTI_Callback()

你只需要写第 3 步,标志位的判断和清除 HAL 已经帮你处理了!

graph LR
    A[中断发生] --> B["EXTI0_IRQHandler()<br>(stm32f1xx_it.c)"]
    B --> C["HAL_GPIO_EXTI_IRQHandler()<br>(HAL 内部)"]
    C --> D[检查 & 清除中断标志]
    D --> E["HAL_GPIO_EXTI_Callback()<br>(你来实现)"]

CubeMX 生成的中断相关代码

stm32f1xx_it.c 中(自动生成,不需修改):

void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);  // 自动生成
}

gpio.c 中的初始化(自动生成):

void MX_GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    /* LED */
    HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);  // 初始灭
    GPIO_InitStruct.Pin   = GPIO_PIN_13;
    GPIO_InitStruct.Mode  = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull  = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    /* 按键 - 外部中断 */
    GPIO_InitStruct.Pin  = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;  // 下降沿触发中断
    GPIO_InitStruct.Pull = GPIO_PULLUP;            // 上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* NVIC */
    HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 1);  // 抢占1,子优先级1
    HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}

不需要手动开启 AFIO 时钟

在 HAL 库中,HAL_GPIO_Init() 设置 EXTI 模式时会自动处理 AFIO 相关的配置,你不需要手动操作。


五、中断编程三要素(HAL 版)

总结一下,使用 CubeMX + HAL 配置中断的流程:

graph LR
    A["1. CubeMX 配置外设<br>使能中断"] --> B["2. CubeMX 配置 NVIC<br>设置优先级"]
    B --> C["3. 实现回调函数<br>处理逻辑"]

要素一:CubeMX 配置外设并使能中断

在对应外设的配置页面中勾选中断使能:

外设 CubeMX 操作
EXTI 引脚设置为 GPIO_EXTIx 模式
TIM Parameter Settings → 使能,NVIC 标签页勾选中断
USART NVIC Settings 标签页中勾选中断
ADC 启用 DMA 或在 NVIC 中勾选中断

要素二:配置 NVIC 优先级

System Core → NVIC 中设置:

  • 优先级分组(全局只设置一次)
  • 各中断的抢占优先级和子优先级

CubeMX 生成的代码中会调用:

HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);     // 分组(main.c 中)
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 1);                 // 设置优先级
HAL_NVIC_EnableIRQ(EXTI0_IRQn);                          // 使能中断

要素三:实现回调函数

HAL 库为每种外设提供了对应的回调函数(__weak 弱定义),你只需要重写它:

外设 回调函数
EXTI HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
TIM HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
UART HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
ADC HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)

HAL 回调机制的优势

  • 不需要手动判断和清除中断标志位(HAL 内部已处理)
  • 回调函数名统一、规范,不容易写错
  • 多个相同外设可以在回调函数中通过 handle 指针区分

回调函数中仍然要快进快出

虽然 HAL 简化了中断处理,但回调函数本质上还是在中断上下文中执行。应避免在回调中做耗时操作,推荐用标志位通知主循环处理。


六、常见问题

中断回调函数写在哪个文件里?

推荐写在 main.c/* USER CODE BEGIN 4 */ 区域,或者单独创建一个 callback.c 文件。不要写在 stm32f1xx_it.c 中(会被 CubeMX 覆盖)。

中断中可以调用 HAL_Delay 吗?

不可以HAL_Delay() 依赖 SysTick 中断,如果你的中断优先级高于 SysTick(默认最低优先级 15),会导致 SysTick 无法更新计数器,HAL_Delay() 卡死。如需延时操作,应在中断中设置标志位,在主循环中处理。

中断 vs 轮询,怎么选?

  • 中断:适合事件不频繁、实时性要求高的场景(按键、通信接收)
  • 轮询:适合事件非常频繁、处理简单的场景(高速 ADC 连续采集)
  • 一般原则:能用中断就用中断,CPU 效率更高