Cortex-M7 D-Cache 与 DMA 缓存一致性说明

Cortex-M7 D-Cache 与 DMA 缓存一致性说明

适用文件:Core/Src/usart.cCore/Src/main.c

芯片架构:STM32H7 / Cortex-M7(含 32 字节 D-Cache 缓存行)


0. 当前工程 D-Cache 状态(重要)

结论:本工程目前未启用 D-Cache。

检查依据

检查项 文件 结果
SCB_EnableDCache() 调用 全工程 不存在
MPU 缓存属性配置 main.c:243 IsCacheable = MPU_ACCESS_NOT_CACHEABLE(全局不可缓存)
system_stm32h7xx.c 启动初始化 system_stm32h7xx.c 无 Cache 使能
启动文件 startup_stm32h743iitx.s 无 Cache 使能

Cortex-M7 上电复位后 D-Cache 默认关闭 ,必须显式调用 SCB_EnableDCache() 才会激活。

当前状态下 Cache 维护函数的行为

SCB_InvalidateDCache_by_AddrSCB_CleanDCache_by_Addr 在函数内部会检查 SCB->CCR 寄存器的 DC 位。若 D-Cache 未开启,这两个函数直接返回,不执行任何操作,不会出错。

因此当前程序可以正确运行,但 Cache 维护代码尚未发挥实际作用。

启用 D-Cache 的正确步骤

如果将来要开启 D-Cache 以提升性能,需要同时修改两处:

第一步:在 main.cMPU_Config() 调用后、HAL_Init() 之前添加:

c 复制代码
MPU_Config();           /* 已有 */
SCB_EnableICache();     /* 可选:使能指令缓存 */
SCB_EnableDCache();     /* 新增:使能数据缓存 */
HAL_Init();             /* 已有 */

第二步:在 MPU 中将 SRAM 区域配置为 Cacheable(否则 D-Cache 对内存无效):

c 复制代码
/* 示例:将 SRAM(0x24000000,512KB)配置为 Write-back、可缓存 */
MPU_InitStruct.BaseAddress    = 0x24000000;
MPU_InitStruct.Size           = MPU_REGION_SIZE_512KB;
MPU_InitStruct.IsCacheable    = MPU_ACCESS_CACHEABLE;
MPU_InitStruct.IsBufferable   = MPU_ACCESS_BUFFERABLE;
MPU_InitStruct.TypeExtField   = MPU_TEX_LEVEL1;

启用后,usart.c 中已有的 aligned(32) 对齐和 Clean/Invalidate 调用将自动生效,无需修改。


1. 背景:为什么会有缓存一致性问题

Cortex-M7 内核内置 D-Cache(数据缓存) ,缓存行(Cache Line)大小固定为 32 字节

D-Cache 的工作方式:

  • CPU 读内存 → 先查缓存,命中则直接返回,不访问 RAM(极快)
  • CPU 写内存 → 写入缓存(Write-back 模式),不立即写回 RAM

问题根源:DMA 控制器直接读写 RAM,完全绕过 D-Cache。这导致两种竞争场景:

场景 现象
DMA 写 RAM(RX),CPU 随后读 CPU 从缓存拿到旧数据,看不到 DMA 写入的新内容
CPU 写缓存(TX),DMA 随后读 RAM DMA 从 RAM 拿到旧数据,CPU 的修改还没刷到 RAM

解决手段:在 DMA 传输前后,对 DMA 缓冲区执行 Cache 维护操作(Invalidate / Clean)。


2. 32 字节对齐:__attribute__((aligned(32)))

c 复制代码
/* usart.c, 第 29 行 */
uint8_t rs485_rx_buf[RS485_RX_BUF_SIZE] __attribute__((aligned(32)));

为什么必须对齐到 32 字节?

SCB_InvalidateDCache_by_AddrSCB_CleanDCache_by_Addr 操作的最小单位是一整条缓存行(32 字节),不能操作某缓存行的一部分。

如果缓冲区地址不是 32 的倍数(未对齐):

