3. 中断系统入门:NVIC与中断服务函数
中断,说白了就是让CPU放下手头的事,先去处理一个更紧急的任务。做嵌入式开发,不懂中断就像开车不会踩刹车——迟早要出事。这一节我们就把中断系统的几个核心概念掰开揉碎,讲清楚。
3.1 NVIC:谁在管理中断?
NVIC全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。它是Cortex-M内核里专门管中断的硬件模块。我刚开始接触STM32时,总觉得NVIC是个很玄乎的东西,后来发现它其实就是个“中断调度员”。
它的核心职责有三点:
- 使能/失能中断——决定哪个中断源能进来
- 设置优先级——多个中断同时来,谁先处理
- 向量表管理——中断来了,跳转到哪个函数执行
你想想看,如果没有NVIC,所有中断都得靠CPU轮询去查,那效率得多低?NVIC硬件自动完成这些判断,CPU只管执行中断服务函数就行。
关键点:NVIC是硬件模块,不是软件模拟的。它的响应速度是纳秒级的,比软件轮询快几个数量级。
3.2 中断优先级分组:谁更重要?
中断优先级分组,是Cortex-M里一个容易让人迷糊的概念。我见过不少新手在这里栽跟头——明明配置了优先级,结果中断嵌套就是不对。
STM32把优先级分成两部分:抢占优先级和子优先级。抢占优先级高的可以打断正在执行的低优先级中断,这叫嵌套。子优先级只在同抢占优先级时起作用,决定谁先执行。
优先级分组通过寄存器SCB->AIRCR配置,一共5种分组方式:
| 分组 | 抢占优先级位数 | 子优先级位数 | 优先级范围 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 bit | 4 bits | 0~15 |
| NVIC_PriorityGroup_1 | 1 bit | 3 bits | 0~7 |
| NVIC_PriorityGroup_2 | 2 bits | 2 bits | 0~3 |
| NVIC_PriorityGroup_3 | 3 bits | 1 bit | 0~1 |
| NVIC_PriorityGroup_4 | 4 bits | 0 bits | 0~15 |
我的习惯:项目里我一般用Group_2,也就是2位抢占优先级、2位子优先级。这样抢占优先级有4级(0~3),够用又不浪费。子优先级也能区分同抢占级的中断顺序。
3.3 中断使能与失能
使能中断,就是告诉NVIC:“这个中断我允许它进来”。失能就是关掉它。操作很简单,但位置很重要。
在STM32标准库中,使能一个中断通常分两步:
- 外设自身的中断使能——比如USART的接收中断使能位
- NVIC的中断使能——调用
NVIC_EnableIRQ()
两步缺一不可。我曾经调试一个UART接收中断,怎么都进不去,查了半天发现外设中断使能位没置——这种低级错误,犯过一次就记住了。
代码示例:
// 使能USART1接收中断
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
// 配置NVIC
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
失能中断同理:
NVIC_DisableIRQ(USART1_IRQn);
注意:失能中断时,如果当前正在执行该中断的服务函数,NVIC会等它执行完才真正关闭。这叫“优雅关闭”,不会导致中断函数执行到一半被强行掐断。
3.4 中断服务函数写法
中断服务函数,也叫ISR(Interrupt Service Routine)。写法有固定套路,不能随便起名字。
在STM32中,中断服务函数的名字已经在启动文件里定义好了。比如USART1的中断服务函数叫USART1_IRQHandler。你必须在代码里实现这个函数,否则中断来了会跳转到HardFault_Handler——嗯,那可不是什么好结果。
标准写法:
void USART1_IRQHandler(void)
{
// 检查中断标志位
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
// 读取接收数据寄存器,自动清除RXNE标志
uint8_t data = USART_ReceiveData(USART1);
// 处理接收到的数据
// ...
// 清除中断标志位(某些外设需要手动清除)
USART_ClearITPendingBit(USART1, USART_IT_RXNE);
}
}
写ISR有几个铁律:
- 尽量短小精悍——中断里别做复杂运算,别调用printf,别用延时
- 标志位及时清除——不清除会反复进中断,死循环
- 保护共享数据——中断和主循环共享的变量,加volatile关键字
避坑指南:我曾经在ISR里调用了HAL_Delay(),结果系统直接卡死。因为HAL_Delay()依赖SysTick中断,而SysTick中断优先级如果比当前中断低,就永远等不到——死锁了。所以记住:中断里别用依赖其他中断的函数。
3.5 中断系统核心流程
下面这张图把中断从触发到响应的完整流程画出来了,我建议你多看几遍,理解每一步在干什么。
流程其实不复杂:外设触发 → NVIC判断优先级 → 如果可以抢占就保存现场 → 执行ISR → 恢复现场。每一步都是硬件自动完成的,我们只需要写好ISR里的逻辑。
3.6 写在后面
中断系统是嵌入式开发的基石。NVIC的优先级分组、使能失能、ISR写法,这三块搞明白了,后面UART中断收发才能玩得转。我建议你拿到开发板后,先写一个最简单的按键中断程序,把NVIC配置走一遍,感受一下中断的“打断”效果。纸上得来终觉浅,亲手跑一遍比看十遍都管用。