STM32H7 D-Cache与DMA一致性:SCB_CleanDCache_by_Addr的典型误用与排查方法

1. 为什么D-Cache会与DMA冲突?

STM32H7搭载Cortex-M7内核,主频可达480MHz,为了匹配高速运算,芯片内部集成了D-Cache(数据缓存)。CPU访问数据时,会先将数据从主存(如SRAM、SDRAM)加载到Cache中,后续读写直接操作Cache,从而减少访问延迟。

但DMA(直接存储器访问)是独立于CPU的外设,它直接读写主存,不经过Cache。这就导致:

  • CPU写数据:数据可能只停留在Cache中,尚未写回主存。此时启动DMA发送,DMA读到的是旧数据。
  • DMA写数据:DMA将新数据写入主存,但CPU可能仍从Cache中读取旧数据。

这种不一致性轻则导致数据错乱,重则引发HardFault。

2. Cache维护操作:Clean、Invalidate、Clean&Invalidate

CMSIS提供了三个核心函数:

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

使用原则

  • CPU写→DMA读(如发送):调用SCB_CleanDCache_by_Addr,确保数据写回主存。
  • DMA写→CPU读(如接收):调用SCB_InvalidateDCache_by_Addr,丢弃旧Cache,让CPU读取新数据。
  • 双向传输:使用SCB_CleanInvalidateDCache_by_Addr

3. SCB_CleanDCache_by_Addr的典型误用

3.1 地址未按Cache Line对齐

Cortex-M7的D-Cache Line大小为32字节SCB_CleanDCache_by_Addr内部按行操作,如果起始地址未对齐,可能清理到相邻数据,或漏掉部分数据。

错误示例

uint8_t buffer[100];
// buffer地址假设为0x20000001(未对齐)
SCB_CleanDCache_by_Addr((uint32_t*)buffer, 100); // 危险!

正确做法:确保缓冲区32字节对齐,且长度向上取整到32的倍数。

__attribute__((aligned(32))) uint8_t buffer[128];
SCB_CleanDCache_by_Addr((uint32_t*)buffer, 128);

3.2 长度参数错误

长度应为字节数,但很多开发者误传元素个数。例如:

uint32_t data[10];
SCB_CleanDCache_by_Addr((uint32_t*)data, 10); // 错误!只清理了10字节

应改为:

SCB_CleanDCache_by_Addr((uint32_t*)data, sizeof(data)); // 40字节

3.3 方向混淆

在DMA接收完成后,误用SCB_CleanDCache_by_Addr,导致CPU仍读取旧Cache数据。

错误场景

// DMA接收完成中断中
SCB_CleanDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer)); // 错误!
process_data(rx_buffer); // 可能读到旧数据

应使用:

SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, sizeof(rx_buffer));

3.4 忽略MPU配置

若将DMA缓冲区配置为Write-Through或Non-Cacheable,则无需手动维护Cache。但很多开发者未配置MPU,导致默认Cacheable,却忘记维护。

4. 完整代码示例

以下示例展示UART DMA发送与接收的正确Cache维护流程。

#include "stm32h7xx.h"

#define BUFFER_SIZE 128
__attribute__((aligned(32))) uint8_t tx_buffer[BUFFER_SIZE];
__attribute__((aligned(32))) uint8_t rx_buffer[BUFFER_SIZE];

// UART DMA发送
void uart_send_dma(uint8_t *data, uint16_t len) {
    memcpy(tx_buffer, data, len);
    // 清理D-Cache,确保数据写回主存
    SCB_CleanDCache_by_Addr((uint32_t*)tx_buffer, BUFFER_SIZE);
    HAL_UART_Transmit_DMA(&huart1, tx_buffer, len);
}

// UART DMA接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (huart->Instance == USART1) {
        // 无效化D-Cache,丢弃旧数据
        SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buffer, BUFFER_SIZE);
        process_data(rx_buffer, BUFFER_SIZE);
        // 重新启动接收
        HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
    }
}

5. 系统化排查方法

当出现DMA数据错乱时,按以下步骤排查:

  1. 确认D-Cache状态:检查SCB->CCR寄存器的DC位,确认D-Cache是否使能。
  2. 检查缓冲区对齐:使用__attribute__((aligned(32)))确保32字节对齐。
  3. 验证维护操作:在DMA传输前后添加调试代码,打印缓冲区内容,对比Cache维护前后的差异。
  4. 使用MPU配置:将DMA缓冲区区域配置为Non-Cacheable,一劳永逸。
  5. 利用硬件特性:STM32H7支持AXI SRAM的Cache维护,但需注意不同内存区域的Cache属性。

6. 注意事项

  • 性能权衡:频繁的Cache维护会降低性能,建议将DMA缓冲区集中管理,减少维护次数。
  • 中断安全:在中断中调用Cache维护函数时,确保不会与主循环冲突。
  • 多核场景:STM32H7为单核,但若使用双核(如H745),需考虑核间共享内存的Cache一致性。
  • 工具辅助:使用STM32CubeMX配置MPU,可自动生成Non-Cacheable区域代码。

通过理解D-Cache与DMA的交互机制,并正确使用SCB_CleanDCache_by_Addr等函数,可以彻底解决数据一致性问题,充分发挥STM32H7的高性能优势。