第4章:中断处理与嵌套向量中断控制器(NVIC)
中断,是嵌入式系统的灵魂。没有中断,CPU就得像个傻子一样轮询,效率低得吓人。在ADAS系统里,中断更是命根子——摄像头数据来了你得立刻处理,雷达报警你得马上响应,刹车指令更是半毫秒都不能耽误。
这一章,我们聊聊ARM Cortex-R系列里的NVIC。说白了,它就是中断的“总调度官”。
4.1 NVIC架构:谁在管这些中断?
NVIC全称是Nested Vectored Interrupt Controller,嵌套向量中断控制器。名字很长,但功能很纯粹:管理所有中断的优先级、响应顺序、以及向量跳转。
Cortex-R系列的NVIC和Cortex-M系列很像,但有个关键区别——R系列支持更灵活的中断优先级分组,而且对实时性要求更高。我当年从M4转到R5时,第一反应是“这玩意儿怎么配置这么复杂?”后来才发现,复杂是为了更精细的控制。
NVIC的核心特性:
- 支持多达240个中断源——ADAS系统里,传感器、通信、执行器全算上,绰绰有余
- 8级可编程优先级——注意,是8级,不是M系列的16级。R系列更强调确定性
- 硬件自动压栈/出栈——中断发生时,CPU自动保存现场,不用你操心
- 尾链技术(Tail Chaining)——连续中断时,省去多余的出栈入栈,速度飞快
关键点:NVIC的寄存器映射在系统控制块(SCB)中,基地址通常是0xE000E000。别记错了,我见过有人把地址写错,结果中断死活不响应,查了两天才发现是地址偏移算错了。
4.2 中断优先级与抢占:谁说了算?
中断优先级,是NVIC最核心的概念。你想想看,如果刹车中断和娱乐系统中断同时来了,谁先处理?当然是刹车。
Cortex-R的优先级分两部分:抢占优先级和子优先级。抢占优先级决定能不能打断当前中断,子优先级决定同级别中断谁先执行。
配置优先级分组,靠的是寄存器PRIGROUP。我习惯用这样的分组方式:
| PRIGROUP值 | 抢占优先级位数 | 子优先级位数 | 适用场景 |
|---|---|---|---|
| 0 | 3 | 0 | 所有中断平等,无子优先级 |
| 1 | 2 | 1 | 两级抢占,两级子优先级 |
| 2 | 1 | 2 | 一级抢占,三级子优先级 |
| 3 | 0 | 3 | 无抢占,纯轮询 |
在ADAS里,我通常用PRIGROUP=1。为什么?因为我们需要两级抢占:最高级给安全关键中断(刹车、转向),次级给普通传感器数据。子优先级用来区分同级别中断的先后顺序。
我的经验:优先级数值越小,优先级越高。这和直觉相反,很多人一开始会搞混。我曾经在项目里把刹车中断设成了优先级7(最低),结果油门中断优先级0(最高)先响应了……嗯,那次测试差点出事。从那以后,我每次配置优先级都会反复核对。
4.3 中断向量表重定位:别让中断跑偏了
中断向量表,就是一张“中断处理函数地址表”。CPU收到中断信号后,从这张表里找到对应的函数地址,跳过去执行。
默认情况下,向量表在Flash的起始地址(0x00000000)。但问题来了——如果你要动态更新中断处理函数,或者从RAM里运行代码,向量表就不能锁死在Flash里。
这时候就需要向量表重定位。Cortex-R提供了VTOR寄存器(Vector Table Offset Register),你可以把向量表搬到任意地址。
配置方法很简单:
// 假设我们把向量表搬到RAM的0x20000000地址
#define VECTOR_TABLE_ADDR 0x20000000
// 先拷贝向量表到RAM
memcpy((void*)VECTOR_TABLE_ADDR, (void*)0x00000000, 256 * 4);
// 设置VTOR寄存器
SCB->VTOR = VECTOR_TABLE_ADDR;
嗯,这里要注意:向量表必须对齐到512字节边界。为什么是512?因为Cortex-R的向量表最多256个中断,每个4字节,总共1024字节。但硬件要求对齐到2的幂次,512是最小对齐单位。
避坑指南:我曾经在量产前夜发现中断偶尔跑飞,查了三天,最后发现是VTOR设置时地址没对齐。0x20000100这种地址是不行的,必须0x20000000或0x20000200这种。记住,低9位必须为0。
4.4 实战:配置一个高优先级定时器中断
光说不练假把式。我们直接上手,配置一个定时器中断,优先级设为最高(0),用来做ADAS系统的控制周期基准。
我选的是Cortex-R5内置的系统定时器(SysTick),虽然它通常用于OS心跳,但拿来演示中断配置再合适不过。
步骤1:配置定时器
// SysTick寄存器定义
#define SYSTICK_BASE 0xE000E010
#define STK_CTRL (*((volatile uint32_t *)(SYSTICK_BASE + 0x00)))
#define STK_LOAD (*((volatile uint32_t *)(SYSTICK_BASE + 0x04)))
#define STK_VAL (*((volatile uint32_t *)(SYSTICK_BASE + 0x08)))
// 配置定时器每1ms中断一次(假设CPU频率200MHz)
// 重装载值 = 200MHz / 1000 = 200000
STK_LOAD = 200000 - 1;
STK_VAL = 0; // 清空计数器
// 使能定时器,使能中断,选择处理器时钟
STK_CTRL = 0x07; // bit0=使能, bit1=中断使能, bit2=时钟源选择
步骤2:配置NVIC优先级
// SysTick中断号是15(Cortex-R5固定)
#define SYSTICK_IRQn 15
// 设置抢占优先级为0(最高),子优先级为0
// 注意:NVIC_IPR寄存器每个中断占8位,高4位有效
uint32_t *ipr = (uint32_t *)(0xE000E400 + (SYSTICK_IRQn / 4) * 4);
uint32_t shift = (SYSTICK_IRQn % 4) * 8;
*ipr &= ~(0xFF << shift);
*ipr |= (0x00 << shift); // 优先级0,最高
步骤3:使能中断
// NVIC_ISER寄存器使能中断
uint32_t *iser = (uint32_t *)(0xE000E100);
*iser |= (1 << (SYSTICK_IRQn % 32));
步骤4:编写中断处理函数
// 中断向量表中的函数名必须是 SysTick_Handler
void SysTick_Handler(void)
{
// 清除中断标志(SysTick自动清除,但其他外设需要手动清)
// 读取STK_CTRL寄存器即可清除标志
uint32_t temp = STK_CTRL; // 防止编译器优化
// 在这里执行你的ADAS控制逻辑
// 比如:更新传感器数据、检查安全阈值、触发控制算法
static uint32_t tick_count = 0;
tick_count++;
// 每1000ms(1秒)翻转一次LED,表示系统在运行
if (tick_count % 1000 == 0) {
GPIO_Toggle(LED_PIN);
}
}
重要提醒:中断处理函数要短小精悍。我见过有人把整个控制算法都塞进中断里,结果导致其他中断响应延迟,系统直接崩溃。记住,中断里只做最紧急的事,耗时操作放到任务里做。
验证方法
配置完成后,用示波器抓GPIO引脚,你应该能看到1秒翻转一次的方波。如果看不到,检查以下几点:
- 定时器重装载值是否正确?CPU频率别算错了
- NVIC优先级配置是否生效?读回寄存器确认
- 中断向量表地址是否正确?特别是做了重定位后
- 全局中断是否使能?别忘了
__enable_irq()
我的调试习惯:每次配置完中断,我都会先写一个最简单的处理函数——就翻转一个GPIO。这样用示波器一看就知道中断有没有跑起来。等确认中断正常了,再往里填实际逻辑。这招帮我省了无数调试时间。
小结
NVIC是Cortex-R的中枢神经。优先级配置决定了系统的实时性,向量表重定位给了你灵活性,而定时器中断则是ADAS控制周期的基石。
我个人觉得,搞懂NVIC,你就掌握了Cortex-R的一半。剩下的,无非是外设驱动和算法实现。下一章,我们聊聊更刺激的——内存保护单元(MPU),让你的ADAS系统不再“裸奔”。