3、CMS32中断系统:NVIC与中断优先级、外部中断EXTI配置、中断服务函数编写、中断实战(按键中断)

中断,说白了就是CPU正在干一件事,突然被一个更紧急的事件叫停,转去处理那个事件,处理完再回来接着干。嵌入式系统里,中断是灵魂。没有中断,你的CPU就得一直轮询按键、轮询串口,啥正事都干不了。

我个人习惯,拿到一个新MCU,第一个外设实验就是按键中断。为什么?因为中断响应时间、优先级抢占、嵌套这些核心概念,都能在按键中断里看得清清楚楚。今天我们就来把CMS32的中断系统彻底捋一遍。

3.1 NVIC与中断优先级

NVIC,全称Nested Vectored Interrupt Controller,嵌套向量中断控制器。CMS32内核集成了这个家伙,它负责管理所有中断的使能、挂起、优先级和向量表。

你想想看,如果两个中断同时来了,谁先响应?这就靠优先级了。CMS32的NVIC支持最多256个优先级(实际芯片会裁剪,CMS32通常支持16级),数值越小,优先级越高。

概念 说明 我的经验
抢占优先级 决定能否打断正在执行的中断 高抢占优先级可以嵌套低抢占优先级
子优先级 同抢占优先级时,决定谁先执行 子优先级不决定嵌套,只决定排队顺序
优先级分组 分配抢占位和子优先级的位数 CMS32常用分组2:2位抢占 + 2位子优先级

核心要点:NVIC的优先级分组一旦配置,整个系统运行期间不要随意更改。我曾经在一个项目里,中途改了分组,结果中断嵌套逻辑全乱了,排查了两天才发现是这里的问题。

配置NVIC优先级分组,代码很简单:

// 设置优先级分组:2位抢占优先级,2位子优先级
NVIC_SetPriorityGrouping(2);

使能一个中断,并设置它的优先级:

// 使能EXTI0中断,抢占优先级1,子优先级0
NVIC_EnableIRQ(EXTI0_IRQn);
NVIC_SetPriority(EXTI0_IRQn, (1 << 2) | 0);  // 高4位:抢占1,低4位:子0

小技巧:NVIC_SetPriority的第二个参数,CMS32只使用高4位。低4位读回永远是0。所以传参时,抢占优先级左移2位(分组2时),再或上子优先级。

3.2 外部中断EXTI配置

EXTI,外部中断/事件控制器。它负责检测GPIO引脚上的电平变化,产生中断请求送给NVIC。CMS32的EXTI支持最多16条外部中断线(EXTI0~EXTI15),每条线对应一个GPIO引脚。

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

  1. 使能GPIO时钟 —— 别漏了,漏了GPIO不工作
  2. 配置GPIO模式为输入 —— 上拉或下拉根据电路决定
  3. 连接GPIO到EXTI线 —— 通过SYSCFG配置
  4. 配置EXTI触发方式和使能 —— 上升沿、下降沿、双边沿

来看一个实际配置,把PA0配置为下降沿触发的外部中断:

// 1. 使能GPIOA时钟
RCC_EnableGPIOA();

// 2. 配置PA0为输入,上拉
GPIO_InitTypeDef gpio = {0};
gpio.Pin = GPIO_PIN_0;
gpio.Mode = GPIO_MODE_INPUT;
gpio.Pull = GPIO_PULL_UP;
GPIO_Init(GPIOA, &gpio);

// 3. 连接PA0到EXTI0
SYSCFG_EXTILineConfig(EXTI_PORTA, EXTI_PIN0);

// 4. 配置EXTI0:下降沿触发,使能中断
EXTI_InitTypeDef exti = {0};
exti.Line = EXTI_LINE_0;
exti.Mode = EXTI_MODE_INTERRUPT;
exti.Trigger = EXTI_TRIGGER_FALLING;
exti.LineCmd = ENABLE;
EXTI_Init(&exti);

注意:SYSCFG_EXTILineConfig这个函数,很多人会忘。GPIO和EXTI之间不是自动连接的,必须通过SYSCFG手动映射。我见过不下三个同事,中断不触发,查了半天发现是这步没做。

3.3 中断服务函数编写

中断服务函数,ISR(Interrupt Service Routine)。CMS32的每个中断源都有固定的中断向量号,中断服务函数的名字必须和启动文件里的向量表一致。

常见的命名规则:XXX_IRQHandler。比如EXTI0的中断服务函数叫EXTI0_IRQHandler。

编写ISR时,有几个铁律:

  • 快进快出 —— ISR里不要做复杂运算,不要延时,不要调用printf
  • 清除中断标志 —— 不清除标志,会一直进中断,系统卡死
  • 尽量短小 —— 复杂逻辑放到主循环或任务里处理

一个标准的EXTI0中断服务函数:

void EXTI0_IRQHandler(void)
{
    // 检查中断标志
    if (EXTI_GetITStatus(EXTI_LINE_0) != RESET)
    {
        // 处理按键逻辑:比如翻转LED
        GPIO_TogglePin(GPIOB, GPIO_PIN_5);
        
        // 清除中断标志
        EXTI_ClearITPendingBit(EXTI_LINE_0);
    }
}

