4. 向量表与中断映射:多核系统中中断控制器(NVIC/GIC)的配置与路由

中断,说白了就是嵌入式系统的“神经反射”。单核时代,你写个中断服务函数,挂到向量表上,完事。但到了多核系统,事情就变得微妙了——这个中断来了,到底该由哪个核去处理?是 Core0 还是 Core1?能不能动态切换?

我刚开始接触 STM32H7 的双核系列时,就踩过一个坑:外设中断只路由到了 Core0,Core1 那边死活进不了中断。查了两天才发现,是中断分发器的配置没做对。嗯,这一章我们就来把 NVIC 和 GIC 的配置逻辑彻底理清楚。

4.1 单核与多核的中断控制器差异

先看个表格,把 NVIC 和 GIC 的核心区别摆出来:

特性 NVIC(单核/CM7) GIC(多核/A 系列)
支持核数 1 个 最多 8 个
中断路由 固定到唯一核 可编程路由到指定核
优先级模型 8 位抢占优先级 + 8 位子优先级 分组优先级模型(Group/Sub)
中断号范围 0~239(STM32 常用 0~81) ID0~1019(SPI、PPI、SGI)
软件触发中断 不支持 支持 SGI(Software Generated Interrupt)

STM32H7 双核系列(比如 H745/H747)用的是双 Cortex-M7 + M4,每个核都有自己的 NVIC。但外设中断线是共享的,需要通过 中断分发器(Interrupt Distributor) 来决定每个中断去哪个核。

核心要点:多核系统中,中断路由不是自动的。你必须显式配置每个中断的目标核。否则,中断可能永远到不了你期望的核。

4.2 中断分发器的工作原理

STM32H7 的中断分发器,本质上是一个可编程的交叉开关。每个外设中断源(比如 TIM1、UART4)都有一个对应的路由寄存器位,你可以设置它路由到 Core0、Core1,或者同时路由到两个核。

我画了一张图,帮你理解这个结构:

STM32H7 双核中断分发架构 TIM1 中断 UART4 中断 DMA1 中断 EXTI 中断 中断分发器 (Interrupt Distributor) 路由寄存器 优先级分组 使能控制 Core0 (CM7) NVIC_0 Core1 (CM4) NVIC_1 说明: 每个外设中断源通过分发器独立配置目标核。可以路由到单一核,也可以广播到两个核。 路由配置在系统启动时完成,运行中也可以动态修改(但要注意竞态条件)。

4.3 中断路由的配置方法

在 STM32H7 上,中断路由是通过 HAL_ExclusiveConfig 或直接操作 SYSCFG 寄存器来实现的。具体来说,每个外设中断对应一个 SYSCFG_ITLINE 寄存器中的位。

举个例子,把 UART4 的中断路由到 Core1:

/* 使能 SYSCFG 时钟 */
__HAL_RCC_SYSCFG_CLK_ENABLE();

/* 将 UART4 中断路由到 Core1(CM4) */
SYSCFG->ITLINE0 |= SYSCFG_ITLINE0_UART4_ITF_CM4;

/* 在 Core1 上使能 NVIC */
HAL_NVIC_SetPriority(UART4_IRQn, 5, 0);
HAL_NVIC_EnableIRQ(UART4_IRQn);

如果你想让中断同时到达两个核,就设置两个位:

/* 同时路由到 Core0 和 Core1 */
SYSCFG->ITLINE0 |= (SYSCFG_ITLINE0_UART4_ITF_CM7 | SYSCFG_ITLINE0_UART4_ITF_CM4);

注意:同时路由到两个核时,两个核都会响应同一个中断。如果两个核的中断服务函数操作了同一个资源,必须加互斥保护。我曾经在一个项目中因为没注意这个,导致两个核同时修改一个全局变量,数据全乱了。

4.4 中断优先级在多核中的处理

每个核的 NVIC 都有自己的优先级寄存器。这意味着,同一个中断号在 Core0 和 Core1 上可以有不同的优先级。

但这里有个陷阱:硬件仲裁优先级。当两个核同时收到中断时,硬件会先比较中断号,再比较优先级。具体规则是:

  • 中断号越小,硬件优先级越高
  • 中断号相同时,比较每个核上配置的软件优先级

