一、为什么 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 读取。
边界条件(极易出错):
- 地址必须 32 字节对齐:Cortex-M7 Cache Line 为 32 字节。若地址未对齐,clean/invalidate 会操作包含该地址的整个 Cache Line,可能误伤相邻数据。
- size 应为 32 的整数倍:否则同样会波及多余区域。
- invalidate 前必须确保 Cache 中没有未写回的数据:否则会丢失数据。正确顺序是:先 clean,再 invalidate。
- DMA 接收时:先 invalidate 再启动 DMA,防止 CPU 读到旧 Cache;DMA 完成后再次 invalidate 再读取。
- 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 字节对齐。掌握这些边界条件,才能写出稳定高效的嵌入式代码。