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的步骤,我总结为四步走:
- 使能GPIO时钟 —— 别漏了,漏了GPIO不工作
- 配置GPIO模式为输入 —— 上拉或下拉根据电路决定
- 连接GPIO到EXTI线 —— 通过SYSCFG配置
- 配置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响应,每一步都清晰可见。
3.6 常见问题与避坑
做中断开发,有几个坑我几乎每次培训都会提:
- 中断标志没清干净 —— 有些外设有多个中断标志,只清了一个,另一个还挂着,导致反复进中断。我建议在ISR开头就把所有相关标志全读一遍,统一清除。
- 优先级配置冲突 —— 两个中断互相抢占,导致低优先级中断永远得不到执行。用NVIC_SetPriorityGrouping之前,先想清楚你的系统里哪些中断是时间敏感的。
- 在ISR里调用阻塞函数 —— 比如printf、HAL_Delay、malloc。这些函数内部可能依赖其他中断,在ISR里调用会导致死锁。我吃过这个亏,后来ISR里只用寄存器操作。
- 中断嵌套过深 —— 理论上NVIC支持无限嵌套,但实际栈空间有限。嵌套超过3层,栈溢出风险就很高。合理设计优先级,不要让高优先级中断太频繁。
最后提醒:调试中断时,善用调试器的断点。但不要在ISR里打断点,因为中断频率高的话,断点一停,系统实时性全乱。我一般用GPIO翻转来观察中断响应时间,用逻辑分析仪抓波形,比断点靠谱得多。