1. 中断基础概念:中断、优先级与嵌套、向量表与NVIC
各位同学,咱们今天聊聊中断。说实话,中断是嵌入式系统里最核心的机制之一。你想想看,一个单片机怎么知道外部发生了什么事?按键按下了?数据到了?定时器溢出了?总不能一直轮询吧——那太浪费CPU了。中断就是干这个的。
1.1 什么是中断
中断,说白了就是CPU正在干一件事,突然被一个外部或内部事件打断,它得停下来,去处理这个紧急事件,处理完再回来接着干原来的活。
我打个比方:你正在看书,电话响了。你记下看到哪一页(保存现场),去接电话(执行中断服务函数),接完回来继续看书(恢复现场)。这就是中断。
在STM32里,中断源有很多:
- 外部中断:GPIO引脚电平变化、按键触发
- 定时器中断:定时时间到、PWM周期完成
- 通信中断:UART收到数据、SPI传输完成、I2C地址匹配
- DMA中断:数据传输完成、半传输完成
- 系统中断:SysTick、PVD、RTC闹钟
核心要点:中断机制让CPU从"主动查询"变成"被动响应",效率天差地别。我早期做项目时,有个师兄用轮询方式读传感器,CPU占用率高达80%;后来改成中断触发,直接降到5%。
1.2 中断优先级与嵌套
问题来了:如果多个中断同时发生怎么办?谁先处理?
这就涉及到中断优先级。STM32的NVIC(嵌套向量中断控制器)支持最多16级抢占优先级和16级子优先级(具体看芯片型号)。
优先级规则其实很简单:
- 抢占优先级高的先执行
- 抢占优先级相同,子优先级高的先执行
- 抢占优先级和子优先级都相同,看中断号,号小的先执行
那什么是中断嵌套?就是高优先级中断可以打断正在执行的低优先级中断服务函数。我举个例子:
// 假设配置:
// TIM2中断:抢占优先级1,子优先级0
// USART1中断:抢占优先级0,子优先级0
// 执行流程:
// 1. CPU正在跑主循环
// 2. TIM2中断来了 → 进入TIM2_IRQHandler
// 3. 执行到一半,USART1中断来了(优先级更高)
// 4. 暂停TIM2,进入USART1_IRQHandler
// 5. USART1处理完,返回TIM2继续执行
// 6. TIM2处理完,返回主循环
注意:中断嵌套不是越多越好。我曾经在一个项目里把优先级分了8级,结果调试时发现低优先级中断被饿死了——高优先级中断太频繁,低优先级的一直排不上队。后来我重新规划了优先级分组,才解决问题。
STM32的优先级分组通过NVIC_PriorityGroupConfig()设置:
| 分组 | 抢占优先级位数 | 子优先级位数 | 优先级范围 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 0~15(只有子优先级) |
| NVIC_PriorityGroup_1 | 1 | 3 | 抢占0~1,子0~7 |
| NVIC_PriorityGroup_2 | 2 | 2 | 抢占0~3,子0~3 |
| NVIC_PriorityGroup_3 | 3 | 1 | 抢占0~7,子0~1 |
| NVIC_PriorityGroup_4 | 4 | 0 | 抢占0~15(只有抢占优先级) |
我的建议:一般项目用NVIC_PriorityGroup_2就够了。4级抢占优先级够用,4级子优先级也够用。别搞太复杂,否则调试起来自己都晕。
1.3 中断向量表与NVIC
中断向量表是什么?它是一张表,记录了每个中断对应的服务函数地址。CPU收到中断信号后,就去这张表里查,找到对应的函数跳过去执行。
在STM32里,中断向量表默认放在Flash起始地址0x08000000。但有时候我们需要把它重映射到SRAM——比如做IAP在线升级时。
NVIC(Nested Vectored Interrupt Controller)是Cortex-M内核的中断控制器,负责:
- 使能/禁止中断
- 设置优先级
- 管理中断挂起和清除
- 处理中断嵌套
下面这张图展示了中断从触发到响应的完整流程:
在HAL库中,中断相关的配置代码通常长这样:
// 1. 配置中断优先级分组
HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_2);
// 2. 使能中断并设置优先级
HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
HAL_NVIC_EnableIRQ(USART1_IRQn);
// 3. 中断服务函数(在stm32f4xx_it.c中)
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
避坑指南:我曾经在项目里犯过一个低级错误——在中断服务函数里调用了HAL_Delay()。结果程序跑飞了。为什么?因为HAL_Delay()依赖SysTick中断,而SysTick中断优先级可能比当前中断低,导致死锁。记住:中断服务函数里别用延时函数,别做复杂运算,尽量只设标志位。
1.4 中断的使能与屏蔽
有时候我们需要临时关闭所有中断——比如在写Flash的时候,或者在做一些时序敏感的操作时。Cortex-M提供了__disable_irq()和__enable_irq()指令。
// 关闭所有中断(除了NMI和HardFault)
__disable_irq();
// 执行临界区代码
flash_write_data(...);
// 恢复中断
__enable_irq();
但要注意:__disable_irq()是全局关中断,如果中断服务函数正在执行,你把它关了,那系统就卡死了。所以一般只在主循环或任务中使用。
我的习惯:在HAL库里,我一般用__HAL_LOCK()和__HAL_UNLOCK()来做资源保护,而不是直接关中断。HAL库内部已经做了临界区保护,咱们直接用就行。
1.5 中断延迟与实时性
中断延迟是指从中断触发到CPU开始执行ISR的时间。影响因素包括:
- CPU当前执行指令:如果是多周期指令,得等它执行完
- 高优先级中断:如果有更高优先级中断正在执行,得等它处理完
- 中断屏蔽:如果当前关中断了,得等重新开中断
- 总线访问:如果CPU正在访问慢速外设,可能被阻塞
对于Cortex-M4,典型中断延迟是12个时钟周期。嗯,这个数字在数据手册里能找到,但实际项目中我建议留点余量。
经验之谈:做实时控制时,中断延迟是关键指标。我之前做一个电机控制项目,要求中断响应在5微秒以内。结果发现SPI通信中断优先级设得太低,被其他中断阻塞了。后来把电机控制中断提到最高优先级,问题解决。
好了,中断的基础概念就聊到这儿。这些是后面所有章节的基石——优先级怎么配、嵌套怎么管、向量表怎么用,后面讲HAL库中断管理时会反复用到。记住一句话:中断是嵌入式系统的灵魂,NVIC是中断的大脑。