第四章 中断系统:NVIC配置、中断优先级、外部中断EXTI、中断服务函数编写、中断嵌套

中断系统,说白了就是MCU的“紧急事务处理机制”。

我刚开始做嵌入式开发那会儿,总觉得中断就是个“高级点的轮询”。直到有一次,一个电机控制项目在高速运行时,轮询方式根本来不及响应过流信号,板子直接烧了。嗯,从那以后,我再也不敢小看中断了。

4.1 中断系统概述

中断是什么?打个比方:你正在写代码,突然有人敲门。你是继续写代码,还是先去开门?正常人都会先去开门,处理完再回来继续写。中断就是这个“敲门”的动作。

MCU在执行主程序时,如果某个事件发生了(比如按键按下、定时器溢出),它会暂停当前任务,跳转去处理这个事件,处理完再回来。这个机制就叫中断。

核心要点:中断系统由三部分组成——中断源、中断控制器(NVIC)、中断服务函数。缺一不可。

4.2 NVIC配置——中断的“调度中心”

NVIC,全称Nested Vectored Interrupt Controller,嵌套向量中断控制器。名字很长,但功能很简单:它负责管理所有中断的使能、挂起、优先级。

我个人习惯,在项目一开始就把NVIC配置好。因为后面再加配置,容易漏掉。

4.2.1 NVIC寄存器结构

NVIC的核心寄存器就那么几个:

寄存器 功能
ISER 中断使能寄存器(写1使能)
ICER 中断除能寄存器(写1除能)
ISPR 中断挂起寄存器(手动触发中断)
ICPR 中断清除挂起寄存器
IPR 中断优先级寄存器(每个中断占8位)

配置NVIC,说白了就是操作这几个寄存器。不过,我们一般用库函数,不用直接操作寄存器——除非你在做Bootloader或者RTOS移植。

4.2.2 NVIC配置示例(基于STM32 HAL库)

// 使能USART1中断
HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);  // 抢占优先级0,子优先级0
HAL_NVIC_EnableIRQ(USART1_IRQn);

这里要注意:HAL_NVIC_SetPriority的第一个参数是中断号,第二个是抢占优先级,第三个是子优先级。这两个值怎么分?看下一节。

4.3 中断优先级——谁先谁后?

中断优先级,决定了多个中断同时发生时,谁先被处理。

我记得有个项目,用了两个外部中断和一个定时器中断。一开始没配优先级,结果按键中断老是抢不过定时器中断,按键响应延迟严重。后来把按键中断优先级调高,问题解决。

4.3.1 抢占优先级 vs 子优先级

ARM Cortex-M内核的中断优先级分两组:

  • 抢占优先级:决定是否打断正在执行的中断。数值越小,优先级越高。
  • 子优先级:当抢占优先级相同时,决定谁先执行。数值越小,优先级越高。

举个例子:

中断 抢占优先级 子优先级
定时器中断 0 0
外部中断0 1 0
外部中断1 1 1

定时器中断优先级最高,可以打断外部中断0和1。外部中断0和1的抢占优先级相同,所以它们之间不会互相打断,但子优先级高的(数值小)先执行。

4.3.2 优先级分组

STM32用4位来表示优先级,这4位怎么分配?通过HAL_NVIC_SetPriorityGrouping来设置:

// 设置优先级分组:4位抢占优先级,0位子优先级
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4);

常见的分组方式:

分组 抢占优先级位数 子优先级位数
NVIC_PRIORITYGROUP_0 0 4
NVIC_PRIORITYGROUP_1 1 3
NVIC_PRIORITYGROUP_2 2 2
NVIC_PRIORITYGROUP_3 3 1
NVIC_PRIORITYGROUP_4 4 0

我的建议:一般项目用NVIC_PRIORITYGROUP_4就够了。所有中断的抢占优先级都不同,简单粗暴。只有当中断数量超过16个时,才考虑用分组。

4.4 外部中断EXTI——最常用的中断

