HAL库中断配置流程:从优先级到服务函数
中断,说白了就是CPU正在干活,突然被一个紧急事件打断,跑去处理完再回来继续干活。这个机制在嵌入式系统里太常用了——按键响应、定时器溢出、串口收到数据,几乎每个外设都离不开它。
我刚开始用HAL库的时候,总觉得中断配置挺绕的。后来踩了几个坑,才摸清楚这套流程的门道。今天咱们就把 HAL_NVIC_SetPriority、HAL_NVIC_EnableIRQ 和中断服务函数这三个环节彻底讲透。
中断配置的三步走
HAL库的中断配置,其实就三步。我习惯把它记成「设优先级 → 使能中断 → 写服务函数」。顺序不能乱,少了哪一步都不行。
| 步骤 | API函数 | 作用 |
|---|---|---|
| 1 | HAL_NVIC_SetPriority |
设置中断优先级分组和抢占/响应优先级 |
| 2 | HAL_NVIC_EnableIRQ |
使能NVIC中的对应中断通道 |
| 3 | 中断服务函数 |
编写中断触发后要执行的代码 |
HAL_NVIC_SetPriorityGrouping 确定优先级分组。虽然默认分组通常是4(4位抢占+0位响应),但显式设置一次能避免后续移植时出问题。
HAL_NVIC_SetPriority:优先级到底怎么设?
这个函数的原型很简单:
void HAL_NVIC_SetPriority(IRQn_Type IRQn, uint32_t PreemptPriority, uint32_t SubPriority);
三个参数:中断号、抢占优先级、响应优先级。但这里有个坑——优先级分组会影响这两个参数的实际含义。
STM32的NVIC支持最多16级优先级(4位二进制)。这4位怎么分配?由优先级分组决定:
| 分组 | 抢占优先级位数 | 响应优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PRIORITYGROUP_0 | 0 | 4 | 不支持抢占,只有响应优先级 |
| NVIC_PRIORITYGROUP_1 | 1 | 3 | 2级抢占,8级响应 |
| NVIC_PRIORITYGROUP_2 | 2 | 2 | 4级抢占,4级响应 |
| NVIC_PRIORITYGROUP_3 | 3 | 1 | 8级抢占,2级响应 |
| NVIC_PRIORITYGROUP_4 | 4 | 0 | 16级抢占,不支持子优先级 |
举个例子。假设我选了分组2(2位抢占+2位响应),那么:
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0)—— 抢占优先级1,响应优先级0HAL_NVIC_SetPriority(TIM2_IRQn, 0, 3)—— 抢占优先级0,响应优先级3
这时候TIM2的抢占优先级更高(0 < 1),所以TIM2可以打断USART1的中断。但如果两个中断的抢占优先级相同,就看响应优先级——谁小谁先执行,但不能互相打断。
HAL_NVIC_EnableIRQ:使能中断通道
设完优先级,下一步就是使能中断。这个函数更简单:
void HAL_NVIC_EnableIRQ(IRQn_Type IRQn);
参数就是中断号。比如使能USART1的中断:
HAL_NVIC_EnableIRQ(USART1_IRQn);
这里有个细节——HAL_NVIC_EnableIRQ 只操作NVIC的使能寄存器,它不会帮你配置外设本身的中断使能位。比如串口,你还得调用 __HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE) 来使能接收中断。这两个使能是「与」的关系,缺一不可。
我见过不少新手只调了 HAL_NVIC_EnableIRQ,外设的中断标志位没使能,结果中断死活不触发。排查了半天才发现是外设那边没开。
中断服务函数编写规范
中断触发后,CPU会跳转到中断向量表里对应的地址执行。这个地址对应的函数,就是中断服务函数(ISR)。
STM32的中断服务函数名是固定的,在启动文件(比如 startup_stm32f4xx.s)里已经声明了弱定义。你需要自己实现这些函数:
// USART1中断服务函数
void USART1_IRQHandler(void)
{
// 检查中断标志位
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE))
{
// 读取数据
uint8_t data = (uint8_t)(huart1.Instance->DR & 0xFF);
// 处理数据(比如放入环形缓冲区)
// ...
// 清除标志位
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE);
}
}
写ISR有几个铁律,我列出来:
- 快进快出:中断里别做耗时操作,比如打印、延时、复杂计算。收到数据就存起来,处理逻辑放主循环。
- 清除标志位:不清理标志位,中断会反复触发,系统直接卡死。
- 保护共享数据:如果ISR和主循环共用变量,记得加volatile关键字,必要时关中断保护。
- 不要调用HAL_Delay:HAL_Delay依赖SysTick中断,优先级如果比当前中断低,会死锁。
完整配置流程示例
把上面三步串起来,一个典型的中断配置流程长这样:
// 1. 设置优先级分组(整个系统只设一次)
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 2. 配置USART1中断优先级
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
// 3. 使能NVIC中的USART1中断通道
HAL_NVIC_EnableIRQ(USART1_IRQn);
// 4. 使能USART1外设的接收中断(HAL库内部调用)
__HAL_UART_ENABLE_IT(&huart1, UART_IT_RXNE);
// 5. 编写中断服务函数(在另一个.c文件里)
void USART1_IRQHandler(void)
{
// 快速处理...
}
嗯,这里要注意——步骤4其实通常在 HAL_UART_Receive_IT 里已经帮你做了。如果你用HAL库的轮询或DMA模式,不需要手动调这个宏。
中断配置的完整知识图谱
下面这张图把整个中断配置的流程和关键节点串起来了,你可以对照着看:
几个容易忽略的细节
最后补充几个我实际项目中遇到的细节:
- 中断向量表偏移:如果你用了Bootloader,记得在跳转前设置
SCB->VTOR指向应用程序的中断向量表地址。不然中断来了,CPU跑去找Bootloader的ISR,直接跑飞。 - 中断嵌套深度:虽然STM32支持中断嵌套,但嵌套层数太多会爆栈。我一般控制在3层以内,超过这个数就考虑用事件标志+主循环处理。
- HAL库的弱定义:启动文件里的ISR都是
__weak弱定义,你只要写一个同名函数就会覆盖它。但注意——如果你写了USART1_IRQHandler却忘了实现,链接器不会报错,只会默默链接那个空的弱函数。中断来了,啥也不干。
HardFault_Handler。查了半天,原来是中断向量表里I2C的中断号写错了,跟另一个外设冲突。所以配置中断号的时候,一定对着参考手册的向量表核对一遍。
中断配置说难不难,说简单也不简单。核心就是三步走——优先级、使能、ISR。每一步都有细节,但只要你按照这个流程走,基本不会出大问题。下次咱们聊聊HAL库的中断回调机制,那个又是另一套玩法了。