2. 中断控制器:NVIC的工作原理与配置
说到中断,就绕不开NVIC。NVIC的全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。这名字挺长,但说白了,它就是CPU和外部中断源之间的一个“调度员”。
我刚开始做嵌入式开发时,总觉得中断就是“来了就处理”这么简单。直到有一次,我在一个多中断源的系统中遇到了优先级反转的问题——一个低优先级的中断把高优先级的给堵死了。嗯,从那以后,我才真正开始认真研究NVIC。
2.1 NVIC的核心职责
NVIC到底管什么?我总结了三件事:
- 中断使能与屏蔽——决定哪个中断能进来,哪个得等着
- 优先级管理——多个中断同时来了,谁先处理
- 向量表映射——中断来了,CPU该跳到哪里去执行
你想想看,如果没有NVIC,每个外设都得直接跟CPU“喊话”,那CPU得疯掉。NVIC就像一个秘书,帮你过滤、排序、安排这些“电话”。
2.2 嵌套向量中断——名字里的秘密
“嵌套”这个词很关键。它意味着:一个中断正在处理时,更高优先级的中断可以打断它。
举个例子:
- 系统正在运行主程序
- UART中断来了(优先级3),CPU跳去处理
- 处理到一半,定时器中断来了(优先级1,更高)
- CPU暂停UART处理,先去处理定时器
- 定时器处理完,回来继续处理UART
- UART处理完,回到主程序
这就是嵌套。我在项目中遇到过一种情况:如果不小心把两个中断设成相同优先级,那嵌套就乱了,系统行为变得不可预测。所以,优先级分配一定要仔细。
2.3 优先级分组——别小看这个配置
ARM Cortex-M系列的NVIC支持优先级分组。什么意思呢?就是把优先级寄存器里的位,分成两组:
- 抢占优先级(组优先级)——决定能否打断
- 子优先级——同组内谁先执行
配置方式是通过SCB->AIRCR寄存器。常用的分组方式有:
| 分组 | 抢占优先级位数 | 子优先级位数 | 典型应用场景 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 所有中断同级,按子优先级排队 |
| NVIC_PriorityGroup_1 | 1 | 3 | 只有2级抢占,适合简单系统 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4级抢占,我常用的配置 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8级抢占,复杂系统 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16级抢占,最灵活 |
我个人习惯:用NVIC_PriorityGroup_2。4级抢占够用,子优先级还能做点微调。别一上来就搞16级,容易把自己绕晕。
2.4 中断使能与屏蔽——该开的开,该关的关
NVIC里每个中断源都有独立的使能位。配置方法很简单:
// 使能USART1中断
NVIC_EnableIRQ(USART1_IRQn);
// 禁用USART1中断
NVIC_DisableIRQ(USART1_IRQn);
// 设置优先级
NVIC_SetPriority(USART1_IRQn, 2);
这里有个坑:设置优先级要在使能中断之前做。我曾经在调试一个电机驱动时,先使能了中断再设优先级,结果中断一使能就触发了,优先级还是默认值,整个系统直接卡死。嗯,从那以后我都是先设优先级,再使能。
2.5 向量表——中断来了往哪跳
每个中断源都有一个固定的中断号,对应向量表中的一个入口地址。当NVIC判定某个中断需要处理时,它会告诉CPU:“去这个地址执行代码”。
向量表默认放在Flash的起始地址(0x00000000)。但有时候我们需要重定向它,比如在做Bootloader时:
// 将向量表重定向到SRAM
SCB->VTOR = (uint32_t)SRAM_BASE | 0x200;
注意:重定向向量表时,新地址必须对齐到512字节边界。我见过有人写错了偏移量,结果中断一触发就跑飞了。调试了整整一天才发现是VTOR没对齐。
2.6 中断优先级配置的实战建议
说了这么多理论,来点实际的。我在一个工业控制项目中,总结了这么几条优先级分配原则:
- 时间敏感的中断给最高优先级——比如电机控制的PWM中断,错过一个周期电机就抖
- 数据量大的中断给中等优先级——比如DMA传输完成中断,可以稍微等一等
- 人机交互的中断给低优先级——按键、触摸这些,慢一点用户感觉不到
我曾经犯过一个错:把按键中断设得比定时器中断还高。结果按一下按键,定时器就被打断,PWM输出变得不稳定,电机嗡嗡响。后来把优先级调过来,问题就解决了。
2.7 NVIC配置的完整示例
下面是一个完整的NVIC配置流程,以STM32F4为例:
void NVIC_Configuration(void)
{
// 1. 设置优先级分组:2位抢占,2位子优先级
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
// 2. 配置USART1中断
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 3. 配置定时器中断(更高优先级)
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 更高
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
你看,配置其实不复杂。关键是想清楚:哪个中断更重要?哪个能等?
2.8 中断延迟——一个容易被忽略的问题
NVIC虽然快,但也不是瞬间响应。从中断触发到CPU开始执行中断服务函数,中间有固定的延迟:
- Cortex-M3/M4:通常12个CPU周期
- Cortex-M0/M0+:通常16个CPU周期
这个延迟在大多数场景下可以忽略。但如果你在做高速数据采集或实时控制,就得算一算了。我记得有个项目要求中断响应在1微秒以内,当时CPU主频是72MHz,12个周期就是约167纳秒,嗯,够用。
核心要点回顾:
- NVIC是中断的调度中心,管使能、优先级、向量映射
- 优先级分组决定了抢占和子优先级的位数分配
- 先设优先级,再使能中断——这个顺序别搞反
- 向量表重定向要注意对齐
- 中断延迟在高速场景下需要关注
好了,NVIC的核心内容就这些。配置起来其实不复杂,但一定要想清楚优先级分配。我见过太多系统因为优先级没设好,出现各种奇怪的问题。记住一句话:高优先级给时间敏感的任务,低优先级给能等的任务。