STM32 HAL库中DMA与Cache一致性问题的定位与规避实战
一、问题现象:为什么数据“不更新”
在STM32F7/H7等带D-Cache的MCU上,常见两类诡异现象:
- 接收方向:DMA把串口/ADC数据写入SRAM,CPU读到的却是旧值或全0。
- 发送方向:CPU填好缓冲区后启动DMA,总线上发出的却是旧数据或部分新数据。
根因是CPU与DMA看到的内存视图不一致。Cortex-M7的D-Cache按Cache Line(通常32字节)操作,CPU写数据可能只停留在Cache中(Write-Back),DMA直接访问物理SRAM自然读不到;反之DMA写入SRAM后,CPU若命中旧Cache行,也不会去内存取新值。
关键认知:DMA不经过Cache,它只认物理内存。
二、原理:Cache Line与三种维护操作
- Clean(回写):把Cache中脏行写回SRAM,供DMA读取。用于发送前。
- Invalidate(无效化):丢弃Cache行内容,强制CPU下次从SRAM取。用于接收后。
- Clean+Invalidate:两者都做,常用于接收缓冲区复用前。
HAL库提供对应API(定义在stm32h7xx_hal.h等):
void SCB_CleanDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
void SCB_CleanInvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
注意dsize单位为字节,且地址需按32字节对齐,长度建议向上取整到Cache Line倍数,否则会误伤相邻数据。
三、定位方法:三步确认是否命中Cache问题
-
关闭D-Cache复测:在
main初始化后调用SCB_DisableDCache()(仅调试用),若问题消失,基本可判定为一致性问题。 - 观察地址对齐:打印DMA缓冲区地址,检查是否32字节对齐;非对齐缓冲区极易在Invalidate时误伤邻变量。
- 单步对比内存:用调试器Memory窗口直接看SRAM物理地址,与CPU变量值对比,二者不一致即为Cache未同步。
四、规避方案(按推荐度排序)
方案A:缓冲区放入Non-Cacheable区域(最稳)
利用链接脚本把DMA缓冲区放到MPU配置为Non-Cacheable的RAM段,彻底免维护。以H7为例,在.ld中新增段:
.dma_buffer (NOLOAD) :
{
. = ALIGN(32);
*(.dma_buffer)
. = ALIGN(32);
} > RAM_D1
C代码中声明并对齐:
__attribute__((section(".dma_buffer"), aligned(32)))
uint8_t dma_rx_buf[256];
再用MPU把该区域设为MPU_ACCESS_NOT_CACHEABLE。此法零运行时开销,适合高频DMA。
方案B:手动维护Cache(改动最小)
适合已有工程快速修复。核心原则:发送前Clean,接收后Invalidate。
#define DMA_BUF_SIZE 256
#define CACHE_LINE 32
__attribute__((aligned(32)))
static uint8_t rx_buf[DMA_BUF_SIZE];
__attribute__((aligned(32)))
static uint8_t tx_buf[DMA_BUF_SIZE];
/* 发送:CPU写完后Clean,再启动DMA */
void uart_dma_send(uint8_t *data, uint16_t len)
{
memcpy(tx_buf, data, len);
SCB_CleanDCache_by_Addr((uint32_t *)tx_buf, DMA_BUF_SIZE);
HAL_UART_Transmit_DMA(&huart1, tx_buf, len);
}
/* 接收完成回调:先Invalidate,再读数据 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);
process_data(rx_buf, DMA_BUF_SIZE);
/* 重新挂接收前,先Clean+Invalidate,避免脏行回写覆盖新数据 */
SCB_CleanInvalidateDCache_by_Addr((uint32_t *)rx_buf, DMA_BUF_SIZE);
HAL_UART_Receive_DMA(&huart1, rx_buf, DMA_BUF_SIZE);
}
五、注意事项
-
长度必须按Cache Line向上取整:
Invalidate按行操作,长度不足会漏刷,过长会误伤相邻变量。 - 接收回调里先Invalidate再读,顺序反了读到旧值。
- 环形缓冲区要按“已写区间”精确维护,不能整块无脑Invalidate,否则可能丢弃CPU刚写入但未回写的数据。
- DMA描述符、链表同样要对齐并维护,H7的MDMA尤其要注意。
- 调试器访问内存可能触发Cache行为,定位时以物理地址为准。
- 若使用
HAL_UARTEx_ReceiveToIdle_DMA,在RxEventCallback中同样执行Invalidate。
六、小结
DMA与Cache一致性问题的本质是“两个主设备看同一块内存”。工程上优先用Non-Cacheable段从架构上消除问题;存量工程用Clean/Invalidate + 32字节对齐快速修复。牢记口诀:发前Clean,收后Invalidate,长度对齐Cache Line,即可规避绝大多数隐蔽Bug。
暂无评论,来抢沙发~