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,响应优先级0
  • HAL_NVIC_SetPriority(TIM2_IRQn, 0, 3) —— 抢占优先级0,响应优先级3

这时候TIM2的抢占优先级更高(0 < 1),所以TIM2可以打断USART1的中断。但如果两个中断的抢占优先级相同,就看响应优先级——谁小谁先执行,但不能互相打断。

我曾经踩过的坑:有一次我把所有中断都设成抢占优先级0,响应优先级0。结果高频率的定时器中断把串口接收中断堵得死死的,数据一直丢。后来给串口设了更高的抢占优先级,问题才解决。记住:实时性要求高的中断,抢占优先级要设小一点。

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中断,优先级如果比当前中断低,会死锁。
我的经验:ISR里我一般只做三件事——读硬件寄存器、存数据到缓冲区、清标志位。数据处理全部扔给主循环的任务队列。这样中断响应时间能控制在几微秒以内。

完整配置流程示例

把上面三步串起来,一个典型的中断配置流程长这样:

// 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模式,不需要手动调这个宏。

中断配置的完整知识图谱

下面这张图把整个中断配置的流程和关键节点串起来了,你可以对照着看:

HAL库中断配置完整流程 1. 设置优先级分组 2. HAL_NVIC_SetPriority 3. HAL_NVIC_EnableIRQ 4. 使能外设中断标志 5. 编写中断服务函数 关键提醒 • 分组只需设一次 • 抢占优先级越小越优先 • 响应优先级只在同级有效 • NVIC使能 ≠ 外设使能 • ISR要快进快出 • 必须清除标志位 • 避免HAL_Delay • 共享数据加volatile • 复杂处理放主循环 优先级分组影响 分组0:0抢占 + 4响应 分组1:1抢占 + 3响应 分组2:2抢占 + 2响应 分组3:3抢占 + 1响应 分组4:4抢占 + 0响应 常用推荐:分组2或4 分组2:4级抢占够用 分组4:简单粗暴

几个容易忽略的细节

最后补充几个我实际项目中遇到的细节:

  • 中断向量表偏移:如果你用了Bootloader,记得在跳转前设置 SCB->VTOR 指向应用程序的中断向量表地址。不然中断来了,CPU跑去找Bootloader的ISR,直接跑飞。
  • 中断嵌套深度:虽然STM32支持中断嵌套,但嵌套层数太多会爆栈。我一般控制在3层以内,超过这个数就考虑用事件标志+主循环处理。
  • HAL库的弱定义:启动文件里的ISR都是 __weak 弱定义,你只要写一个同名函数就会覆盖它。但注意——如果你写了 USART1_IRQHandler 却忘了实现,链接器不会报错,只会默默链接那个空的弱函数。中断来了,啥也不干。
我曾经踩过的坑:有一次调试一个I2C中断,死活不进去。单步跟踪发现程序跑到了 HardFault_Handler。查了半天,原来是中断向量表里I2C的中断号写错了,跟另一个外设冲突。所以配置中断号的时候,一定对着参考手册的向量表核对一遍。

中断配置说难不难,说简单也不简单。核心就是三步走——优先级、使能、ISR。每一步都有细节,但只要你按照这个流程走,基本不会出大问题。下次咱们聊聊HAL库的中断回调机制,那个又是另一套玩法了。