第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秒翻转一次的方波。如果看不到,检查以下几点:

  1. 定时器重装载值是否正确?CPU频率别算错了
  2. NVIC优先级配置是否生效?读回寄存器确认
  3. 中断向量表地址是否正确?特别是做了重定位后
  4. 全局中断是否使能?别忘了__enable_irq()

我的调试习惯:每次配置完中断,我都会先写一个最简单的处理函数——就翻转一个GPIO。这样用示波器一看就知道中断有没有跑起来。等确认中断正常了,再往里填实际逻辑。这招帮我省了无数调试时间。

小结

NVIC是Cortex-R的中枢神经。优先级配置决定了系统的实时性,向量表重定位给了你灵活性,而定时器中断则是ADAS控制周期的基石。

我个人觉得,搞懂NVIC,你就掌握了Cortex-R的一半。剩下的,无非是外设驱动和算法实现。下一章,我们聊聊更刺激的——内存保护单元(MPU),让你的ADAS系统不再“裸奔”。