避坑指南:我曾经在一个产品里,ISR里加了一个while循环等待外部设备响应。结果外部设备挂了,while出不来,整个系统死在那。从那以后,我ISR里绝对不放任何等待循环。如果必须等待,用状态机+定时器轮询。

3.4 中断实战:按键中断

理论说完了,来一个完整的按键中断实验。硬件连接很简单:按键接PA0,按下接地;LED接PB5,低电平点亮。

完整代码框架:

// main.c
#include "main.h"

int main(void)
{
    // 系统时钟配置
    SystemClock_Config();
    
    // 初始化LED
    GPIO_InitTypeDef gpio_led = {0};
    RCC_EnableGPIOB();
    gpio_led.Pin = GPIO_PIN_5;
    gpio_led.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_led.Pull = GPIO_PULL_UP;
    gpio_led.Speed = GPIO_SPEED_HIGH;
    GPIO_Init(GPIOB, &gpio_led);
    GPIO_SetPin(GPIOB, GPIO_PIN_5);  // 初始熄灭
    
    // 初始化按键中断
    GPIO_InitTypeDef gpio_key = {0};
    RCC_EnableGPIOA();
    gpio_key.Pin = GPIO_PIN_0;
    gpio_key.Mode = GPIO_MODE_INPUT;
    gpio_key.Pull = GPIO_PULL_UP;
    GPIO_Init(GPIOA, &gpio_key);
    
    SYSCFG_EXTILineConfig(EXTI_PORTA, EXTI_PIN0);
    
    EXTI_InitTypeDef exti = {0};
    exti.Line = EXTI_LINE_0;
    exti.Mode = EXTI_MODE_INTERRUPT;
    exti.Trigger = EXTI_TRIGGER_FALLING;
    exti.LineCmd = ENABLE;
    EXTI_Init(&exti);
    
    // 配置NVIC
    NVIC_SetPriorityGrouping(2);
    NVIC_SetPriority(EXTI0_IRQn, (1 << 2) | 0);
    NVIC_EnableIRQ(EXTI0_IRQn);
    
    while(1)
    {
        // 主循环做其他事情
    }
}

// 中断服务函数
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_LINE_0) != RESET)
    {
        // 消抖:简单延时,实际项目用定时器
        for(volatile uint32_t i = 0; i < 10000; i++);
        
        // 再次检测电平,确认是真实按键
        if (GPIO_ReadPin(GPIOA, GPIO_PIN_0) == 0)
        {
            GPIO_TogglePin(GPIOB, GPIO_PIN_5);
        }
        
        EXTI_ClearITPendingBit(EXTI_LINE_0);
    }
}

关于消抖:上面代码用了for循环延时消抖,这在教学里没问题。但实际产品中,我建议用定时器+状态机来做消抖。为什么?因为for循环会阻塞CPU,如果系统里还有其他实时任务,这个延时就是灾难。

按下按键,LED翻转。松开再按,再翻转。这就是按键中断的完整流程。

3.5 中断系统知识结构图

下面这张图,把CMS32中断系统的核心脉络画出来了。从外设触发到CPU响应,每一步都清晰可见。

CMS32中断系统核心流程 外设事件 GPIO / 定时器 / 串口 EXTI外部中断 边沿检测 / 中断挂起 NVIC中断控制器 优先级 / 嵌套 / 向量表 CPU内核 压栈 / 跳转 / 执行ISR 中断服务函数ISR 清标志 / 处理逻辑 / 出栈 关键说明: 1. 外设产生中断事件 → EXTI检测边沿并挂起中断 → NVIC判断优先级 2. NVIC决定是否响应 → CPU自动压栈(保存现场)→ 跳转到向量表对应ISR 3. ISR执行 → 清除中断标志 → CPU出栈(恢复现场)→ 返回主程序

3.6 常见问题与避坑

做中断开发,有几个坑我几乎每次培训都会提:

  • 中断标志没清干净 —— 有些外设有多个中断标志,只清了一个,另一个还挂着,导致反复进中断。我建议在ISR开头就把所有相关标志全读一遍,统一清除。
  • 优先级配置冲突 —— 两个中断互相抢占,导致低优先级中断永远得不到执行。用NVIC_SetPriorityGrouping之前,先想清楚你的系统里哪些中断是时间敏感的。
  • 在ISR里调用阻塞函数 —— 比如printf、HAL_Delay、malloc。这些函数内部可能依赖其他中断,在ISR里调用会导致死锁。我吃过这个亏,后来ISR里只用寄存器操作。
  • 中断嵌套过深 —— 理论上NVIC支持无限嵌套,但实际栈空间有限。嵌套超过3层,栈溢出风险就很高。合理设计优先级,不要让高优先级中断太频繁。

最后提醒:调试中断时,善用调试器的断点。但不要在ISR里打断点,因为中断频率高的话,断点一停,系统实时性全乱。我一般用GPIO翻转来观察中断响应时间,用逻辑分析仪抓波形,比断点靠谱得多。