在双核 MCU 上做共享内存通信,本以为直接读写内存最简单高效,却没想到掉进了 D-Cache 的坑。本文记录了我从怀疑同步标志位到最终发现缓存一致性问题的全过程,并总结了针对异构双核的共享内存通信开发经验。
一次调试,让我彻底理解了什么叫“真正的共享内存”。

最近的一个项目中,我在一颗双核 MCU(主核 A + 辅核 B)上做共享内存通信时,遇到了一个看似“偶发”的问题。
这次经历让我对 双核之间的共享内存同步缓存一致性、以及异构核通信的实时性有了更深刻的理解。

一、偶发异常“旧数据”

一开始设计的时候,我想让两个核之间直接通过一块预留的共享内存进行数据交互,避免使用 RPMsg 等复杂框架,以获得更高的实时性。

于是我定义了一个简单的数据结构:

typedef struct {
    __IO uint32_t param_ready;
    __IO uint32_t param_id;
    __IO uint8_t  payload[32];
} SHARED_PARAM_T;

#define SHM_ADDR   (0x20500000)
#define SHARED_PARAM   (*(SHARED_PARAM_T *)SHM_ADDR)

通信流程非常直接:

  • 核 A 写入数据并设置 param_ready = 1
  • 核 B 轮询该标志并读取数据,处理完后清零。

逻辑看似完美,实测初期也没问题。
但随着运行时间变长,我发现核 B 偶尔会读取到“旧数据”。标志位已经置 1,可 payload 内容却没有更新。

二、同步顺序有问题?

我一开始怀疑是编译器优化或执行顺序问题,于是加上了 __IO,甚至在写入和读取时都插入了内存屏障:

写入:

memcpy(SHARED_PARAM.payload, src, 32);
__DSB();
SHARED_PARAM.param_ready = 1;

读取:

if (SHARED_PARAM.param_ready) {
    __DSB();
    process(SHARED_PARAM.payload);
    SHARED_PARAM.param_ready = 0;
}

但即便如此,问题仍然偶尔出现。
这意味着同步逻辑本身没问题,而是更底层的东西出了错。

三、真正的原因:缓存一致性

继续深入研究后,我发现问题的根源在于 D-Cache
虽然这块内存被定义为“共享区”,但实际上每个核都有独立的 D-Cache。
如果共享内存区域被标记为可缓存(Cacheable),写入数据可能只是暂存在本核的 Cache 中,另一个核看不到。

也就是说:

  • 核 A 写完数据后,数据仍在 A 的 Cache 里;
  • 核 B 从自己的 Cache 中读到的仍是旧数据;

表面上是“偶发不同步”,实质是 Cache 未同步导致的数据不一致

四、解决方案:让共享内存真正“共享”

方法一:使用 Non-cacheable 内存区域(推荐)

最根本的解决办法是让共享区完全不经过缓存。

可以在分散加载文件或者链接脚本中单独划出一段 Non-cacheable 内存作为共享内存空间:

#if defined(__use_shmem__)
#define m_shmem_start                  0x20500000
#define m_shmem_size                   0x00040000
#endif

#if defined(__use_shmem__)
  ; shared memory data
  SH_MEM    m_shmem_start    m_shmem_size
  {
    .ANY (sh_mem_section)
  }
#endif

并在 MPU 配置中设置属性为:

  • Normal memory
  • Non-cacheable
  • Shareable

最后在代码中显式放置共享数据结构:

typedef struct {
    __IO uint32_t param_ready;
    __IO uint32_t param_id;
    __IO uint8_t  payload[32];
} SHARED_PARAM_T;

__attribute__((section("sh_mem_section"))) SHARED_PARAM_T gSharedParam;

这样,两个核看到的永远是同一份“真实的”内存。

✅ 方法二:手动失效化 Cache(实际不推荐)

也可以在通信前后手动无效化 Cache:

// 核 A 写完数据后
SCB_CleanDCache_by_Addr((uint32_t *)&SHARED_PARAM, sizeof(SHARED_PARAM));

// 核 B 读数据前
SCB_InvalidateDCache_by_Addr((uint32_t *)&SHARED_PARAM, sizeof(SHARED_PARAM));

这种方式虽然也能用,但会导致实时性下降。毕竟清空Cache的操作本身也会消耗时间。
对特别频繁通信的场景,更推荐第一种方案。

五、延伸思考:异构双核的频率差异

这次调试还让我意识到一个常被忽略的点:
在异构双核(例如主核 200MHz、副核 800MHz)的系统中,通信双方的频率差异大也可能放大同步问题。
如果读取核心频率低,用轮询读取共享标志位时,可能在主核更新多次期间只看到一次状态变化。

因此,共享内存通信虽然实时,但对设计规范要求更高,包括:

  • 明确数据的生效时机;
  • 确保标志位和数据的原子一致;
  • 必要时引入轻量级握手机制(例如 ready/ack)。

六、总结:共享内存通信的关键经验

这次看似简单的共享内存同步问题,最终让我对双核通信的底层机制有了更深的理解。

问题 原因 解决思路
数据不同步 缓存未同步 使用 Non-cacheable 内存
偶发旧数据 Cache 脏数据未刷出 清空/无效化 D-Cache
读写不同步 访问频率差异 加入握手机制或延迟判断

最终结论:

双核共享内存通信的实时性远高于 RPMsg、消息队列等机制,但前提是你真正理解了 Cache 的行为。


一次小小的 bug,反而让我更彻底地理解了“非缓存区”的意义。
这就是嵌入式开发的魅力所在——越底层,越真实。


如果你也在调双核通信,不妨先确认你的共享区是不是真正的“共享区”。
也许一次 MPU 设置,就能让你的系统稳定又丝滑。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