STM32H7 L1 Cache与DMA数据一致性:Cache维护操作的正确时机与踩坑记录

1. 为什么 Cache 和 DMA 会打架?

STM32H7 基于 Cortex-M7 内核,配有 L1 D-Cache 和 I-Cache。CPU 访问内存时,数据会被缓存到 D-Cache 中。而 DMA 控制器直接访问物理内存(SRAM/SDRAM),不经过 Cache

当 CPU 写数据到某块内存,数据可能还停留在 Cache 中未写回物理内存,此时启动 DMA 发送,DMA 读到的是旧数据;反之,DMA 将新数据写入内存后,CPU 读到的却是 Cache 中的旧副本。这就是数据一致性问题

  • 写方向(CPU→DMA):CPU 写数据后需执行 Clean 操作,将 Cache 内容写回物理内存,再启动 DMA。
  • 读方向(DMA→CPU):DMA 传输完成后,需执行 Invalidate 操作,丢弃 Cache 中的旧数据,强制 CPU 从物理内存重新加载。

2. Cache 维护操作的正确时机

2.1 发送数据(CPU→外设)

// 1. 准备数据到缓冲区
memcpy(tx_buf, source, len);

// 2. Clean:将 Cache 内容写回物理内存
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);

// 3. 启动 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);

关键点:Clean 必须在启动 DMA 之前执行。若在 DMA 传输中 Clean,可能覆盖 DMA 已更新的数据。

2.2 接收数据(外设→CPU)

// 1. 启动 DMA 接收(此时不要 Invalidate!)
HAL_UART_Receive_DMA(&huart1, rx_buf, len);

// 2. 等待 DMA 传输完成(中断或轮询)
// ...

// 3. Invalidate:丢弃 Cache 旧数据
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, len);

// 4. 安全读取数据
process_data(rx_buf, len);

关键点:Invalidate 必须在 DMA 传输完成之后执行。若在 DMA 传输中 Invalidate,可能丢弃 DMA 尚未写入的数据,导致数据丢失。

3. 完整示例:UART DMA 收发

#define BUF_SIZE 64

__attribute__((aligned(32))) uint8_t tx_buf[BUF_SIZE];
__attribute__((aligned(32))) uint8_t rx_buf[BUF_SIZE];
volatile uint8_t rx_done = 0;

void uart_send(uint8_t *data, uint16_t len)
{
    memcpy(tx_buf, data, len);
    SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
    HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}

void uart_receive_start(void)
{
    rx_done = 0;
    HAL_UART_Receive_DMA(&huart1, rx_buf, BUF_SIZE);
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, BUF_SIZE);
        rx_done = 1;
    }
}

4. 踩坑记录与注意事项

坑1:缓冲区未对齐导致维护失效

SCB_CleanDCache_by_Addr 要求地址 32 字节对齐。若缓冲区未对齐,操作可能无效或误伤相邻数据。

  • 解决:使用 __attribute__((aligned(32))) 强制对齐。

坑2:Invalidate 时机错误导致数据丢失

在 DMA 传输过程中调用 Invalidate,会丢弃 DMA 刚写入但尚未被 CPU 读取的数据。

  • 解决:严格在 DMA 完成中断中执行 Invalidate。

坑3:多缓冲区共享 Cache Line

若两个缓冲区位于同一 Cache Line(32 字节),Clean 一个缓冲区可能影响另一个。

  • 解决:确保 DMA 缓冲区按 Cache Line 对齐,或使用 MPU 将 DMA 区域配置为 Write-Through/Non-Cacheable。

坑4:忘记配置 MPU 导致性能下降

频繁 Clean/Invalidate 影响性能。对于频繁 DMA 的区域,建议用 MPU 配置为 Non-Cacheable。

MPU_Region_InitTypeDef MPU_InitStruct;
MPU_InitStruct.Enable = MPU_REGION_ENABLE;
MPU_InitStruct.BaseAddress = 0x30000000; // SRAM1
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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);

坑5:DMA 描述符与数据缓冲区混用

若 DMA 描述符(如链表模式)也放在 Cacheable 区域,需同样维护。

  • 解决:将描述符放在 Non-Cacheable 区域,或每次修改后 Clean。

5. 总结

  • 写方向:先 Clean,再启动 DMA。
  • 读方向:先等 DMA 完成,再 Invalidate。
  • 对齐:缓冲区按 32 字节对齐。
  • MPU:高频 DMA 区域建议配置为 Non-Cacheable。
  • 调试:若数据异常,优先检查 Cache 维护操作。

掌握这些原则,就能在 STM32H7 上安全高效地使用 Cache 与 DMA,充分发挥芯片性能。