一、问题现象:ADC 数据偶尔“跳变”
最近在基于 STM32H743 的项目中,使用 ADC + DMA 连续采集 8 通道数据,发现一个诡异现象:
- 系统运行几分钟后,ADC 缓冲区中个别通道的值突然变成 0 或极大值;
- 重启后正常,但运行一段时间又复现;
- 关闭 D-Cache 后问题消失,但 CPU 性能明显下降。
这明显是 D-Cache 与 DMA 一致性 问题。STM32H7 的 Cortex-M7 内核带有 D-Cache,DMA 直接访问物理内存,而 CPU 访问的是 Cache。若两者不同步,就会读到旧数据或写入丢失。
二、原理:为什么 DMA 与 D-Cache 会“打架”
Cortex-M7 的 D-Cache 是**写回(Write-Back)、写分配(Write-Allocate)**策略:
- CPU 写数据时,先写 Cache,不立即写回内存;
- CPU 读数据时,若 Cache 未命中,从内存加载到 Cache。
DMA 则绕过 Cache 直接读写内存。因此:
- CPU 写 → DMA 读:CPU 数据可能还在 Cache 中未写回,DMA 读到旧数据;
- DMA 写 → CPU 读:DMA 已更新内存,但 CPU 可能读到 Cache 中的旧数据。
解决方法是使用 CMSIS 提供的 Cache 维护函数:
-
SCB_CleanDCache_by_Addr(addr, size):将 Cache 内容写回内存(Clean); -
SCB_InvalidateDCache_by_Addr(addr, size):丢弃 Cache 内容,强制从内存重新加载(Invalidate); -
SCB_CleanInvalidateDCache_by_Addr(addr, size):先 Clean 再 Invalidate。
三、排查过程:SCB_CleanDCache_by_Addr 的误用
3.1 错误代码
原代码在启动 DMA 前调用了:
SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
看似正确,但 adc_buf 定义如下:
uint16_t adc_buf[BUF_SIZE]; // BUF_SIZE = 256
问题出在 地址对齐 和 长度单位。
3.2 三个致命错误
-
地址未 32 字节对齐:Cortex-M7 的 Cache 行大小为 32 字节。
SCB_CleanDCache_by_Addr要求地址 32 字节对齐,否则可能操作到相邻数据,甚至触发 HardFault。adc_buf是uint16_t数组,默认对齐 2 字节,不满足要求。 - 长度参数错误:函数第二个参数是 字节数,但内部按 32 字节行处理。若长度不是 32 的倍数,会多清理一行,可能破坏其他变量。
-
方向混淆:DMA 是“读”内存(外设到内存),CPU 在 DMA 完成后要读数据,应该用
Invalidate,而不是Clean。用Clean反而把 Cache 中的旧数据写回,覆盖了 DMA 的新数据!
四、正确配置步骤
4.1 内存对齐
将缓冲区定义为 32 字节对齐:
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];
或使用 ALIGN_32BYTES 宏(CMSIS 提供)。
4.2 根据数据流向选择维护函数
-
CPU 写 → DMA 读(如发送):先
SCB_CleanDCache_by_Addr,再启动 DMA。 -
DMA 写 → CPU 读(如接收):DMA 完成后,调用
SCB_InvalidateDCache_by_Addr,再读数据。 -
双向:使用
SCB_CleanInvalidateDCache_by_Addr。
4.3 完整示例(ADC + DMA 接收)
#include "stm32h7xx.h"
#define BUF_SIZE 256
__attribute__((aligned(32))) uint16_t adc_buf[BUF_SIZE];
volatile uint8_t dma_done = 0;
void ADC_DMA_Init(void) {
// ... ADC 和 DMA 初始化代码省略
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
}
// DMA 传输完成回调
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
// 无效化 Cache,确保 CPU 读取到 DMA 写入的最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
dma_done = 1;
}
int main(void) {
HAL_Init();
SystemClock_Config();
SCB_EnableDCache(); // 使能 D-Cache
ADC_DMA_Init();
while (1) {
if (dma_done) {
dma_done = 0;
// 安全读取 adc_buf
process_adc_data(adc_buf, BUF_SIZE);
// 重新启动 DMA 前,若 CPU 修改了缓冲区,需 Clean
// SCB_CleanDCache_by_Addr((uint32_t*)adc_buf, sizeof(adc_buf));
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_SIZE);
}
}
}
五、注意事项与避坑指南
-
地址必须 32 字节对齐,长度建议为 32 的倍数,否则用
SCB_CleanDCache_by_Addr等函数可能越界。 - 区分 Clean 与 Invalidate:Clean 是“写回”,Invalidate 是“丢弃”。方向搞反会导致数据错乱。
- DMA 缓冲区不要与栈或全局变量混用,避免 Cache 行共享导致误伤。
- 使用 MPU 配置 Write-Through 或 Non-Cacheable 内存:对于频繁 DMA 的缓冲区,可将其配置为 Non-Cacheable,一劳永逸,但会牺牲 CPU 访问速度。
- 调试时关闭 D-Cache 可快速定位,但正式产品必须开启并正确维护。
- CMSIS 函数内部已处理 32 字节对齐,但传入未对齐地址仍可能出错,务必自行保证。
六、总结
STM32H7 的 D-Cache 与 DMA 一致性问题是嵌入式开发中的经典“坑”。SCB_CleanDCache_by_Addr 用错只是表象,根源在于对 Cache 维护机制理解不深。牢记三点:对齐、方向、时机。正确使用 Cache 维护函数,既能享受 H7 的高性能,又能保证数据可靠。