4. 中断与事件系统:NVIC嵌套向量中断控制器
中断系统,说白了就是CPU的“紧急通道”。
我刚开始做汽车级开发时,总觉得中断嘛,不就是个回调函数?后来在ECU项目中,一个优先级没设对,直接导致CAN报文丢失,车子差点在路上趴窝。嗯,从那以后,我对中断的态度就变了——它不只是个功能,而是整个系统的“命门”。
4.1 NVIC:嵌套向量中断控制器
NVIC是ARM Cortex-M内核的中断管家。它管三件事:中断使能、中断挂起、中断优先级。
在STM32上,NVIC最多支持240个中断。但汽车级芯片通常不会全用,我一般只用到60个左右。你想想看,每个中断都要分配优先级,这活儿不轻。
核心概念:NVIC不是外设,它是内核的一部分。所以它的寄存器访问速度极快,几乎没有延迟。
我个人习惯,在项目启动阶段就把所有中断的优先级规划好。别等到写代码时再临时拍脑袋,那样容易出乱子。
4.2 中断优先级分组
STM32的中断优先级分两组:抢占优先级和子优先级。这两者的关系,我打个比方:
- 抢占优先级:像公司里的部门经理。级别高的可以打断级别低的。
- 子优先级:像部门里的员工。同级别经理下,员工按资历排队。
优先级分组由寄存器SCB->AIRCR决定。STM32支持4位优先级,分组方式如下:
| 分组 | 抢占优先级位数 | 子优先级位数 | 抢占优先级范围 | 子优先级范围 |
|---|---|---|---|---|
| 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 | 无 |
避坑指南:我曾经在一个项目中,把分组设成了Group_0(无抢占优先级)。结果高优先级中断无法打断低优先级,导致一个紧急刹车信号被延时处理了200微秒。在汽车上,200微秒可能意味着事故。
我建议汽车级项目用Group_2或Group_3。这样既有足够的抢占层级,又能保留子优先级做精细调度。
4.3 抢占优先级与子优先级
这两者的区别,我用一个实际场景说明:
假设有两个中断:
- 中断A:抢占优先级1,子优先级0
- 中断B:抢占优先级1,子优先级1
- 中断C:抢占优先级0,子优先级0
执行顺序是这样的:
- 如果A正在执行,B来了——B会等待,因为抢占优先级相同,子优先级低的先执行。
- 如果A正在执行,C来了——C会打断A,因为C的抢占优先级更高(0比1高)。
- 如果A和B同时来——A先执行,因为子优先级0比1高。
为什么会这样?因为抢占优先级决定“谁可以打断谁”,子优先级决定“谁先排队”。
4.4 中断向量表重定向
中断向量表,就是一张“中断函数地址表”。默认在Flash的起始地址0x08000000。
但在汽车级开发中,我们经常需要做IAP(在应用编程)。这时候就要把向量表搬到RAM里。
代码实现很简单:
// 重定向中断向量表到RAM
#define VECTOR_TABLE_RAM_BASE 0x20000000
void VectorTable_Relocate(void)
{
// 1. 复制向量表到RAM
memcpy((void*)VECTOR_TABLE_RAM_BASE,
(void*)SCB->VTOR,
sizeof(uint32_t) * 16); // 只复制前16个系统异常
// 2. 设置VTOR寄存器指向RAM
SCB->VTOR = VECTOR_TABLE_RAM_BASE;
// 3. 使能内存屏障,确保写入完成
__DSB();
__ISB();
}
小技巧:我习惯在重定向后,读回VTOR寄存器确认一下。曾经遇到过芯片复位后VTOR自动恢复默认值的情况,排查了半天才发现是硬件bug。
4.5 外部中断EXTI
EXTI是STM32的外部中断控制器。它负责把GPIO引脚上的电平变化,转换成中断信号。
配置EXTI的步骤:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式
- 将GPIO连接到EXTI线(通过SYSCFG_EXTICR寄存器)
- 配置EXTI触发方式(上升沿、下降沿、双边沿)
- 使能EXTI中断
- 配置NVIC优先级
代码示例:
void EXTI_Config(void)
{
// 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 配置PA0为输入
GPIOA->MODER &= ~(0x3 << 0); // 00: 输入模式
// 连接PA0到EXTI0
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
// 配置下降沿触发(汽车上常用,抗干扰好)
EXTI->FTSR |= EXTI_FTSR_TR0;
// 清除挂起位
EXTI->PR |= EXTI_PR_PR0;
// 使能中断
EXTI->IMR |= EXTI_IMR_MR0;
// 配置NVIC优先级
NVIC_SetPriority(EXTI0_IRQn, 2); // 抢占优先级2
NVIC_EnableIRQ(EXTI0_IRQn);
}
注意:EXTI线0-15对应GPIO的PIN0-15。但每个EXTI线只能连接一个GPIO引脚。比如EXTI0只能连接PA0、PB0、PC0...中的一个。不能同时用PA0和PB0触发EXTI0。
4.6 事件触发与唤醒
事件和中断的区别,我一句话说清楚:
- 中断:CPU必须响应,执行ISR。
- 事件:CPU可以不响应,只触发硬件动作(如DMA传输、定时器启动)。
在汽车级低功耗设计中,事件唤醒特别重要。比如:
- 车门打开事件唤醒ECU
- CAN总线活动事件唤醒控制器
- 按键按下事件唤醒显示屏
配置事件唤醒的代码:
void Event_Wakeup_Config(void)
{
// 配置EXTI为事件模式(不是中断模式)
EXTI->EMR |= EXTI_EMR_MR0; // 使能事件
// 配置唤醒源
PWR->CSR |= PWR_CSR_EWUP1; // 使能PA0作为唤醒源
// 进入停止模式
PWR->CR |= PWR_CR_LPDS; // 低功耗深度睡眠
PWR->CR |= PWR_CR_CWUF; // 清除唤醒标志
SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;
__WFI(); // 等待事件唤醒
}
我个人经验:事件唤醒的响应时间一般在几十微秒级别。如果要求更快的唤醒,可以考虑用SRAM保持模式。
4.7 实战:多级中断嵌套处理
好了,理论讲完了。咱们来点实战。
假设一个汽车门控系统:
- 中断1:CAN接收中断(优先级1,子优先级0)——最紧急
- 中断2:车门开关检测(优先级2,子优先级0)——中等
- 中断3:LED闪烁定时器(优先级3,子优先级0)——最低
代码实现:
// 中断优先级分组:Group_2(2位抢占,2位子优先级)
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 配置CAN中断(最高优先级)
NVIC_SetPriority(CAN1_RX0_IRQn, 1 << 4 | 0); // 抢占1,子0
NVIC_EnableIRQ(CAN1_RX0_IRQn);
// 配置GPIO中断(中等优先级)
NVIC_SetPriority(EXTI0_IRQn, 2 << 4 | 0); // 抢占2,子0
NVIC_EnableIRQ(EXTI0_IRQn);
// 配置定时器中断(最低优先级)
NVIC_SetPriority(TIM2_IRQn, 3 << 4 | 0); // 抢占3,子0
NVIC_EnableIRQ(TIM2_IRQn);
// CAN中断服务函数
void CAN1_RX0_IRQHandler(void)
{
// 这里可以打断其他所有中断
// 处理CAN报文
uint32_t data = CAN_Receive();
Process_CAN_Message(data);
}
// 车门检测中断
void EXTI0_IRQHandler(void)
{
if(EXTI->PR & EXTI_PR_PR0)
{
// 这里只能被CAN中断打断
// 读取车门状态
uint8_t door_status = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
Update_Door_Status(door_status);
EXTI->PR |= EXTI_PR_PR0; // 清除标志
}
}
// LED定时器中断
void TIM2_IRQHandler(void)
{
if(TIM2->SR & TIM_SR_UIF)
{
// 这里可以被任何中断打断
// 翻转LED
GPIO_ToggleBits(GPIOB, GPIO_Pin_0);
TIM2->SR &= ~TIM_SR_UIF; // 清除标志
}
}
关键点:多级中断嵌套时,一定要考虑堆栈深度。每个中断嵌套一层,堆栈就多占几十字节。我曾经在项目中因为堆栈溢出导致系统随机死机,排查了三天才发现是中断嵌套太深,堆栈不够用。
我建议的做法:
- 中断嵌套不超过3层
- 每个ISR尽量短小,把耗时操作放到任务中
- 在ISR中不要调用printf、malloc等不可重入函数
- 使用volatile修饰共享变量
嗯,中断系统就讲到这里。下一章咱们聊聊定时器,那又是另一番天地了。