一、为什么 Cache 会让 DMA 数据“错乱”?
STM32H7 的 Cortex-M7 内核包含 L1 数据 Cache(D-Cache),默认写回(Write-Back)策略。当 CPU 写数据时,数据可能只停留在 Cache 中,并未立即写入 SRAM;当 DMA 从 SRAM 读取时,读到的就是旧数据。反之,DMA 将新数据写入 SRAM 后,CPU 若直接读 Cache,可能读到旧缓存内容。这就是 Cache 一致性(Coherency) 问题。
- 写回(Write-Back):CPU 写只更新 Cache,延迟写回内存。
- 写通(Write-Through):CPU 写同时更新 Cache 和内存,但性能略低。
- Cache 行(Cache Line):Cortex-M7 的 D-Cache 行大小为 32 字节,所有 Clean/Invalidate 操作均以行为单位。
二、解决思路:MPU 配置 + 手动维护
2.1 MPU 配置(推荐)
通过 MPU 将 DMA 缓冲区所在内存区域配置为 Non-Cacheable(非缓存) 或 Write-Through(写通),可从根本上避免一致性问题。
- Non-Cacheable:CPU 访问直接读写 SRAM,无 Cache 介入,性能略低但最安全。
- Write-Through:CPU 写同时更新 Cache 和 SRAM,DMA 读安全;但 DMA 写后 CPU 读仍需 Invalidate。
2.2 手动 Clean/Invalidate(灵活)
若缓冲区仍需 Cache 加速,则必须在 DMA 传输前后手动维护:
-
DMA 发送前(CPU 写 → DMA 读):调用
SCB_CleanDCache_by_Addr(),将 Cache 中脏数据写回 SRAM。 -
DMA 接收后(DMA 写 → CPU 读):调用
SCB_InvalidateDCache_by_Addr(),丢弃 Cache 中旧数据,强制从 SRAM 重新加载。
注意:Invalidate 会丢弃未写回的数据,若 CPU 在 DMA 接收前写过该缓冲区,必须先 Clean 再 Invalidate,否则数据丢失。
三、实战踩坑记录
坑 1:DMA 发送前未 Clean,数据“发不出去”
现象:UART DMA 发送字符串,串口助手收到乱码或旧数据。
原因:CPU 写缓冲区后,数据在 Cache 中,DMA 从 SRAM 读到旧值。
解决:发送前调用 SCB_CleanDCache_by_Addr()。
uint8_t tx_buf[64] __attribute__((aligned(32)));
void uart_dma_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);
}
坑 2:DMA 接收后未 Invalidate,CPU 读到旧数据
现象:ADC DMA 采集的数据不变,或 Ethernet 接收包解析错误。
原因:DMA 将新数据写入 SRAM,但 CPU 读的是 Cache 中的旧数据。
解决:接收完成后调用 SCB_InvalidateDCache_by_Addr()。
uint8_t rx_buf[128] __attribute__((aligned(32)));
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, sizeof(rx_buf)); // 关键!
process_data(rx_buf, sizeof(rx_buf));
}
坑 3:MPU 属性配置错误,触发 HardFault
现象:配置 MPU 后,访问 DMA 缓冲区立即 HardFault。
原因:MPU 区域未按 32 字节对齐,或属性组合非法(如 Normal 内存却设为 Device)。
解决:确保基地址和大小对齐(大小需为 2 的幂,且 ≥32 字节),属性使用 ARM_MPU_ATTR_NORMAL_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_ENABLE;
HAL_MPU_ConfigRegion(&MPU_InitStruct);
HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT);
}
坑 4:Clean/Invalidate 地址未对齐,操作越界
现象:调用 Clean/Invalidate 后,相邻变量被意外修改。
原因:Cortex-M7 的 Cache 操作以 32 字节行为单位,若地址未对齐,会波及相邻数据。
解决:缓冲区必须 32 字节对齐,长度建议向上取整到 32 的倍数。使用 __attribute__((aligned(32))) 或 ALIGN_32BYTES 宏。
__attribute__((aligned(32))) uint8_t dma_buf[256];
// 或使用 CMSIS 宏
ALIGN_32BYTES(uint8_t dma_buf[256]);
四、完整示例:ADC + DMA + Cache 维护
#include "stm32h7xx_hal.h"
#define ADC_BUF_LEN 128
ALIGN_32BYTES(uint16_t adc_buf[ADC_BUF_LEN]);
void ADC_DMA_Init(void) {
// ... ADC 和 DMA 初始化代码省略
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_BUF_LEN);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
// DMA 传输完成,Invalidate Cache 以读取最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
// 处理数据
for (int i = 0; i < ADC_BUF_LEN; i++) {
float voltage = adc_buf[i] * 3.3f / 65535.0f;
// ...
}
// 若需再次启动 DMA,且 CPU 修改了 adc_buf,则先 Clean
// SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
// HAL_ADC_Start_DMA(...);
}
五、注意事项与最佳实践
- 优先使用 MPU 将 DMA 缓冲区设为 Non-Cacheable,简单可靠,避免手动维护遗漏。
- 若必须使用 Cache,严格遵循“发送前 Clean,接收后 Invalidate” 原则。
- 缓冲区必须 32 字节对齐,长度建议为 32 的倍数。
- 避免在 DMA 传输过程中 CPU 访问同一缓冲区,否则需额外同步。
- 使用
SCB_EnableDCache()后,所有 DMA 相关代码都需检查一致性。 - 调试时若发现数据“时好时坏”,优先怀疑 Cache 问题。
- 参考 ST 官方文档 AN4838《Managing memory protection unit (MPU) in STM32 MCUs》和 AN4839《Level 1 cache on STM32F7 Series and STM32H7 Series》。
掌握以上要点,你就能在 STM32H7 上驯服 Cache 与 DMA,让高性能与数据一致性兼得。