第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域的部分区域。

我画了张图,把主要的内存块和地址范围标了出来。你看一眼就明白了。

STM32H7 多核内存映射与共享区域 D1域 (Cortex-M7 主域) • ITCM / DTCM (0x0000 0000) • AXI-SRAM (0x2400 0000) 512KB, 主核直接访问 • Flash (0x0800 0000) • D1域外设 D2域 (Cortex-M4 从域) • SRAM1 (0x3000 0000) • SRAM2 (0x3002 0000) • SRAM3 (0x3004 0000) • D2域外设 • 备份SRAM (0x3880 0000) D3域 (系统控制) • SRAM4 (0x3800 0000) • 备份寄存器 • RTC / 电源控制 • 低速外设 共享内存区域(多核通信核心) • AXI-SRAM (0x2400 0000 - 0x2407 FFFF): M7/M4均可通过AHB总线访问 • D2域SRAM (0x3000 0000 - 0x3007 FFFF): M4直接访问,M7需通过AHB桥 注:M7核通过AXI总线访问D1域,通过AHB桥访问D2/D3域 M4核通过AHB总线直接访问D2域,通过AHB桥访问D1/D3域 ⚠ 避坑:M7访问D2域SRAM时,必须关闭对应区域的Cache!

看到没?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还不够。实际通信时,还需要一套握手协议。我常用的模式是这样的:

  1. M4写数据到共享缓冲区,然后写一个“数据就绪”标志位
  2. M7轮询标志位,发现就绪后读取数据
  3. 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传输越界了,写到了共享区域之外。

定位方法:

  1. 加校验:每个数据包尾部加CRC32。M7读取时校验,发现错误就丢弃并记录。
  2. MPU异常捕获:在共享区域前后各放一个MPU保护区,配置为“不可访问”。一旦有代码越界访问,立即触发MemManage Fault。
  3. 日志标记:在共享内存里放一个时间戳和任务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要关、保护要严”。你把这三点拿捏住了,多核通信就稳了八成。剩下的两成,靠调试和耐心。