外部中断EXTI,就是检测GPIO引脚上的电平变化。按键、传感器信号、通信触发,都靠它。

4.4.1 EXTI工作原理

EXTI可以检测四种触发方式:

  • 上升沿触发
  • 下降沿触发
  • 双边沿触发
  • 软件触发

配置EXTI,需要做三件事:

  1. 配置GPIO为输入模式
  2. 将GPIO连接到EXTI线
  3. 配置EXTI触发方式和使能

4.4.2 EXTI配置示例

// 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. 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);

这里有个坑:GPIO的Mode要选GPIO_MODE_IT_xxx,而不是GPIO_MODE_INPUT。我刚开始就犯过这个错,配置了半天中断就是不触发。

4.5 中断服务函数编写——中断的“处理程序”

中断服务函数,就是中断触发后要执行的代码。写这个函数,有几个铁律:

  • 快进快出:中断里不要做复杂运算,不要延时,不要打印。
  • 清除标志位:不清除标志位,中断会反复触发。
  • 保护共享数据:中断和主程序共享的变量,要用volatile修饰。

4.5.1 中断服务函数模板

void EXTI0_IRQHandler(void)
{
    // 检查中断标志位
    if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
    {
        // 处理中断事件
        // 比如:设置一个标志位
        g_button_pressed = 1;
        
        // 清除中断标志位
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
    }
}

我曾经踩过的坑:在中断里调用了printf函数。结果程序一跑就死机。后来查资料才知道,printf用了轮询方式发送数据,中断里用轮询,相当于在中断里等另一个中断,直接死锁。

4.6 中断嵌套——中断里还有中断

中断嵌套,就是高优先级中断打断低优先级中断。听起来复杂,其实就一句话:抢占优先级高的可以打断抢占优先级低的

4.6.1 中断嵌套示例

假设有两个中断:

  • 定时器中断(抢占优先级0)
  • 外部中断(抢占优先级1)

执行流程:

  1. 主程序正在运行
  2. 外部中断触发,进入外部中断服务函数
  3. 外部中断执行到一半,定时器中断触发
  4. 因为定时器中断优先级更高,所以暂停外部中断,去执行定时器中断
  5. 定时器中断执行完,返回外部中断继续执行
  6. 外部中断执行完,返回主程序

你想想看,如果外部中断的优先级比定时器高,那定时器就打断不了外部中断。这就是优先级的作用。

4.6.2 中断嵌套的注意事项

  • 栈空间要够:每嵌套一层,就要多占一些栈空间。嵌套太深,栈溢出,程序就崩了。
  • 不要关中断太久:关中断会导致高优先级中断无法响应。我见过有人为了保护一段代码,关了中断500ms,结果系统直接卡死。
  • 优先级不要设得太乱:建议把实时性要求高的中断设高优先级,比如电机控制、通信接收。按键、显示这类可以设低优先级。

4.7 本章知识体系

下面这张图,把中断系统的核心逻辑串起来了:

中断系统知识体系 中断源 NVIC控制器 中断服务函数 外部中断EXTI 定时器中断 串口中断 DMA中断 中断使能/除能 优先级配置 中断挂起/清除 中断嵌套管理 快进快出 清除标志位 保护共享数据 避免死锁 核心逻辑:中断源触发 → NVIC判断优先级 → 执行中断服务函数 → 返回 高优先级可打断低优先级,形成中断嵌套

4.8 避坑指南

我曾经犯过的错,你千万别再犯:

  • 中断里用延时函数——直接卡死整个系统
  • 忘记清除中断标志位——中断反复触发,CPU占用率100%
  • 中断里操作复杂数据结构——数据被破坏,程序跑飞
  • 多个中断共享变量不加volatile——编译器优化后,变量值不对
  • 中断优先级设得乱七八糟——高优先级中断响应延迟,系统实时性变差

嗯,中断系统就讲到这里。说白了,中断就是个“紧急事务处理机制”。配置好NVIC,写好中断服务函数,注意优先级和嵌套,你的MCU就能高效处理各种突发事件了。


专注资料整理