一、为什么 DMA 与 D-Cache 会打架?

Cortex-M7 的 D-Cache 位于内核与总线矩阵之间。CPU 访问内存时,若命中 Cache 则直接读写 Cache,不会立即同步到 SRAM。而 DMA 控制器直接访问 SRAM,不经过 Cache。

  • CPU 写数据后启动 DMA 发送:数据可能还在 Cache 中,DMA 读到的是旧值。
  • DMA 接收数据后 CPU 读取:Cache 中可能缓存了旧数据,CPU 读到旧值。

因此,必须在 DMA 传输前后对 Cache 进行维护操作。

二、MPU 配置:为 DMA 缓冲区划定安全区

STM32H7 的 MPU 可将特定内存区域配置为 Write-Through 或 Non-Cacheable,从根本上避免一致性问题。推荐将 DMA 缓冲区放在 Non-Cacheable 区域。

void MPU_Config(void)
{
    HAL_MPU_Disable();
    MPU_Region_InitTypeDef MPU_InitStruct = {0};

    MPU_InitStruct.Enable = MPU_REGION_ENABLE;
    MPU_InitStruct.BaseAddress = 0x30000000; // D2 SRAM 地址
    MPU_InitStruct.Size = MPU_REGION_SIZE_64KB;
    MPU_InitStruct.AccessPermission = MPU_REGION_FULL_ACCESS;
    MPU_InitStruct.IsBufferable = MPU_ACCESS_NOT_BUFFERABLE;
    MPU_InitStruct.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; // 关键:非缓存
    MPU_InitStruct.IsShareable = MPU_ACCESS_SHAREABLE;
    MPU_InitStruct.Number = MPU_REGION_NUMBER0;
    MPU_InitStruct.TypeExtField = MPU_TEX_LEVEL0;
    MPU_InitStruct.SubRegionDisable = 0x00;
    MPU_InitStruct.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE;

    HAL_MPU_ConfigRegion(&MPU_InitStruct);
    HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
  • 将 DMA 缓冲区放在 0x30000000 起始的 64KB 区域,并设为 Non-Cacheable。
  • 若使用 DTCM(0x20000000),注意 DTCM 本身不经过 Cache,无需维护,但 DMA 无法访问 DTCM。

三、clean 与 invalidate 的边界条件

若无法使用 Non-Cacheable 区域,则需手动维护 Cache。CMSIS 提供两个函数:

  • SCB_CleanDCache_by_Addr(addr, size):将 Cache 中已修改的数据写回 SRAM。
  • SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制下次从 SRAM 读取。

边界条件(极易出错):

  1. 地址必须 32 字节对齐:Cortex-M7 Cache Line 为 32 字节。若地址未对齐,clean/invalidate 会操作包含该地址的整个 Cache Line,可能误伤相邻数据。
  2. size 应为 32 的整数倍:否则同样会波及多余区域。
  3. invalidate 前必须确保 Cache 中没有未写回的数据:否则会丢失数据。正确顺序是:先 clean,再 invalidate。
  4. DMA 接收时:先 invalidate 再启动 DMA,防止 CPU 读到旧 Cache;DMA 完成后再次 invalidate 再读取。
  5. DMA 发送时:先 clean 再启动 DMA,确保 SRAM 数据最新。

四、完整代码示例:UART DMA 发送与接收

以下示例使用 D2 SRAM 中的缓冲区,并演示 clean/invalidate 的正确用法。

#include "stm32h7xx_hal.h"

#define BUF_SIZE 128
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t tx_buf[BUF_SIZE];
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t rx_buf[BUF_SIZE];

// 发送前:clean D-Cache
void DMA_SendData(UART_HandleTypeDef *huart, uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    HAL_UART_Transmit_DMA(huart, tx_buf, len);
}

// 接收前:invalidate D-Cache
void DMA_StartReceive(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    HAL_UART_Receive_DMA(huart, rx_buf, BUF_SIZE);
}

// 接收完成回调中:再次 invalidate 后读取
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
    // 此时 rx_buf 中为 DMA 写入的最新数据
    process_data(rx_buf, BUF_SIZE);
}
  • 使用链接脚本将 .dma_buffer 段定位到 D2 SRAM(0x30000000)。
  • 若 MPU 已将该区域设为 Non-Cacheable,则上述 clean/invalidate 可省略,但保留也无害。

五、注意事项与常见坑

  • 不要对栈上变量做 DMA:栈地址可能未对齐,且 Cache 维护范围难以控制。
  • HAL 库的 SCB_InvalidateDCache 会操作整个 Cache:除非必要,否则使用 by_Addr 版本。
  • 中断中调用 clean/invalidate 需谨慎:这些操作可能耗时,影响实时性。
  • 多缓冲区场景:每个缓冲区独立维护,避免跨区域误操作。
  • 调试时关闭 Cache:可临时关闭 D-Cache 验证是否为一致性问题。
  • 使用 __DSB() 确保操作完成:在 clean/invalidate 后、启动 DMA 前插入内存屏障。

六、总结

STM32H7 的 D-Cache 与 DMA 共存需要开发者明确数据流向。优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,可一劳永逸。若必须使用 Cacheable 内存,则严格遵循“发送前 clean、接收前 invalidate、接收后 invalidate”的原则,并保证地址与长度 32 字节对齐。掌握这些边界条件,才能写出稳定高效的嵌入式代码。