基于 Cortex-M7 的 MPU 配置不当导致 DMA 与 CPU 缓存一致性故障的排查流程

引言

在嵌入式开发中,Cortex-M7 凭借其强大的处理能力和可选的 L1 Cache,成为高性能应用的理想选择。然而,Cache 的引入也带来了新的挑战——缓存一致性(Cache Coherency)问题。当 DMA 外设直接访问内存,而 CPU 通过 Cache 读写同一区域时,若缺乏正确的内存属性配置,就会导致数据错乱。本文以 STM32H743 平台为例,记录一次因 MPU 配置不当引发的 DMA 传输数据损坏的完整排查过程。

故障现象

某项目中,使用 SPI DMA 接收传感器数据,数据长度 256 字节,存入全局数组 rx_buffer。系统运行后,发现接收到的数据前 64 字节正确,后续字节随机出错,且错误模式不固定。初步怀疑是 SPI 配置问题,但反复检查时序和 DMA 设置均无异常。

原理分析:MPU、Cache 与 DMA 的三角关系

1. Cortex-M7 的 Cache 与内存属性

Cortex-M7 内核具有独立的 I-Cache 和 D-Cache。CPU 访问内存时,会优先查找 Cache。而 DMA 是独立于 CPU 的外设,直接访问物理内存(SRAM),不经过 Cache。当 CPU 和 DMA 同时操作同一内存区域时,就会产生一致性问题。

2. MPU 的作用

MPU 允许我们将内存区域划分为不同的区域,并设置其访问权限和属性。其中最关键的是 Cache 策略,主要有三种:

  • Write-Through(写通):CPU 写数据时,同时更新 Cache 和主存,读操作仍使用 Cache。
  • Write-Back(写回):CPU 写数据时,仅更新 Cache,标记为脏,延迟写回主存。
  • Non-cacheable(不可缓存):CPU 直接访问主存,不使用 Cache。

对于 DMA 与 CPU 共享的内存区域,推荐使用 Non-cacheableWrite-Through 策略,避免因 Write-Back 导致的数据滞后。

3. 故障根因

在本案例中,rx_buffer 位于默认的 SRAM 区域,而默认情况下,Cortex-M7 的 D-Cache 对该区域采用 Write-Back 策略。当 DMA 写入数据到 rx_buffer 时,CPU 的 Cache 中可能仍保留着旧数据。当 CPU 读取时,会优先命中 Cache,从而读到过时数据。此外,DMA 写入的数据可能被 CPU 的 Cache 写回操作覆盖,导致数据损坏。

排查流程

步骤 1:确认 Cache 状态

首先,检查 D-Cache 是否启用。在 STM32H7 中,默认启动文件会启用 D-Cache。通过调试器查看 SCB->CCR 寄存器,确认 D-Cache 已使能。

if (SCB->CCR & SCB_CCR_DC_Msk) {
    // D-Cache 已启用
}

步骤 2:检查 MPU 配置

查看 MPU 寄存器,发现默认配置中,SRAM 区域被设置为 Write-Back 策略。这证实了我们的怀疑。

步骤 3:临时禁用 D-Cache 验证

为了快速验证,我们可以临时禁用 D-Cache,观察故障是否消失。

SCB_DisableDCache();
// 测试 DMA 接收

测试发现数据完全正确,这进一步确认了问题与 Cache 相关。

步骤 4:正确配置 MPU

我们需要为 DMA 缓冲区所在的内存区域配置 MPU,将其设置为 Non-cacheableWrite-Through。这里我们选择 Non-cacheable,因为它能彻底避免一致性问题,且性能损失在可接受范围内(对于缓冲区而言)。

解决方案:MPU 配置代码

以下代码展示了如何为 rx_buffer 所在的 SRAM 区域配置 MPU,使其成为 Non-cacheable。

#include "cmsis_armcc.h"

void MPU_Config(void)
{
    // 确保 MPU 已禁用
    MPU_Disable();

    // 配置区域 0:整个 SRAM1(地址 0x30000000,大小 128KB)
    // 实际使用时,可根据缓冲区地址和大小调整
    MPU_Region_InitTypeDef MPU_InitStruct;

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000;
    MPU_InitStruct.Size = MPU_REGION_SIZE_128KB;
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;  // TEX=0, C=0, B=0 -> Non-cacheable
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;  // 可共享,便于 DMA 访问
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU
    MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

注意:TypeExtFieldIsCacheableIsBufferable 的组合决定了 Cache 策略。对于 Non-cacheable,TEX=0, C=0, B=0。

完整示例:DMA 接收缓冲区配置

以下是一个完整的初始化序列,包括 MPU 配置、DMA 配置和接收逻辑。

// 全局缓冲区,需 32 字节对齐(Cache line 大小)
__attribute__((aligned(32))) uint8_t rx_buffer[256];

void SystemInit_MPU(void)
{
    // 禁用 MPU
    MPU_Disable();

    // 配置区域 0:rx_buffer 所在区域(假设在 SRAM1)
    MPU_Region_InitTypeDef MPU_InitStruct = {0};
    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = (uint32_t)rx_buffer;
    MPU_InitStruct.Size = MPU_REGION_SIZE_256B;  // 根据实际大小调整
    MPU_InitStruct.SubRegionDisable = 0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_ENABLE;
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    HAL_MPU_ConfigRegion(&MPU_InitStruct);

    // 使能 MPU,默认特权模式访问
    MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}

void SPI_DMA_Init(void)
{
    // 标准 SPI 和 DMA 初始化,此处省略
    // 确保 DMA 目标地址为 rx_buffer
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();

    // 先配置 MPU,再使能 D-Cache
    SystemInit_MPU();
    SCB_EnableDCache();

    SPI_DMA_Init();

    // 启动 DMA 接收
    HAL_SPI_Receive_DMA(&hspi, rx_buffer, 256);

    while (1) {
        // 处理数据
    }
}

注意事项

  • MPU 区域大小:MPU 区域大小必须是 2 的幂次,且最小为 32 字节。如果缓冲区大小不是 2 的幂次,可以扩大区域覆盖,但需确保不与其他关键数据冲突。
  • 对齐要求:缓冲区地址必须与区域大小对齐(例如,区域大小为 256B,地址需 256 字节对齐)。使用 __attribute__((aligned(32))) 至少保证 Cache line 对齐,但 MPU 区域对齐要求更高。
  • 共享属性:对于 DMA 访问的内存,建议设置为 Shareable,以确保总线一致性(在单核中影响不大,但多核或总线矩阵中重要)。
  • 性能权衡:Non-cacheable 区域会降低 CPU 访问速度,因此仅对 DMA 缓冲区使用,其他数据仍保持 Cacheable。
  • 调试技巧:利用调试器观察 Cache 和内存内容,或使用 SCB_CleanDCache()SCB_InvalidateDCache() 手动维护一致性,但这不是长久之计。

总结

本次故障的根源是 MPU 默认配置将 SRAM 设置为 Write-Back 策略,导致 DMA 与 CPU 缓存不一致。通过正确配置 MPU,将 DMA 缓冲区设为 Non-cacheable,问题得以解决。在实际项目中,务必为 DMA 相关的内存区域规划合适的 Cache 策略,这是 Cortex-M7 开发中不可忽视的一环。希望本文的排查流程和代码示例能帮助你快速定位类似问题。