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内核的中断控制器,负责:

  • 使能/禁止中断
  • 设置优先级
  • 管理中断挂起和清除
  • 处理中断嵌套

下面这张图展示了中断从触发到响应的完整流程:

中断响应完整流程 中断源触发 NVIC仲裁 查向量表 保存现场 执行ISR 恢复现场 返回主程序 说明: NVIC在收到中断请求后,先判断优先级,再查向量表找到ISR地址。CPU自动保存R0-R3、R12、LR、PC、xPSR到栈中, 执行完ISR后通过BX指令恢复现场,继续执行被中断的程序。

在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是中断的大脑。