复制代码
RAM 布局示例(未对齐):
地址: 0x20000018
      ┌──────────────────────────────┐
      │  other_var (属于缓存行 A)    │  ← 0x20000000
      │  ...                         │
      │  rs485_rx_buf 从 0x18 开始   │  ← 0x20000018(不对齐)
      └──────────────────────────────┘

此时对 rs485_rx_buf 执行 Invalidate,HAL 库会将操作范围向下取整到缓存行边界(0x20000000),导致 other_var 的缓存行也被作废 → 相邻变量数据损坏

正确的对齐方式(aligned(32)):

复制代码
RAM 布局示例(已对齐):
地址: 0x20000020
      ┌──────────────────────────────┐
      │  rs485_rx_buf[0..31]         │  ← 缓存行边界,独占完整缓存行
      │  rs485_rx_buf[32..63]        │
      │  ...                         │
      └──────────────────────────────┘

缓冲区从缓存行边界开始,Invalidate/Clean 操作精确覆盖且不影响其他变量。

注意:大小也应是 32 的整数倍

当前 RS485_RX_BUF_SIZE = 128(= 32 × 4),满足条件。

若大小不是 32 的倍数(如 100 字节),操作会覆盖到末尾那条缓存行的剩余字节,同样可能损坏相邻数据。


3. SCB_InvalidateDCache_by_Addr

函数原型

c 复制代码
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);

作用

将指定地址范围内的所有 D-Cache 缓存行标记为无效(Invalid)

CPU 下次访问这段内存时,强制从 RAM 重新加载,不再使用缓存中的旧数据。

注意:Invalidate 不会把缓存中的内容写回 RAM,直接丢弃缓存行。

使用位置 1:RS485_StartReceive()(接收启动前)

c 复制代码
/* usart.c, 第 206-213 行 */
void RS485_StartReceive(void)
{
    RS485_DE_LOW();
    SCB_InvalidateDCache_by_Addr((uint32_t *)rs485_rx_buf, (int32_t)RS485_RX_BUF_SIZE);
    HAL_UARTEx_ReceiveToIdle_DMA(&huart2, rs485_rx_buf, RS485_RX_BUF_SIZE);
    __HAL_DMA_DISABLE_IT(huart2.hdmarx, DMA_IT_HT);
}

时序分析:

复制代码
CPU 上次处理数据时可能已将 rs485_rx_buf 的部分内容加载进缓存
          ↓
【Invalidate】:丢弃缓存中该区域的旧数据
          ↓
启动 DMA RX:DMA 将串口数据直接写入 RAM
          ↓
(等待接收完成)

目的:防止 CPU 在使用旧缓存时,DMA 的新写入被缓存掩盖,确保下次读取是真正的新数据。

使用位置 2:HAL_UARTEx_RxEventCallback()(接收完成后)

c 复制代码
/* usart.c, 第 244-255 行 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
    if (huart->Instance == USART2)
    {
        SCB_InvalidateDCache_by_Addr((uint32_t *)rs485_rx_buf, (int32_t)RS485_RX_BUF_SIZE);
        rs485_rx_len  = Size;
        rs485_rx_flag = 1;
        RS485_StartReceive();
    }
}

时序分析:

复制代码
DMA 将接收到的 RS485 数据写入 RAM(绕过缓存)
          ↓
UART IDLE 中断触发,进入回调
          ↓
【Invalidate】:使 rs485_rx_buf 对应的缓存行失效
          ↓
CPU 读取 rs485_rx_buf:强制从 RAM 加载 → 读到 DMA 写入的真实数据

目的:DMA 已把新数据写入 RAM,但 CPU 缓存中还是旧内容。Invalidate 后 CPU 才能看到新数据。


4. SCB_CleanDCache_by_Addr

函数原型

c 复制代码
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);

作用

将 D-Cache 中指定地址范围内所有**脏缓存行(Dirty Cache Lines)**的内容写回到 RAM。

写回后缓存行变为 Clean 状态(仍然有效,下次 CPU 读取仍命中缓存)。

与 Invalidate 的区别:Clean = 写回 RAM 但不丢弃缓存;Invalidate = 丢弃缓存不写回 RAM。

使用位置:RS485_SendDMA()(发送启动前)

c 复制代码
/* usart.c, 第 220-226 行 */
void RS485_SendDMA(const uint8_t *data, uint16_t len)
{
    RS485_DE_HIGH();
    SCB_CleanDCache_by_Addr((uint32_t *)data, (int32_t)len);
    HAL_UART_Transmit_DMA(&huart2, (uint8_t *)data, len);
}

