# 引言 在嵌入式开发中,STM32F4 凭借高性能 Cortex-M4 内核和丰富外设成为主流选择。然而,当启用 D-Cache(数据缓存)后,DMA 与 CPU 之间的数据一致性成为隐患。例如,使用 DMA 接收串口数据时,CPU 可能读到过期的缓存数据,导致数据错乱。本文将从原理到实战,讲解如何利用 MPU(内存保护单元)配置 D-Cache 策略,彻底解决该问题。 # 1. 问题根源:D-Cache 与 DMA 的冲突 ## 1.1 D-Cache 的工作原理 D-Cache 是 CPU 内部的高速缓存,用于减少对慢速 RAM 的访问次数。当 CPU 读取内存时,会先检查 Cache;若命中则直接返回,否则从 RAM 加载并缓存。写入时,若采用 Write-Back 策略,数据先写入 Cache,标记为脏,待后续回写到 RAM。 ## 1.2 DMA 的旁路特性 DMA 控制器直接访问 RAM,不经过 CPU 的 Cache。因此,当 DMA 向 RAM 写入数据时,Cache 中可能保留着旧数据;当 CPU 读取该地址时,会命中 Cache 的旧数据,而非最新数据。反之,CPU 写入数据后,若尚未回写,DMA 读取 RAM 时也会得到旧数据。 ## 1.3 典型场景 - DMA 接收串口/SPI 数据到缓冲区,CPU 处理该缓冲区。 - CPU 填充缓冲区,DMA 发送到外设。 - 双缓冲或多缓冲机制中切换时。 # 2. 解决方案:MPU 配置 D-Cache 策略 ## 2.1 MPU 简介 MPU 允许将内存区域划分为多个区域,并设置访问权限和 Cache 属性。通过配置区域属性,可以控制 D-Cache 的行为。 ## 2.2 两种策略 - **策略一:关闭 DMA 缓冲区对应的 Cache**(Write-Through 或 Non-cacheable)。 - **策略二:使用 Write-Through 模式**,即 CPU 写入时同时更新 RAM,读取时仍可缓存,但 DMA 写入后,CPU 需主动失效 Cache 行。 推荐使用策略一,简单可靠。 # 3. 实战步骤 ## 3.1 硬件环境 - STM32F407 开发板 - 串口 DMA 收发 - 外部 SDRAM(可选,但更易触发问题) ## 3.2 配置 MPU 使用 STM32CubeMX 或直接代码配置。以下为直接代码方式: ```c #include "stm32f4xx.h" void MPU_Config(void) { // 禁用 MPU MPU->CTRL = 0; // 配置区域0:DMA缓冲区(例如 0x20000000 起始,大小 32KB) MPU->RNR = 0; // 区域编号 MPU->RBAR = 0x20000000; // 基地址 // 设置属性:大小=32KB,TEX=0,C=0,B=0(Non-cacheable),允许读写,可执行 MPU->RASR = (0 << 0) | // 禁用指令访问 (1 << 1) | // 允许特权访问 (1 << 2) | // 允许用户访问 (0 << 3) | // 保留 (0 << 4) | // 保留 (0 << 5) | // 保留 (0 << 6) | // 保留 (0 << 7) | // 保留 (0 << 8) | // 保留 (0 << 9) | // 保留 (0 << 10) | // 保留 (0 << 11) | // 保留 (0 << 12) | // 保留 (0 << 13) | // 保留 (0 << 14) | // 保留 (0 << 15) | // 保留 (0 << 16) | // 保留 (0 << 17) | // 保留 (0 << 18) | // 保留 (0 << 19) | // 保留 (0 << 20) | // 保留 (0 << 21) | // 保留 (0 << 22) | // 保留 (0 << 23) | // 保留 (0 << 24) | // 保留 (0 << 25) | // 保留 (0 << 26) | // 保留 (0 << 27) | // 保留 (0 << 28) | // 保留 (0 << 29) | // 保留 (0 << 30) | // 保留 (0 << 31); // 保留 // 实际上,RASR 的位域需要正确设置,建议使用 CMSIS 提供的宏 // 这里简化,实际使用如下: MPU->RASR = (0x0 << 0) | // TEX=0 (0x1 << 1) | // C=0,B=0 => Non-cacheable (0x0 << 3) | // S=0 (0x0 << 4) | // 保留 (0x0 << 5) | // 保留 (0x0 << 6) | // 保留 (0x0 << 7) | // 保留 (0x0 << 8) | // 保留 (0x0 << 9) | // 保留 (0x0 << 10) | // 保留 (0x0 << 11) | // 保留 (0x0 << 12) | // 保留 (0x0 << 13) | // 保留 (0x0 << 14) | // 保留 (0x0 << 15) | // 保留 (0x0 << 16) | // 保留 (0x0 << 17) | // 保留 (0x0 << 18) | // 保留 (0x0 << 19) | // 保留 (0x0 << 20) | // 保留 (0x0 << 21) | // 保留 (0x0 << 22) | // 保留 (0x0 << 23) | // 保留 (0x0 << 24) | // 保留 (0x0 << 25) | // 保留 (0x0 << 26) | // 保留 (0x0 << 27) | // 保留 (0x0 << 28) | // 保留 (0x0 << 29) | // 保留 (0x0 << 30) | // 保留 (0x0 << 31); // 保留 // 但实际需要设置 SIZE 和 ENABLE 位,见下方正确写法 } ``` **正确配置示例**(使用 CMSIS 宏): ```c void MPU_Config(void) { // 禁用 MPU MPU->CTRL = 0; // 配置区域0:DMA缓冲区(例如 0x20000000 起始,大小 32KB) MPU->RNR = 0; MPU->RBAR = 0x20000000; // 设置属性:TEX=0,C=0,B=0 => Non-cacheable,允许读写,可执行 MPU->RASR = (0x0 << 0) | // TEX=0 (0x0 << 1) | // C=0 (0x0 << 2) | // B=0 (0x0 << 3) | // S=0 (0x0 << 4) | // 保留 (0x0 << 5) | // 保留 (0x0 << 6) | // 保留 (0x0 << 7) | // 保留 (0x0 << 8) | // 保留 (0x0 << 9) | // 保留 (0x0 << 10) | // 保留 (0x0 << 11) | // 保留 (0x0 << 12) | // 保留 (0x0 << 13) | // 保留 (0x0 << 14) | // 保留 (0x0 << 15) | // 保留 (0x0 << 16) | // 保留 (0x0 << 17) | // 保留 (0x0 << 18) | // 保留 (0x0 << 19) | // 保留 (0x0 << 20) | // 保留 (0x0 << 21) | // 保留 (0x0 << 22) | // 保留 (0x0 << 23) | // 保留 (0x0 << 24) | // 保留 (0x0 << 25) | // 保留 (0x0 << 26) | // 保留 (0x0 << 27) | // 保留 (0x0 << 28) | // 保留 (0x0 << 29) | // 保留 (0x0 << 30) | // 保留 (0x0 << 31); // 保留 // 实际应使用宏定义,例如: // MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) | (0x0 << MPU_RASR_C_Pos) | (0x0 << MPU_RASR_B_Pos) | (0x1 << MPU_RASR_ENABLE_Pos) | (0x1 << MPU_RASR_SIZE_Pos); // 但为了简洁,这里省略。 } ``` **推荐使用 STM32CubeMX 生成**,在 `MPU Configuration` 中设置区域属性为 `Non-cacheable`。 ## 3.3 启用 MPU 和 D-Cache ```c void Cache_Init(void) { // 启用 D-Cache 和 I-Cache(可选) SCB_EnableDCache(); SCB_EnableICache(); // 配置 MPU MPU_Config(); // 启用 MPU MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; __DSB(); __ISB(); } ``` ## 3.4 使用 DMA 缓冲区 定义缓冲区时,确保其位于 MPU 配置的区域内: ```c // 定义在 0x20000000 起始的 32KB 区域内 uint8_t dma_rx_buffer[1024] __attribute__((section(".dma_buffer"))); ``` 在链接脚本中,将 `.dma_buffer` 段定位到 0x20000000。或者使用 `__attribute__((aligned(32)))` 并手动指定地址。 # 4. 完整代码示例 以下是一个串口 DMA 接收的完整示例,使用 MPU 配置缓冲区为 Non-cacheable: ```c #include "stm32f4xx.h" #include // 缓冲区定义 uint8_t rx_buffer[256] __attribute__((aligned(32))); // MPU 配置 void MPU_Config(void) { // 禁用 MPU MPU->CTRL = 0; // 配置区域0:覆盖整个 SRAM(例如 0x20000000,大小 128KB) MPU->RNR = 0; MPU->RBAR = 0x20000000; // 设置属性:Non-cacheable,允许读写,可执行 MPU->RASR = (0x0 << MPU_RASR_TEX_Pos) | // TEX=0 (0x0 << MPU_RASR_C_Pos) | // C=0 (0x0 << MPU_RASR_B_Pos) | // B=0 (0x0 << MPU_RASR_S_Pos) | // S=0 (0x1 << MPU_RASR_ENABLE_Pos) | // 使能区域 (0x0 << MPU_RASR_AP_Pos) | // 特权/用户读写 (0x0 << MPU_RASR_XN_Pos) | // 可执行 (0x0 << MPU_RASR_SIZE_Pos); // 大小,需计算,例如 128KB => 0x10 // 注意:SIZE 位需要正确设置,128KB 对应 0x10(2^17) // 这里简化,实际使用宏定义。 } // 初始化串口 DMA void UART_DMA_Init(void) { // 使能时钟 RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN; RCC->APB1ENR |= RCC_APB1ENR_USART2EN; // 配置 USART2 引脚等(省略) // 配置 DMA 流 DMA2_Stream5->CR &= ~DMA_SxCR_EN; // 禁用 DMA2_Stream5->PAR = (uint32_t)&USART2->DR; DMA2_Stream5->M0AR = (uint32_t)rx_buffer; DMA2_Stream5->NDTR = sizeof(rx_buffer); DMA2_Stream5->CR = DMA_SxCR_CHSEL_1 | DMA_SxCR_PL_1 | DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0 | DMA_SxCR_MINC | DMA_SxCR_DIR_0 | DMA_SxCR_TCIE; // 使能 DMA 中断 NVIC_EnableIRQ(DMA2_Stream5_IRQn); // 使能 DMA DMA2_Stream5->CR |= DMA_SxCR_EN; } // DMA 中断处理 void DMA2_Stream5_IRQHandler(void) { if (DMA2->LISR & DMA_LISR_TCIF5) { DMA2->LIFCR |= DMA_LIFCR_CTCIF5; // 数据已接收,可直接处理 rx_buffer,因为它是 Non-cacheable // 处理数据... } } int main(void) { // 初始化时钟、GPIO 等(省略) // 配置 MPU 和 Cache MPU_Config(); SCB_EnableDCache(); SCB_EnableICache(); MPU->CTRL = MPU_CTRL_ENABLE_Msk | MPU_CTRL_PRIVDEFENA_Msk; __DSB(); __ISB(); // 初始化串口 DMA UART_DMA_Init(); while (1) { // 主循环 } } ``` # 5. 注意事项 - **缓冲区对齐**:MPU 区域要求基地址和大小对齐,通常按 32 字节对齐。 - **区域大小**:MPU 区域大小必须是 2 的幂,且最小 32 字节。 - **覆盖范围**:如果 DMA 缓冲区分散,可配置多个 MPU 区域,或直接配置整个 SRAM 为 Non-cacheable,但会牺牲性能。 - **Cache 维护**:如果使用 Write-Through 策略,仍需在 DMA 写入后执行 `SCB_InvalidateDCache_by_Addr()` 使缓存行失效。 - **中断安全**:在中断中访问缓冲区时,确保 MPU 配置已生效。 - **性能权衡**:Non-cacheable 会降低 CPU 访问该区域的速度,但 DMA 缓冲区通常较小,影响有限。 # 6. 总结 通过 MPU 配置 DMA 缓冲区为 Non-cacheable,可以彻底避免 D-Cache 与 DMA 的数据一致性问题。本文提供了原理分析和完整代码,开发者可根据实际需求调整区域大小和策略。在追求极致性能时,可考虑 Write-Through 加手动失效,但需谨慎处理时序。掌握这一技巧,将显著提升 STM32F4 项目的稳定性。