一、问题背景:DMA与Cache的冲突

在STM32F4系列(Cortex-M4内核)中,当启用D-Cache(数据缓存)时,CPU访问内存会优先读写Cache,而DMA外设直接访问物理内存(SRAM)。若DMA将串口数据写入SRAM,而CPU从Cache读取旧数据,就会得到过时内容。反之,CPU写入的数据若未回写(Clean)到SRAM,DMA可能读取错误数据。

对于串口不定长接收,常用DMA+空闲中断(IDLE)或超时判断帧结束。双缓冲(Double Buffer)可提高吞吐,但每次DMA切换缓冲区后,CPU必须使对应缓冲区的Cache行失效(Invalidate),才能读到DMA写入的新数据。

二、双缓冲接收机制与Cache失效场景

2.1 双缓冲原理

  • 使用两个缓冲区(如buf1和buf2),DMA交替填充。
  • 当DMA完成一个缓冲区传输(或半传输中断),切换到另一个缓冲区,同时触发中断通知CPU处理已满的缓冲区。
  • 典型配置:DMA循环模式(Circular Mode)+ 半传输/传输完成中断,或使用空闲中断判断帧尾。

2.2 Cache一致性问题具体表现

  • 数据错乱:CPU从Cache读取旧数据,导致接收内容不完整或重复。
  • 数据丢失:DMA写入新数据后,CPU未失效Cache,读取时可能跳过新数据。
  • 性能下降:频繁的Cache操作(如全失效)会降低效率,需精准操作。

三、解决方案:SCB_InvalidateDCache实战

3.1 核心API介绍

Cortex-M4提供CMSIS函数:

void SCB_InvalidateDCache_by_Addr(uint32_t *addr, int32_t dsize);
  • 作用:使指定地址范围的数据Cache行失效,下次CPU访问时从SRAM重新加载。
  • 注意:地址需32字节对齐(Cache行大小),长度需为32的倍数,否则可能无效或影响相邻数据。

3.2 配置步骤

  1. 使能D-Cache(若未使能):
    SCB_EnableDCache();
    
  2. 配置DMA:使用双缓冲模式(或循环模式+半传输中断),确保缓冲区地址32字节对齐。
  3. 在中断处理中失效Cache:在DMA切换缓冲区后、CPU读取数据前,调用失效函数。

3.3 完整代码示例(基于STM32F4 HAL库)

以下示例使用UART5 + DMA循环模式,双缓冲接收不定长数据(以空闲中断判断帧结束)。

// 缓冲区定义(注意32字节对齐)
#define BUF_SIZE 256
__attribute__((aligned(32))) uint8_t rx_buf[2][BUF_SIZE];
volatile uint8_t current_buf = 0;

// DMA配置(循环模式,双缓冲)
void UART_DMA_Init(void)
{
    // 使能D-Cache(若未使能)
    SCB_EnableDCache();

    // 配置UART5 DMA接收(略)...
    // 使用HAL_UART_Receive_DMA(&huart5, rx_buf[0], BUF_SIZE);
    // 然后调用HAL_UART_DMAPause/Resume实现双缓冲切换,或使用DMA双缓冲模式(需寄存器配置)
    // 这里以循环模式+半传输中断为例:
    __HAL_DMA_ENABLE_IT(&hdma_uart5_rx, DMA_IT_HT); // 半传输中断
    __HAL_DMA_ENABLE_IT(&hdma_uart5_rx, DMA_IT_TC); // 传输完成中断
}

// DMA中断回调(在HAL_UART_RxCpltCallback中处理)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == UART5)
    {
        // 判断是半传输还是全传输(循环模式)
        if (__HAL_DMA_GET_FLAG(&hdma_uart5_rx, DMA_FLAG_HTIF))
        {
            // 半传输完成,当前缓冲区为buf[0]
            current_buf = 0;
            // 失效buf[0]的Cache
            SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf[0], BUF_SIZE);
            // 处理数据(如解析帧)
            ProcessData(rx_buf[0], BUF_SIZE);
        }
        if (__HAL_DMA_GET_FLAG(&hdma_uart5_rx, DMA_FLAG_TCIF))
        {
            // 全传输完成,当前缓冲区为buf[1]
            current_buf = 1;
            SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf[1], BUF_SIZE);
            ProcessData(rx_buf[1], BUF_SIZE);
        }
    }
}

// 处理数据(示例)
void ProcessData(uint8_t *buf, uint16_t len)
{
    // 此时buf中的数据是DMA写入的最新内容
    // 注意:由于是循环模式,可能包含上一帧残留,需根据协议解析
}

注意:上述代码中,SCB_InvalidateDCache_by_Addr的地址和长度必须对齐32字节。若缓冲区大小不是32的倍数,需向上取整。

3.4 更精确的失效策略

对于不定长数据,可在空闲中断(IDLE)中获取实际接收长度,然后仅失效该长度范围内的Cache行:

// 空闲中断处理(需配置UART空闲中断)
void UART_IDLE_Handler(UART_HandleTypeDef *huart)
{
    if (huart->Instance == UART5)
    {
        // 清除IDLE标志
        __HAL_UART_CLEAR_IDLEFLAG(huart);
        
        // 获取DMA剩余计数,计算实际接收长度
        uint16_t remain = __HAL_DMA_GET_COUNTER(&hdma_uart5_rx);
        uint16_t received = BUF_SIZE - remain;
        
        // 失效当前缓冲区的前received字节(注意对齐)
        uint32_t aligned_len = (received + 31) & ~31; // 向上取整到32
        SCB_InvalidateDCache_by_Addr((uint32_t *)rx_buf[current_buf], aligned_len);
        
        // 处理数据
        ProcessData(rx_buf[current_buf], received);
        
        // 切换缓冲区(需重新配置DMA地址)
        current_buf ^= 1;
        // 重新启动DMA接收(略)
    }
}

四、注意事项与常见陷阱

  • 地址对齐:缓冲区起始地址必须32字节对齐,否则SCB_InvalidateDCache_by_Addr可能无效或报错。可使用__attribute__((aligned(32)))memalign
  • 长度对齐:失效长度需为32的倍数,否则可能只失效部分行,导致数据残留。
  • 中断优先级:DMA中断和空闲中断优先级需合理设置,避免数据覆盖。
  • 双缓冲切换:在循环模式下,半传输和全传输中断交替触发,需确保处理完一个缓冲区后再切换,防止DMA写入未处理缓冲区。
  • 其他外设:若使用DMA发送,CPU写入数据后需调用SCB_CleanDCache_by_Addr,确保数据回写到SRAM。
  • 性能考量:频繁失效Cache会降低性能,建议仅在必要时失效,并尽量缩小失效范围。

五、总结

在STM32F4上使用DMA+双缓冲接收串口数据时,Cache一致性是不可忽视的问题。通过SCB_InvalidateDCache_by_Addr精准失效缓冲区,可确保CPU读取到DMA写入的最新数据。本文提供的代码和步骤可直接应用于实际项目,但需根据具体场景调整缓冲区大小和中断逻辑。掌握这一技巧,能有效避免嵌入式开发中的隐性Bug,提升系统稳定性。