引言

在 STM32F4 系列高性能 MCU 中,D-Cache(数据缓存)能显著提升 CPU 访问内存的效率,但同时也引入了缓存一致性问题。当 DMA 外设直接访问内存时,若未正确配置 MPU(内存保护单元)或未执行缓存维护操作,CPU 与 DMA 看到的数据可能不一致,导致数据损坏或功能异常。本文以实际项目为例,讲解问题原理、排查步骤及解决方案。

问题现象与原理

典型症状

  • 以太网接收数据包偶尔出现 CRC 错误或内容错乱。
  • USB 批量传输数据丢失或重复。
  • ADC 采集的数据在 DMA 搬运后,CPU 读取的值与预期不符。

根本原因

STM32F4 的 D-Cache 是写回(write-back)模式,CPU 写入数据时先更新缓存,仅当缓存行被替换或显式清理时才写回内存。DMA 外设直接访问物理内存,不经过缓存。若 DMA 读取的内存区域仍驻留在缓存中且未写回,则 DMA 读到的是旧数据;若 DMA 写入内存,而 CPU 缓存中已有该区域的副本,则 CPU 可能读到过期的缓存数据。

排查方法

1. 确认是否启用 D-Cache

检查启动代码或初始化中是否调用了 SCB_EnableDCache()。若未启用,则问题可能源于其他原因。

2. 定位 DMA 缓冲区地址

查看 DMA 描述符或配置中的缓冲区地址,确认其是否位于可缓存区域(如 SRAM1/SRAM2)。

3. 检查 MPU 配置

若未配置 MPU,默认情况下整个 SRAM 区域是可缓存的。此时,任何 DMA 与 CPU 共享的数据都可能引发一致性问题。

4. 临时禁用 D-Cache 验证

在初始化时注释掉 SCB_EnableDCache(),观察问题是否消失。若消失,则基本确定是缓存一致性问题。

解决方案:MPU 配置 + 缓存维护

方案一:配置 MPU 将 DMA 缓冲区设为非缓存

将 DMA 使用的内存区域配置为“非缓存、非缓冲”属性,这样 CPU 访问该区域时直接操作内存,避免缓存干扰。

配置步骤

  1. 确定 DMA 缓冲区地址和大小(需对齐到 32 字节)。
  2. 使用 MPU_Region_InitTypeDef 配置 MPU 区域。
  3. 使能 MPU 并设置优先级。

代码示例

void MPU_Config(void)
{
    MPU_Region_InitTypeDef MPU_InitStruct;

    // 禁用 MPU 进行配置
    HAL_MPU_Disable();

    // 配置 DMA 缓冲区区域(假设地址 0x20001000,大小 4KB)
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x20001000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_4KB;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;  // 不使用 TEX
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_NOT_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;  // 关键:非缓存
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

调用 MPU_Config() 在系统初始化时执行。

方案二:使用缓存维护函数

若不想配置 MPU,也可在 DMA 操作前后手动执行缓存清理或无效化操作。

  • 当 CPU 写入数据并准备 DMA 读取时,调用 SCB_CleanDCache()SCB_CleanDCache_by_Addr() 将缓存写回内存。
  • 当 DMA 写入数据并准备 CPU 读取时,调用 SCB_InvalidateDCache()SCB_InvalidateDCache_by_Addr() 使缓存行失效。

代码示例

// 假设缓冲区地址为 buf,长度为 len

// CPU 写入数据后,清理缓存
SCB_CleanDCache_by_Addr((uint32_t*)buf, (int32_t)len);

// 启动 DMA 读取...

// DMA 传输完成后,无效化缓存
SCB_InvalidateDCache_by_Addr((uint32_t*)buf, (int32_t)len);

注意:地址需 32 字节对齐,长度需为 32 的倍数,否则需手动调整。

完整示例:以太网 DMA 接收

以下展示在 STM32F429 上使用 DMA 接收以太网数据帧时的正确配置。

// 定义 DMA 缓冲区(建议使用 __attribute__((aligned(32))))
__attribute__((aligned(32))) uint8_t rx_buffer[1522];

void HAL_ETH_RxCpltCallback(ETH_HandleTypeDef *heth)
{
    // 无效化缓存,确保 CPU 读取到 DMA 写入的最新数据
    SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));
    // 处理数据...
}

// 在初始化中配置 MPU(可选,但推荐)
void MPU_Config(void)
{
    // 如上所示,将 rx_buffer 所在区域设为非缓存
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MPU_Config();  // 先配置 MPU
    SCB_EnableDCache();  // 再使能 D-Cache
    // 初始化以太网等...
}

注意事项

  • 对齐与大小:MPU 区域大小必须是 2 的幂次,且基地址需对齐到区域大小。若缓冲区不满足,可定义更大的区域或使用缓存维护函数。
  • 性能权衡:将 DMA 缓冲区设为非缓存会降低 CPU 访问该区域的性能,但通常影响不大。
  • 多区域配置:若多个 DMA 缓冲区分散在不同地址,可配置多个 MPU 区域,但注意 MPU 区域数量有限(STM32F4 通常 8 个)。
  • 缓存维护开销:频繁调用缓存维护函数可能影响实时性,建议优先使用 MPU 配置。
  • 调试技巧:使用调试器查看内存内容,对比 DMA 写入前后的数据,可快速确认一致性错误。

总结

D-Cache 与 DMA 的交互是 STM32F4 开发中的常见陷阱。通过合理配置 MPU 将 DMA 缓冲区设为非缓存,或在使用时执行缓存清理/无效化操作,可有效避免数据不一致问题。建议在项目初期就规划好内存区域属性,以减少后期调试成本。