一、为什么 D-Cache 会让 DMA 数据“出错”?
STM32H7 的 Cortex-M7 内核带 16KB D-Cache。CPU 访问内存时,数据可能只停留在 Cache 中,并未写回 SRAM。而 DMA 控制器直接访问物理内存,不经过 Cache。
- 写方向(CPU 写 → DMA 读):CPU 写入的数据还在 Cache 里(Write-Back 模式),DMA 读到的却是 SRAM 中的旧数据。
- 读方向(DMA 写 → CPU 读):DMA 把新数据写入 SRAM,但 CPU 读的是 Cache 中的旧缓存行。
解决思路:在 DMA 传输前后,手动维护 Cache 一致性。
二、地址对齐:容易被忽视的致命细节
Cache 操作以 32 字节 Cache Line 为单位。若缓冲区地址或长度未按 32 字节对齐,Clean/Invalidate 会波及相邻数据,导致“误伤”。
- 必须对齐:DMA 缓冲区起始地址 32 字节对齐,长度建议为 32 的整数倍。
-
推荐做法:使用
__attribute__((aligned(32)))定义缓冲区,或使用SCB_InvalidateDCache_by_Addr时传入对齐后的地址和长度。
// 正确:32字节对齐的DMA缓冲区
__attribute__((aligned(32))) uint8_t dma_buffer[256];
三、Clean 与 Invalidate 的顺序
- Clean:将 Cache 中已修改的数据写回 SRAM。
- Invalidate:丢弃 Cache 中的内容,强制下次读取从 SRAM 获取。
顺序原则:
- CPU 写 → DMA 读:先 Clean,再启动 DMA。
- DMA 写 → CPU 读:DMA 完成后,先 Invalidate,再读数据。
- DMA 写 → CPU 写同一区域:先 Invalidate,再写,避免旧数据覆盖。
注意:Invalidate 会丢弃未 Clean 的数据!若 CPU 刚写过该区域,必须先 Clean 再 Invalidate。
四、完整代码示例(以 UART DMA 接收为例)
#include "stm32h7xx.h"
#define DMA_BUF_SIZE 256
__attribute__((aligned(32))) uint8_t uart_rx_buf[DMA_BUF_SIZE];
volatile uint8_t dma_rx_complete = 0;
void uart_dma_init(void) {
// 1. 配置UART和DMA(略去具体寄存器配置)
// 2. 启动DMA接收前,Invalidate Cache,确保DMA写入的数据不会被Cache旧值覆盖
SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buf, DMA_BUF_SIZE);
// 3. 启动DMA接收
// HAL_UART_Receive_DMA(&huart1, uart_rx_buf, DMA_BUF_SIZE);
}
// DMA接收完成中断回调
void DMA_RxComplete_Callback(void) {
// 4. DMA写入了新数据,Invalidate Cache,让CPU读到最新数据
SCB_InvalidateDCache_by_Addr((uint32_t*)uart_rx_buf, DMA_BUF_SIZE);
dma_rx_complete = 1;
}
// CPU发送数据前
void uart_send_data(uint8_t *data, uint16_t len) {
// 5. 将CPU写入的数据Clean到SRAM,确保DMA读到最新值
SCB_CleanDCache_by_Addr((uint32_t*)data, len);
// 6. 启动DMA发送
// HAL_UART_Transmit_DMA(&huart1, data, len);
}
五、实测踩坑记录
- 坑1:缓冲区未对齐,相邻变量被误清。定义两个相邻数组,Invalidate 一个导致另一个数据丢失。解决:所有 DMA 缓冲区强制 32 字节对齐。
- 坑2:Clean 后忘记 Invalidate。DMA 接收完成后直接读数据,发现全是旧值。解决:接收完成必须 Invalidate。
- 坑3:在中断中调用 Cache 维护函数耗时过长。H7 的 Cache 操作约几十个周期,高频中断下影响实时性。解决:仅对实际数据区域操作,避免全 Cache 维护。
-
坑4:使用
SCB_InvalidateDCache()全局失效。这会清掉所有 Cache,包括栈和代码,导致程序跑飞。解决:始终使用by_Addr版本。 - 坑5:MPU 配置为 Write-Through 模式。虽然简化了一致性,但性能下降。建议对 DMA 区域配置为 Non-Cacheable,或使用 Write-Back + 手动维护。
六、最佳实践总结
- 优先将 DMA 缓冲区放在 Non-Cacheable 内存区域(通过 MPU 配置),一劳永逸。
- 若必须使用 Cacheable 区域,严格遵循:写前 Clean,读后 Invalidate。
- 缓冲区地址和长度 32 字节对齐。
- 使用
SCB_CleanDCache_by_Addr和SCB_InvalidateDCache_by_Addr,避免全局操作。 - 在 DMA 传输期间,CPU 不要访问该缓冲区,否则需额外同步。
掌握这些,你就能在 STM32H7 上安全地驾驭 D-Cache 与 DMA,兼顾性能与数据一致性。