一、问题现象:偶发数据错乱
最近在调试一个基于 STM32H743 的项目时,遇到了一个非常典型的偶发问题:
- 使用 ADC 连续扫描 + DMA 循环模式采集 8 通道数据,缓冲区定义在
0x24000000(AXI SRAM)。 - 大部分时间数据正常,但偶尔会出现某个通道的值突然变成 0 或跳变到极大值。
- 降低主频或关闭 D-Cache 后问题消失。
这类“时好时坏、与 Cache 相关”的现象,几乎可以断定是 D-Cache 与 DMA 的数据一致性 问题。
二、原理:D-Cache 与 DMA 为何会冲突
Cortex-M7 的 D-Cache 是写回(Write-Back)、写分配(Write-Allocate)的。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM;而 DMA 是直接访问物理内存的,它看不到 Cache 里的最新数据。
- CPU 写 → DMA 读:CPU 写入的数据还在 Cache 中(脏行),DMA 从 SRAM 读到的是旧数据。
- DMA 写 → CPU 读:DMA 把新数据写入 SRAM,但 CPU 读到的却是 Cache 中的旧副本(若该行之前被缓存过)。
解决方法是使用 Cache 维护操作:
-
SCB_CleanDCache_by_Addr():将 Cache 中的脏数据写回 SRAM(Clean)。 -
SCB_InvalidateDCache_by_Addr():丢弃 Cache 中的副本,强制下次从 SRAM 重新加载(Invalidate)。 -
SCB_CleanInvalidateDCache_by_Addr():先 Clean 再 Invalidate。
三、根因:维护操作放错位置
很多开发者知道要用这些函数,但放错位置同样会导致偶发错误。常见错误有两种:
错误 1:DMA 发送前忘记 Clean
// 错误示例:CPU 填充缓冲区后直接启动 DMA
fill_buffer(tx_buf, len);
HAL_DMA_Start(&hdma, (uint32_t)tx_buf, (uint32_t)&uart->TDR, len);
// 缺少 SCB_CleanDCache_by_Addr
此时 tx_buf 的最新数据可能还在 Cache 中,DMA 发送的是旧数据。
错误 2:DMA 接收后忘记 Invalidate,或 Invalidate 时机不对
// 错误示例:DMA 接收完成中断中直接读缓冲区
void DMA_IRQHandler(void) {
process(rx_buf, len); // 可能读到 Cache 中的旧数据
}
正确做法是在 DMA 启动之前 Invalidate 接收缓冲区,并在 DMA 完成之后再 Invalidate 一次(如果 CPU 之前访问过该缓冲区)。
更隐蔽的错误是:在 DMA 传输过程中调用 Invalidate。如果此时 CPU 刚写过缓冲区,Invalidate 会丢弃尚未写回的脏数据,导致数据丢失。
四、正确配置步骤
以 ADC + DMA 循环采集为例,推荐流程:
- 将 DMA 缓冲区放在非缓存区域,或使用 MPU 配置为 Write-Through/Non-Cacheable。
- 若必须使用 Cache,则在 DMA 启动前对接收缓冲区执行
SCB_InvalidateDCache_by_Addr。 - DMA 传输完成后(中断或轮询标志),再次执行
SCB_InvalidateDCache_by_Addr再读取数据。 - 对于发送方向,在启动 DMA 前执行
SCB_CleanDCache_by_Addr。 - 注意地址和长度必须 32 字节对齐,否则维护操作可能无效或越界。
五、完整代码示例
#include "stm32h7xx.h"
#define ADC_BUF_LEN 8
#define ADC_BUF_SIZE (ADC_BUF_LEN * sizeof(uint16_t))
// 32 字节对齐的 DMA 缓冲区
__attribute__((aligned(32))) static uint16_t adc_dma_buf[ADC_BUF_LEN];
void adc_dma_init(void)
{
// 1. 启动 DMA 前,无效化 Cache,确保 CPU 不会读到旧副本
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, ADC_BUF_SIZE);
// 2. 配置并启动 ADC + DMA(循环模式)
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_dma_buf, ADC_BUF_LEN);
}
// DMA 传输完成回调(例如半满/全满中断)
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 3. DMA 写入 SRAM 后,CPU 读取前必须再次无效化 Cache
SCB_InvalidateDCache_by_Addr((uint32_t *)adc_dma_buf, ADC_BUF_SIZE);
// 4. 此时读取的数据才是 DMA 写入的最新值
for (int i = 0; i < ADC_BUF_LEN; i++) {
process_adc_value(adc_dma_buf[i]);
}
}
// 发送方向示例
__attribute__((aligned(32))) static uint8_t tx_buf[64];
void uart_dma_send(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
// 5. 启动 DMA 前 Clean,确保数据写回 SRAM
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, len);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
六、注意事项与避坑指南
-
地址与长度对齐:
SCB_*DCache_by_Addr要求地址 32 字节对齐,长度建议向上取整到 32 字节的倍数。 - 不要频繁 Invalidate:Invalidate 会丢弃 Cache 中的脏数据,若在 CPU 写后、DMA 读前调用,会导致数据丢失。
- 循环 DMA 的特殊性:循环模式下 DMA 持续写入,CPU 读取前必须 Invalidate,且最好使用双缓冲(半满/全满)减少冲突。
- MPU 配置更优:对于频繁 DMA 的缓冲区,建议用 MPU 将其配置为 Non-Cacheable 或 Write-Through,从根本上避免一致性问题。
-
调试技巧:关闭 D-Cache 后问题消失,基本可确认是 Cache 一致性问题;也可用
SCB_InvalidateDCache()全局无效化辅助定位。 - 性能权衡:Cache 维护操作有开销,频繁调用会降低性能,需在正确性与效率间平衡。
七、总结
STM32H7 的 D-Cache 与 DMA 数据一致性问题,本质是“CPU 视角”与“DMA 视角”看到的内存不一致。Cache 维护操作本身不难,难的是放对位置:发送前 Clean、接收前 Invalidate、接收后再 Invalidate。理解数据流向,配合 32 字节对齐和 MPU 配置,就能彻底告别这类偶发数据错乱。