我建议你在项目初期就定好优先级分配策略:

中断类型 建议路由核 优先级范围 说明
实时性要求高的(如定时器) Core0 0~3 Core0 通常跑 RTOS,响应更快
数据量大的(如 DMA) Core1 4~7 Core1 做数据搬运,不阻塞主控
低频事件(如按键) 任意核 8~15 优先级放低,不影响关键任务
核间通信(SGI) 目标核 最高(0) 用于核间同步,必须快速响应

4.5 核间中断(SGI)的使用

多核系统里,核间中断是个好东西。你可以让 Core0 写一个寄存器,然后触发一个 SGI 通知 Core1 去读。这样比轮询效率高得多。

在 STM32H7 上,核间中断是通过 HSEM(硬件信号量)和 IPCC(核间通信控制器)实现的。但最直接的方式是用 SYSCFG 的软件触发中断:

/* Core0 触发 Core1 的中断 */
SYSCFG->ITLINE0 |= SYSCFG_ITLINE0_SW_ITF_CM4;

/* Core1 上要提前使能这个中断 */
/* 在 Core1 的启动代码中: */
HAL_NVIC_SetPriority(SW_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(SW_IRQn);

小技巧:我习惯在核间中断的服务函数里只放一个标志位,不做复杂处理。真正的业务逻辑放在主循环里检查这个标志。这样可以避免中断嵌套导致的死锁问题。

4.6 故障定位:中断不响应的常见原因

调试多核中断时,最头疼的就是“中断没反应”。我总结了几条排查路径:

  1. 检查路由配置:确认 SYSCFG_ITLINE 寄存器里对应的位是否设置正确。用调试器直接读寄存器最靠谱。
  2. 检查 NVIC 使能:每个核的 NVIC 都要单独使能。Core0 使能了不代表 Core1 也使能了。
  3. 检查优先级分组:两个核的优先级分组必须一致。一个用 4 位抢占 + 4 位子优先级,另一个用 3 位抢占 + 5 位子优先级,中断行为会乱掉。
  4. 检查中断标志位:外设的中断标志位必须在对应的核上清除。Core0 清除了标志,但 Core1 的中断服务函数里没清,会导致 Core1 反复进中断。
  5. 检查时钟:外设时钟没使能,中断根本不会产生。这个低级错误我犯过不止一次。

避坑指南:我曾经在一个双核项目中,Core0 和 Core1 都使能了同一个外设中断,但只有 Core0 在清除标志。结果 Core1 的中断一直挂起,系统响应越来越慢。最后用逻辑分析仪抓中断引脚才发现问题。所以,谁清除标志,谁负责中断服务,这个原则一定要定好。

4.7 实战:配置一个双核中断系统

最后,我们走一遍完整的配置流程。假设我们要把 TIM1 的中断路由到 Core1,并在 Core1 上处理:

步骤 1:在 Core0 上配置路由

/* Core0 代码 */
void SystemClock_Config(void) {
    /* ... 时钟配置 ... */
    __HAL_RCC_SYSCFG_CLK_ENABLE();
    SYSCFG->ITLINE0 |= SYSCFG_ITLINE0_TIM1_ITF_CM4;
}

步骤 2:在 Core1 上使能中断

/* Core1 代码 */
void Core1_Init(void) {
    HAL_NVIC_SetPriority(TIM1_IRQn, 3, 0);
    HAL_NVIC_EnableIRQ(TIM1_IRQn);
}

void TIM1_IRQHandler(void) {
    /* 清除标志 */
    __HAL_TIM_CLEAR_IT(&htim1, TIM_IT_UPDATE);
    /* 处理业务 */
    g_tim1_flag = 1;
}

步骤 3:验证

用调试器挂到 Core1 上,在 TIM1_IRQHandler 里设断点。触发 TIM1 中断,看能不能停住。如果停不住,回头检查路由寄存器和 NVIC 使能位。

嗯,这一章的内容就到这里。中断路由看起来简单,但实际调试时坑不少。记住:先查路由,再查使能,最后查标志清除。按这个顺序排查,大部分问题都能解决。