4、内存映射与保护:全局内存映射、内存保护单元(MPU)、本地内存与共享内存
好,咱们来聊聊TC3xx的内存系统。说实话,这块内容我第一次接触时也绕了好一阵子。多核架构下,内存不再是简单的「一块RAM」,而是被划分成各种区域,有全局的、有本地的、还有共享的。搞不清楚这些,任务分配和资源保护就无从谈起。
我个人习惯把TC3xx的内存系统想象成一个「小区」。每个CPU核心有自己的「独栋别墅」(本地内存),小区里还有「公共广场」(共享内存),而物业办公室(BMU/MPU)负责管理谁可以进哪个门。嗯,这个比喻虽然糙了点,但道理是通的。
4.1 全局内存映射——一张地图走天下
TC3xx的内存映射,说白了就是给每个物理存储单元分配一个唯一的地址。这个地址是全局的,所有核心、DMA、外设都能看到。
我整理了一张简化的映射表,你感受一下:
| 地址范围 | 用途 | 归属 |
|---|---|---|
| 0x00000000 - 0x0FFFFFFF | Flash / 程序存储 | 全局 |
| 0x10000000 - 0x1FFFFFFF | 本地RAM (每个核独享) | 按核分配 |
| 0x20000000 - 0x2FFFFFFF | 共享RAM (LMU / SRAM) | 全局 |
| 0xF0000000 - 0xFFFFFFFF | 外设寄存器 / 系统控制 | 全局 |
你可能会问:「为什么本地RAM的地址也是全局唯一的?」
原因很简单——调试器、DMA或者其他核心,有时候也需要访问你的本地内存。比如我在项目中调试一个多核通信bug时,就经常用Core0去读Core1的本地RAM,看看它的堆栈是不是被踩了。所以,地址是全局的,但访问权限是受控的。
核心要点:全局内存映射是硬件层面的地址分配。它不关心谁在用,只关心「这个地址对应哪个物理单元」。权限控制,是下一层MPU的事。
4.2 内存保护单元(MPU)——给每个核画个圈
MPU,全称Memory Protection Unit。它不是什么新概念,Cortex-M上也有。但TC3xx的MPU更灵活,也更复杂。
每个核心都有自己的MPU,可以独立配置。你可以给每个核心定义最多16个内存区域,每个区域可以设置:
- 起始地址和大小:比如从0x10000000开始的64KB
- 访问权限:读、写、执行,可以任意组合
- 核心归属:是只允许本核访问,还是允许其他核访问
我记得有一次,一个同事的Core1程序跑飞了,结果把Core0的本地RAM给写坏了。排查了半天,最后发现是MPU没配好,Core1居然有权限写Core0的地盘。从那以后,我每次初始化都会先配好MPU,哪怕只是原型验证阶段。
我的习惯:在系统启动的早期阶段(比如在main()的第一行),就把MPU配死。不要等到任务跑起来了再配,那时候上下文已经乱了,很容易漏掉某些关键路径。
4.3 本地内存与共享内存——独享还是共用?
这是多核编程里最纠结的问题之一。
本地内存(Local Memory):每个核心独享的RAM。访问速度最快,没有总线冲突。适合放:
- 每个核的私有堆栈
- 任务控制块(TCB)
- 中断向量表(每个核可以有自己的IVT)
- 高频访问的局部变量
共享内存(Shared Memory):所有核心都能访问的RAM。通常放在LMU(Local Memory Unit)或全局SRAM中。适合放:
- 全局状态标志
- 核间通信的数据缓冲区
- 大块数据(如图像、日志)
你想想看,如果把所有数据都放在共享内存里,那总线得堵成什么样?每个核读写都要抢总线仲裁,性能直接打折扣。反过来,如果所有数据都放在本地内存,那核间通信就得靠消息传递,延迟又上去了。
所以,我的经验是:「能放本地的尽量放本地,必须共享的才放共享」。听起来像废话,但实际项目中很多人就是图省事,把所有变量都扔到共享区,结果性能一塌糊涂。
我曾经踩过的坑:在共享内存里放了一个全局的环形缓冲区,Core0往里写,Core1往外读。没有加任何同步机制,结果Core0写了一半,Core1就读走了半个数据包。嗯,从那以后我再也不敢在共享内存上裸奔了。要么加锁,要么用硬件支持的核间中断来同步。
4.4 实际配置示例——手把手配MPU
光说不练假把式。咱们看一段实际的MPU配置代码。这是基于iLLD库的,但思路通用。
/* 配置Core0的MPU,保护其本地RAM区域 */
Ifx_Mpu_Config mpuConfig;
/* 清空配置 */
Ifx_Mpu_initConfig(&mpuConfig);
/* 区域0:保护Core0的本地RAM(0x10000000 - 0x1000FFFF) */
mpuConfig.region[0].startAddress = 0x10000000;
mpuConfig.region[0].endAddress = 0x1000FFFF;
mpuConfig.region[0].accessPermission = Ifx_Mpu_AccessPermission_readWrite; /* 本核可读写 */
mpuConfig.region[0].otherAccessPermission = Ifx_Mpu_AccessPermission_noAccess; /* 其他核不可访问 */
mpuConfig.region[0].executePermission = Ifx_Mpu_ExecutePermission_never; /* 不可执行 */
mpuConfig.region[0].enabled = TRUE;
/* 区域1:保护共享内存区域(0x20000000 - 0x20000FFF) */
mpuConfig.region[1].startAddress = 0x20000000;
mpuConfig.region[1].endAddress = 0x20000FFF;
mpuConfig.region[1].accessPermission = Ifx_Mpu_AccessPermission_readWrite;
mpuConfig.region[1].otherAccessPermission = Ifx_Mpu_AccessPermission_readOnly; /* 其他核只能读 */
mpuConfig.region[1].executePermission = Ifx_Mpu_ExecutePermission_never;
mpuConfig.region[1].enabled = TRUE;
/* 应用配置到Core0 */
Ifx_Mpu_applyConfig(&mpuConfig, Ifx_Mpu_Core_0);
这段代码做了两件事:
- Core0的本地RAM,其他核心碰都不能碰。
- 共享内存区域,其他核心只能读,不能写。
这样一来,Core0的私有数据就安全了。其他核想写共享区?得通过Core0来代理,或者用核间通信机制。虽然麻烦了点,但安全啊。
4.5 总结一下
内存映射与保护,说白了就是两件事:
- 映射:告诉硬件,每个地址对应哪块物理存储。
- 保护:告诉硬件,谁可以怎么访问这块存储。
我个人的建议是:在设计阶段就把内存分区图画出来。哪个核用哪块本地RAM,哪块共享RAM用来做什么,MPU规则怎么配。别等到代码写了一半再来补,那时候改起来成本就高了。
嗯,这一章就到这里。下一章咱们聊聊任务分配——怎么把不同的功能模块合理地分配到各个核心上,让它们各司其职,又不打架。