3、多核MCU硬件基础:ARM Cortex-R系列与多核架构、内存模型与一致性、中断控制器(GIC)原理
好,我们进入第三讲。说实话,多核调度能不能做好,一半的功夫在硬件理解上。你如果不清楚Cortex-R系列到底长什么样,不知道GIC怎么分发中断,那写出来的调度代码,跑起来大概率是「看起来对,一压测就崩」。
我个人习惯,在讲AUTOSAR调度之前,先花一节课把硬件底子打牢。今天我们就聊三个核心:Cortex-R系列多核架构、内存模型与一致性、GIC中断控制器。
3.1 ARM Cortex-R系列与多核架构
Cortex-R系列,说白了就是为「实时」而生的。跟Cortex-A(跑Linux那种)不一样,R系列追求的是低延迟、高确定性。我在项目中遇到过,用R5F跑AUTOSAR OS,中断响应可以控制在几十个纳秒级别,这在A系列上很难做到。
3.1.1 核心特点
- 有序流水线:不乱序执行,时序可预测。嗯,这点对实时系统太重要了。
- 紧耦合内存(TCM):CPU直连,零等待。我建议把中断向量表和关键任务栈放在TCM里。
- 硬件锁步(Lockstep):两个核跑同样的代码,比较器实时校验。适合ASIL-D场景。
- 多核簇:比如Cortex-R52,最多支持4核在一个簇内,共享外设和内存。
关键点:Cortex-R系列的多核,不是简单的「把几个CPU焊在一起」。它们通过ACE(AMBA Coherency Extension)总线互联,硬件上就支持缓存一致性。
3.1.2 多核启动流程
你想想看,上电后所有核都从Reset向量开始跑。但AUTOSAR OS要求只有一个核做初始化。怎么做?
// 伪代码:多核启动
Core0:
// 1. 初始化系统时钟、GIC、内存保护
// 2. 设置Core1的启动地址到GIC的SGI寄存器
// 3. 发送SGI中断唤醒Core1
// 4. 继续初始化OS
Core1:
// 1. 等待SGI中断
// 2. 跳转到指定的启动地址
// 3. 执行核本地初始化
// 4. 进入空闲循环,等待OS调度
我曾经在R52上调试启动,发现Core1总是起不来。查了半天,原来是GIC的SGI中断使能位没配。这种坑,你光看手册是想不到的。
3.2 内存模型与一致性
多核编程最大的噩梦是什么?数据不一致。Core0写了一个变量,Core1读到的却是旧值。为什么会这样?因为每个核有自己的Cache。
3.2.1 内存模型
ARM Cortex-R系列使用的是弱一致性内存模型(Weakly Ordered)。什么意思?就是CPU可以乱序访问内存,只要不影响单核程序逻辑。但多核场景下,这就出问题了。
| 内存类型 | 特点 | 典型用途 |
|---|---|---|
| Normal | 可缓存,允许合并和重排 | 代码、数据段 |
| Device | 不可缓存,按序访问 | 外设寄存器 |
| Strongly Ordered | 严格按序,所有核可见 | 中断控制器、系统控制 |
我的经验:AUTOSAR中的Spinlock和共享数据区,一定要放在Normal内存,但访问前后要加内存屏障。Device内存虽然安全,但性能太差,不适合高频访问。
3.2.2 缓存一致性协议
Cortex-R系列多核簇内,用的是MESI协议的变种。每个Cache Line有四种状态:Modified、Exclusive、Shared、Invalid。
举个例子:Core0读变量x,Cache Line变成Exclusive。Core1也读x,Core0的Line变成Shared。Core0写x,先发Invalidate信号给Core1,Core1的Line变成Invalid,Core0的变成Modified。
听起来完美?但有个坑:伪共享(False Sharing)。两个核各自操作不同的变量,但这两个变量恰好在同一个Cache Line里。每次写操作都会导致整个Line失效,性能断崖式下跌。
避坑指南:我曾经在AUTOSAR的RTE层调试,发现两个核的通信延迟忽高忽低。最后定位到是两个核的共享变量紧挨着定义。解决办法:每个共享变量后面加padding,强制对齐到64字节(一个Cache Line大小)。
3.2.3 内存屏障指令
ARM提供了三条屏障指令,我建议你背下来:
- DMB:数据内存屏障。保证DMB之前的所有内存访问,在DMB之后的内存访问之前完成。
- DSB:数据同步屏障。等待所有内存访问完成,才执行下一条指令。
- ISB:指令同步屏障。清空流水线,保证后续指令从内存重新读取。
// 典型用法:释放Spinlock前
// 保证临界区的写操作对其他核可见
__asm volatile("DSB" ::: "memory");
// 然后写lock变量
lock = 0;
嗯,这里要注意:AUTOSAR OS的GetSpinlock/ReleaseSpinlock内部已经加了屏障,你写应用层代码时不需要手动加。但如果你自己实现核间通信,一定要记得加。
3.3 中断控制器(GIC)原理
GIC,全称Generic Interrupt Controller。ARM多核系统的中断管理,全靠它。目前主流是GICv2和GICv3。Cortex-R系列大多用GICv2。
3.3.1 GIC架构
GIC分两部分:
- Distributor:全局控制。配置中断优先级、使能、目标核。
- CPU Interface:每个核一个。处理中断的确认、完成、优先级掩码。
中断类型有三种:
| 类型 | ID范围 | 说明 |
|---|---|---|
| SGI | 0-15 | 软件触发中断,核间通信用 |
| PPI | 16-31 | 私有外设中断,每个核独有(如本地定时器) |
| SPI | 32-1019 | 共享外设中断,可以路由到任意核 |
重点:AUTOSAR OS的CAT2中断(带OS服务的中断)通常用SPI。CAT1中断(无OS服务,直接处理)可以用PPI或SPI。我个人习惯把高频率的CAT1中断绑到特定核的PPI上,避免核间干扰。
3.3.2 中断路由与亲和性
GICv2支持两种路由方式:
- 1:N:一个中断可以发给多个核。但只有一个核会响应(取决于优先级和运行状态)。
- 1:1:指定发给某个核。
在AUTOSAR配置中,你可以在GicConfigSet里设置每个中断的TargetCpu。我建议:
- 定时器中断:绑到负责调度的核(通常是Core0)
- CAN/LIN接收中断:绑到负责对应通信栈的核
- 核间中断(SGI):用于OS的IOC和ScheduleTable同步
小技巧:我曾经调试一个多核系统,发现Core2的CAN中断响应延迟特别大。用示波器一量,发现中断从外设到GIC再到Core2,路径上经过了一个桥接总线。解决办法:把CAN中断改到Core0,Core0再通过SGI通知Core2。延迟从15us降到了3us。
3.3.3 中断优先级与抢占
GICv2支持256级优先级(0最高,255最低)。AUTOSAR OS把中断优先级分成两组:
- OS-ISR Category 2:优先级高于OS调度器,可以抢占Task。
- OS-ISR Category 1:优先级低于OS调度器,不参与Task抢占。
配置时要注意:CAT2中断的优先级必须高于OS调度器的优先级阈值。这个阈值在OsCfg里配,叫OsInterruptPriorityThreshold。
// 典型配置示例
// GIC优先级寄存器设置
GICD_IPRIORITYR[IRQ_CAN_RX] = 0x10; // 高优先级,CAT2
GICD_IPRIORITYR[IRQ_TIMER] = 0x80; // 中等优先级,CAT2
GICD_IPRIORITYR[IRQ_GPIO] = 0xF0; // 低优先级,CAT1
避坑指南:我曾经犯过一个错误,把CAT1中断的优先级设得比CAT2还高。结果CAT1中断频繁打断OS调度,导致Task超时。记住:CAT1中断的优先级必须低于OsInterruptPriorityThreshold,CAT2必须高于它。
3.3.4 中断延迟优化
多核场景下,中断延迟受几个因素影响:
- GIC分发延迟:Distributor把中断路由到目标核的时间。通常几个时钟周期。
- CPU Interface延迟:核从IDLE状态唤醒的时间。如果核在执行高优先级中断,低优先级中断会被阻塞。
- OS调度延迟:如果中断触发了OS调度(如激活Task),还要加上调度器执行时间。
我个人的优化经验:
- 把时间关键的中断绑到空闲核上,避免排队。
- 使用GIC的中断分组功能,把安全相关中断和非安全中断分开。
- 在TCM里放中断服务函数,减少指令Cache Miss。
好了,这一章的内容就这些。硬件基础打牢了,下一章我们才能真正开始聊「多核任务调度」本身。你想想看,如果连GIC怎么配、Cache一致性怎么保证都不清楚,调度优化就是空中楼阁。
嗯,今天就到这里。有问题随时找我。