时序分析:

复制代码
CPU 向发送缓冲区写数据(如 rs485_tx_buf)
  → Write-back 模式:数据写入 D-Cache,RAM 中仍是旧值
          ↓
【Clean】:将缓存中的新数据强制写回 RAM
          ↓
启动 DMA TX:DMA 从 RAM 读取数据 → 发送到 RS485 总线

目的:若没有 Clean,DMA 从 RAM 读到的是 CPU 写之前的旧数据,导致发送内容错误。

发送缓冲区同样需要 32 字节对齐

c 复制代码
/* 调用方定义发送缓冲区时应对齐,例如: */
uint8_t rs485_tx_buf[32] __attribute__((aligned(32)));

原因与 RX 缓冲区完全相同:Clean 操作以 32 字节为单位,未对齐会影响相邻变量。


5. 操作对比速查表

操作 函数 缓存状态变化 使用时机
Invalidate SCB_InvalidateDCache_by_Addr 缓存行 → Invalid(丢弃,不写回) DMA 写 RAM 之前之后(CPU 要读 DMA 结果)
Clean SCB_CleanDCache_by_Addr Dirty 缓存行 → 写回 RAM,变为 Clean DMA 读 RAM 之前(CPU 已写过缓冲区)
Clean+Invalidate SCB_CleanInvalidateDCache_by_Addr 写回 RAM 后丢弃 缓冲区既被 CPU 写过、又将被 DMA 写入时

6. 完整数据流图

RX 流程(DMA 写 → CPU 读)

复制代码
RS485 总线
    │
    ▼ 串行数据
USART2 外设
    │
    ▼ DMA(直接写 RAM,绕过缓存)
rs485_rx_buf(RAM)
    │
    ▼ IDLE 中断
SCB_InvalidateDCache_by_Addr    ← 使缓存失效
    │
    ▼ CPU 读取
rs485_rx_buf(从 RAM 重新加载)  ← 得到真实接收数据

TX 流程(CPU 写 → DMA 读)

复制代码
CPU 填充 rs485_tx_buf
    │
    ▼(数据暂留在 D-Cache,RAM 可能未更新)
SCB_CleanDCache_by_Addr         ← 强制将缓存写回 RAM
    │
    ▼ DMA(从 RAM 读取,绕过缓存)
USART2 外设
    │
    ▼
RS485 总线

7. 常见错误与后果

忘记操作 后果
RX 前不 Invalidate CPU 读到缓存中的旧帧数据(非 DMA 写入的新数据)
RX 后不 Invalidate 同上;尤其在第二帧开始前仍读旧帧
TX 前不 Clean DMA 发送缓存更新前的旧数据,导致发送内容错误
缓冲区不对齐 Invalidate/Clean 操作影响相邻变量,造成数据损坏,极难调试

生成日期:2026-03-30

相关推荐
多看书少吃饭2 小时前
Vue3 + Java + Python 打造企业级大模型知识库(含 SSE 流式对话完整源码)
java·python·状态模式
Arthas2172 小时前
Java大厂面试:从Spring到微服务的全面技术考察
java·jvm·spring·微服务·面试·并发
mifengxing2 小时前
力扣HOT100——(1)两数之和
java·数据结构·算法·leetcode·hot100
m0_738120722 小时前
我的创作纪念日0328
java·网络·windows·python·web安全·php
用户8307196840822 小时前
Spring Boot 中Servlet、Filter、Listener 四种注册方式全解析
java·spring boot
keyborad pianist2 小时前
一篇文章学会Redis
数据库·redis·缓存
xixingzhe22 小时前
spring boot druid 10秒超时问题
java·数据库·spring boot
ok_hahaha2 小时前
java从头开始-黑马点评-分布式锁-redis实现基础版
java·redis·分布式
Nyarlathotep01132 小时前
ReentrantReadWriteLock基础和原理
java·后端