第4章:内存映射与共享内存——AXI-SRAM与D2域的那些坑
内存映射这东西,说白了就是给CPU一张地址地图。STM32H7这种多核芯片,内存布局比单核复杂得多。我刚开始接触H7时,就被AXI-SRAM和D2域绕晕过——明明地址写对了,数据就是读不到。后来才发现,是Cache捣的鬼。
这一章,咱们就把内存映射、共享内存、非Cacheable区域和MPU保护串起来讲。你跟着我走一遍,基本就能避开我当年踩过的坑。
4.1 AXI-SRAM与D2域内存映射全景
先看整体布局。STM32H7系列把内存分成了几个大域:D1、D2、D3。每个域有自己的总线矩阵和内存资源。
核心要点:多核通信的关键,在于找到双方都能访问的“中立区”。这个中立区,通常就是AXI-SRAM和D2域的部分区域。
我画了张图,把主要的内存块和地址范围标了出来。你看一眼就明白了。
看到没?AXI-SRAM在D1域,是M7的“自留地”。但M4也能通过AHB桥访问它。反过来,D2域的SRAM是M4的主场,M7要访问就得走AHB桥。这个桥,就是性能瓶颈所在。
4.2 非Cacheable区域配置——血的教训
为什么非要配非Cacheable?我讲个真实案例。
之前做双核数据采集系统,M7负责FFT运算,M4负责前端采样。我在AXI-SRAM里划了块区域做环形缓冲区,M4往里写,M7往外读。结果呢?M7读到的数据总是“旧”的——明明M4已经写了新值,M7读出来的还是几毫秒前的数据。
排查了两天,最后发现是M7的Cache在作怪。M7第一次读了那块地址后,数据就被缓存了。后续读取直接从Cache返回,根本没走总线。M4写的新数据,自然读不到。
核心原则:多核共享的内存区域,必须配置为非Cacheable(或强序)。否则,Cache一致性会把你搞疯。
配置方法有两种:
- MPU法:通过MPU将共享区域设置为非Cacheable、非Bufferable
- 属性覆盖法:利用STM32H7的AXI总线属性覆盖寄存器
我个人更推荐MPU法,因为更灵活,可以按页粒度控制。下面给出配置代码:
/* 配置AXI-SRAM共享区域为非Cacheable */
/* 假设共享区域:0x2404 0000 - 0x2404 FFFF (64KB) */
void MPU_Config_SharedMemory(void)
{
MPU_Region_InitTypeDef MPU_InitStruct = {0};
/* 禁用MPU,配置期间必须关闭 */
HAL_MPU_Disable();
/* 配置共享区域 */
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x24040000;
MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
MPU_InitStruct.SubRegionDisable = 0x00;
MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
/* 关键:非Cacheable,非Bufferable */
MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 使能MPU */
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
/* 注意:配置后需要清空Cache,防止旧数据残留 */
SCB_CleanInvalidateDCache();
}
小技巧:配置完MPU后,记得调用 SCB_CleanInvalidateDCache()。我吃过这个亏——配完MPU没清Cache,结果还是读到旧数据。清一下,世界就清净了。
4.3 MPU保护单元配置实战
MPU不只是用来配Cache属性的。它还能做内存保护——防止M7乱写M4的私有内存,或者反过来。
我习惯把MPU区域分成三类:
| 区域类型 | 地址范围 | Cache属性 | 访问权限 | 用途 |
|---|---|---|---|---|
| 共享内存 | 0x2404 0000 - 0x2404 FFFF | 非Cacheable | 读写 | 双核通信缓冲区 |
| M4私有内存 | 0x3000 0000 - 0x3003 FFFF | Write-Through | M7只读,M4读写 | 保护M4关键数据 |
| 外设寄存器 | 0x4000 0000 - 0x5FFF FFFF | 非Cacheable | 特权模式访问 | 防止用户态误操作 |
这里有个细节:M4私有内存我配成了Write-Through,而不是非Cacheable。为什么?因为M7读M4的数据时,Write-Through能保证M7读到的是最新值(写操作直接穿透到内存),同时读性能比非Cacheable好一些。算是个折中方案。
4.4 共享内存通信的完整流程
光配好MPU还不够。实际通信时,还需要一套握手协议。我常用的模式是这样的:
- M4写数据到共享缓冲区,然后写一个“数据就绪”标志位
- M7轮询标志位,发现就绪后读取数据
- M7读完后清除标志位,通知M4可以写下一批
代码实现大概长这样:
/* 共享内存结构体定义 */
typedef struct {
uint32_t flag; /* 0: 空闲, 1: 数据就绪 */
uint32_t length; /* 数据长度 */
uint8_t data[4096]; /* 数据缓冲区 */
} __attribute__((aligned(32))) SharedBuffer_t;
/* 确保结构体在非Cacheable区域 */
#define SHARED_BUF ((SharedBuffer_t *)0x24040000)
/* M4核:写入数据 */
void M4_SendData(uint8_t *src, uint32_t len)
{
/* 等待上一帧被M7取走 */
while (SHARED_BUF->flag != 0);
/* 拷贝数据 */
memcpy(SHARED_BUF->data, src, len);
SHARED_BUF->length = len;
/* 内存屏障:确保数据写完后再置标志 */
__DSB();
SHARED_BUF->flag = 1;
__DSB();
}
/* M7核:读取数据 */
void M7_ReceiveData(uint8_t *dst, uint32_t *len)
{
/* 轮询标志 */
while (SHARED_BUF->flag == 0);
/* 读取数据 */
*len = SHARED_BUF->length;
memcpy(dst, SHARED_BUF->data, *len);
/* 清除标志,通知M4 */
__DSB();
SHARED_BUF->flag = 0;
__DSB();
}
关键点:注意代码中的 __DSB()——数据同步屏障。它保证前面的内存操作完成后,才执行后面的指令。没有这个屏障,M7可能读到flag=1但data还没写完。我遇到过这种问题,加了屏障就好了。
4.5 性能调优:如何减少共享内存访问延迟
非Cacheable区域的访问速度确实比Cacheable慢。实测下来,AXI-SRAM的非Cacheable读操作大约需要8-10个CPU周期,而Cacheable命中时只要1-2个周期。
怎么优化?我分享几个实战经验:
- 减少访问次数:尽量一次读/写大块数据,而不是频繁小量读写。比如用DMA传输,而不是CPU逐字节拷贝。
- 对齐访问:共享缓冲区32字节对齐,配合AXI总线的burst传输,效率能提升30%以上。
- 使用双缓冲:一块区域用于M4写,另一块用于M7读,交替使用。避免同一块内存的读写冲突。
- 降低轮询频率:用中断代替轮询。M4写完数据后触发一个中断给M7,M7响应中断来读取。CPU占用率能降一个数量级。
我的习惯:在共享内存的头部放一个16字节的控制块,包含标志、长度、校验和。数据区从偏移16开始。这样控制块和数据区天然分离,不容易踩踏。
4.6 故障定位:共享内存踩踏怎么办?
共享内存最怕什么?踩踏。M7和M4同时写同一个地址,数据就乱了。
我曾经排查过一个诡异的问题:系统运行几小时后,M7读到的数据偶尔会多出几个字节的乱码。查了三天,最后发现是M4的DMA传输越界了,写到了共享区域之外。
定位方法:
- 加校验:每个数据包尾部加CRC32。M7读取时校验,发现错误就丢弃并记录。
- MPU异常捕获:在共享区域前后各放一个MPU保护区,配置为“不可访问”。一旦有代码越界访问,立即触发MemManage Fault。
- 日志标记:在共享内存里放一个时间戳和任务ID,每次写操作都更新。出问题时,看最后是谁写的,就能锁定嫌疑核。
/* MPU保护区配置示例:在共享区域前后各加4KB保护区 */
/* 前保护区:0x2403 F000 - 0x2403 FFFF */
MPU_InitStruct.BaseAddress = 0x2403F000;
MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
MPU_InitStruct.AccessPermission = MPU_REGION_NO_ACCESS; /* 禁止访问 */
HAL_MPU_ConfigRegion(&MPU_InitStruct);
/* 后保护区:0x2405 0000 - 0x2405 0FFF */
MPU_InitStruct.BaseAddress = 0x24050000;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
注意:MPU保护区的大小必须是2的幂次,且地址对齐。比如4KB保护区,地址必须4KB对齐。这个限制容易忽略,配错了MPU直接不工作。
嗯,内存映射和共享内存这块,说白了就是“地址要对、Cache要关、保护要严”。你把这三点拿捏住了,多核通信就稳了八成。剩下的两成,靠调试和耐心。