缓存一致性基础
做 DMA 调试绕不开缓存一致性。我记得刚接触嵌入式时,总以为 CPU 和 DMA 看到的是一样的内存。直到有一次,DMA 把数据搬到了内存,CPU 却读到了“旧”值——后来才知道,是缓存惹的祸。
这一节,我们聊聊缓存的结构、缓存行,还有那套保证一致性的协议。不扯太深的理论,只讲你调试时真正用得上的东西。
1. 缓存结构概览
现代 SoC 里,CPU 核心通常有多级缓存:L1(指令/数据分离)、L2(每核私有或共享)、甚至 L3。DMA 一般是直接访问主存(DDR)或片上 SRAM,它不经过缓存。这就是矛盾的根源——CPU 在缓存里操作了一版数据,DMA 却去主存里拿另一版。
我习惯把缓存看作一个“高速副本池”。CPU 读写副本,DMA 读写原件。一致性协议的工作,就是让这两个视图保持一致。
| 缓存层级 | 典型位置 | 对 DMA 的影响 |
|---|---|---|
| L1 | CPU 核内 | DMA 完全不可见,最易引起不一致 |
| L2 | 核内或共享 | 若共享则需一致性协议维护 |
| L3 | 片内多核共享 | 通常由硬件一致性协议处理 |
| 主存 | 片外 DDR | DMA 直接操作的目标 |
2. 缓存行(Cache Line)—— 一致性的最小单元
缓存不是字节粒度管理的。它把内存切成等长的块,这个块就叫缓存行。常见大小是 64 字节(ARM、x86 都这么用)。说白了,你改 1 个字节,CPU 会先把整个 64 字节的缓存行加载进来,修改后再整行写回。
这对 DMA 调试意味着什么?我举个例子:如果你的 DMA 描述符刚好和某个数据变量在同一个缓存行里,CPU 改变量就会连带把描述符“刷新”掉——嗯,这就是伪共享(false sharing)的典型场景。
如何查看你的平台缓存行大小?Linux 下直接读这个文件:
cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size
大概率返回 64。裸机开发的话,查芯片参考手册的 Cache 章节,写的很清楚。
3. 缓存一致性协议简介
协议要回答一个核心问题:当一个核心(或 DMA)修改了某地址的数据,其他核心的缓存该如何反应?
最经典的是 MESI 协议(Modified、Exclusive、Shared、Invalid)。四个状态,配合总线嗅探或目录,保证多核视图一致。
你想想看,如果 CPU0 持有一个缓存行是 Modified,此时 DMA 直接从内存读走旧数据,那画面多美——数据不一致就来了。协议的作用就是让 CPU0 在 DMA 访问前把修改写回内存,或者让 DMA 去“窥探”缓存。
4. 常见的缓存一致性协议
除了 MESI,还有 MOESI(增加 Owned 状态)、MESIF(Intel 用 Forward 状态),以及 ARM 平台上更复杂的 AMBA ACE 协议。但底层逻辑是一样的——状态 + 嗅探/目录。
下面这个表是 MESI 四个状态的简单对比,方便你快速回忆:
| 状态 | 含义 | 本核读写权限 | 其他核副本情况 |
|---|---|---|---|
| M (Modified) | 数据已修改,与主存不同 | 可读可写 | 无效 |
| E (Exclusive) | 数据干净,仅本核持有 | 可读可写 | 无副本 |
| S (Shared) | 数据干净,多核共享 | 仅可读(写需先失效其他核) | 至少一个副本 |
| I (Invalid) | 数据无效,不可使用 | 无权限(需重新加载) | 可能存在但不可靠 |
5. 调试中的关键检查点
基于上面的基础,我在调试 DMA 缓存一致性问题时,一般按这个顺序检查:
- 确认缓存行对齐。 缓冲区首地址和大小必须是缓存行整数倍。如果不对齐,用
__attribute__((aligned(64)))或 memalign。 - 确认 DMA 描述符所在区域是否 cacheable。 很多 SoC 的 DMA 描述符要求放在 non-cacheable 内存里,否则描述符更新会被缓存。
- 在启动 DMA 前执行 clean cache。 把 CPU 已修改但还没写入内存的数据刷出去。
- 在 DMA 完成中断中执行 invalidate cache。 让 CPU 重新从内存读取最新数据。
- 用工具看实际状态。 调试器可以读 Cache 寄存器,能看到某地址是否命中、处于哪个状态。
好了,这一节就聊这么多。缓存结构、缓存行、一致性协议,是理解后续所有调试技巧的基石。你接下来遇到任何 DMA 数据异常,都可以先回头看看这几个基础概念——大概率问题就出在这里。