3. STM32中断系统:从理论到实战

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

我刚开始学STM32时,总觉得中断这玩意儿挺玄乎。后来做项目多了才发现,没有中断,你的MCU就是个“傻等”的机器——轮询、空转、浪费CPU。有了中断,它才真正活起来。

3.1 NVIC中断控制器:中断的大脑

NVIC,全称Nested Vectored Interrupt Controller,嵌套向量中断控制器。它是Cortex-M内核的中断管家。

我个人习惯把NVIC理解成一个“中断调度中心”。它负责三件事:

  • 使能/禁止中断——哪个中断能用,哪个不能用,它说了算
  • 管理中断优先级——多个中断同时来了,谁先处理
  • 处理中断嵌套——高优先级中断可以打断低优先级中断

STM32的NVIC最多支持240个中断(具体看芯片型号)。每个中断都有独立的使能位和优先级配置。

核心要点:NVIC是硬件级的,配置好了就不用操心。但配置错了,系统可能直接跑飞。

3.2 外部中断(EXTI):感知外部世界

EXTI,External Interrupt,外部中断。它负责检测GPIO引脚上的电平变化。

我在项目中遇到过一个问题:按键按下时,中断触发了多次。后来发现是没做消抖处理。嗯,这里要注意——EXTI对边沿非常敏感,硬件抖动会引发误触发。

EXTI支持以下几种触发方式:

  • 上升沿触发——信号从低到高
  • 下降沿触发——信号从高到低
  • 双边沿触发——上升和下降都触发
  • 软件触发——通过写寄存器模拟中断

配置EXTI的步骤,我总结为四步走:

  1. 使能GPIO时钟,配置GPIO为输入模式
  2. 使能SYSCFG时钟,将GPIO连接到EXTI线
  3. 配置EXTI的触发边沿和中断使能
  4. 配置NVIC,使能对应的中断通道
// 示例:配置PA0为外部中断(下降沿触发)
void EXTI_Config(void)
{
    // 1. 使能GPIOA时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    // 2. 使能SYSCFG时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE);
    
    // 3. 配置PA0为输入
    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);
    
    // 4. 连接EXTI线0到PA0
    SYSCFG_EXTILineConfig(EXTI_PortSource_GPIOA, EXTI_PinSource0);
    
    // 5. 配置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);
    
    // 6. 配置NVIC
    NVIC_InitTypeDef NVIC_InitStruct;
    NVIC_InitStruct.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0x01;
    NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0x00;
    NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStruct);
}

3.3 中断优先级分组:谁先谁后?

中断优先级分组,是NVIC里最容易搞混的地方。我刚开始也踩过坑。

STM32把优先级分成两部分:抢占优先级子优先级

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

分组方式由SCB->AIRCR寄存器决定。STM32提供了5种分组:

分组 抢占优先级位数 子优先级位数 优先级范围
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_PriorityGroupConfig()!如果中途改了分组,所有中断的优先级都会乱掉。我有个项目因此出现了诡异的中断冲突,排查了两天才发现是分组被重复配置了。

3.4 中断服务函数编写:名字不能错

中断服务函数,ISR(Interrupt Service Routine)。名字是固定的,写在启动文件里。你写错了,编译器不会报错,但中断永远不会被执行。

常见的EXTI中断服务函数名:

  • EXTI0_IRQHandler —— 对应EXTI线0
  • EXTI1_IRQHandler —— 对应EXTI线1
  • EXTI2_IRQHandler —— 对应EXTI线2
  • EXTI3_IRQHandler —— 对应EXTI线3
  • EXTI4_IRQHandler —— 对应EXTI线4
  • EXTI9_5_IRQHandler —— 对应EXTI线5~9
  • EXTI15_10_IRQHandler —— 对应EXTI线10~15
// 标准的中断服务函数模板
void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志位
    if(EXTI_GetITStatus(EXTI_Line0) != RESET)
    {
        // 2. 处理中断事件
        // 比如:读取按键状态、清除标志、执行回调
        
        // 3. 清除中断标志位(必须!)
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

我的个人习惯:中断服务函数里只做最紧急的事——设置标志位、清除中断标志。耗时的处理放到主循环里做。你想想看,如果中断里跑了个延时函数,整个系统就卡住了。

3.5 中断应用场景分析

中断用得好,系统效率高。用不好,bug满天飞。

我总结了几种常见场景:

  • 按键检测:用下降沿触发,配合软件消抖。我曾经用轮询方式检测按键,CPU占用率直接飙到30%。换成中断后,降到1%以下。
  • 传感器数据采集:传感器准备好数据后,通过INT引脚通知MCU。MCU在中断里读取数据,不用轮询等待。
  • 通信协议处理:比如Modbus的接收完成中断。收到一帧数据后,立即处理,不丢包。
  • 故障检测:过流、过压等紧急情况,用最高优先级中断处理,确保系统安全。

避坑指南:中断优先级不是越高越好。我见过有人把所有中断都设成最高优先级,结果低优先级中断永远得不到执行。合理分配优先级,才是正道。

3.6 中断系统知识体系图

下面这张图,是我梳理的中断系统知识结构。你看一眼,心里就有数了。

STM32中断系统知识体系 中断系统 NVIC中断控制器 中断使能/禁止 优先级管理 中断嵌套 外部中断EXTI 上升沿/下降沿 GPIO连接配置 中断标志清除 中断优先级分组 抢占优先级 子优先级 5种分组方式 中断服务函数ISR 函数名固定 快速处理原则 应用场景 按键检测 传感器采集

这张图把中断系统的五个核心模块串起来了。NVIC是调度中心,EXTI是外部接口,优先级分组决定谁先谁后,ISR是具体干活的人,应用场景告诉你什么时候用。

嗯,中断系统就讲到这里。记住一句话:中断是嵌入式系统的灵魂,用好它,你的代码会优雅很多。


公众号:蓝海资料掘金营,微信deep3321