STM32H7 零拷贝日志存储:MDMA 与双 Bank Flash 并行搬运实战

1. 为什么需要零拷贝日志存储?

在嵌入式开发中,日志系统常面临两大痛点:

  • CPU 占用高:传统方式需 CPU 将日志数据从缓冲区拷贝到 Flash 写入缓冲区,再进行擦写操作,耗时且阻塞主流程。
  • Flash 擦写慢:STM32H7 的 Flash 擦除时间可达数百毫秒,若在日志写入时打断实时任务,将导致系统响应延迟。

STM32H7 提供了 MDMA(Master Direct Memory Access)双 Bank Flash 架构,通过硬件级并行处理,实现日志数据的零拷贝搬运,即数据从内存直接传输到 Flash,无需 CPU 介入,且两个 Bank 可交替写入,隐藏擦除延迟。

2. 核心原理

2.1 MDMA 与普通 DMA 的区别

MDMA 是 STM32H7 中的高级 DMA 控制器,支持:

  • 内存到内存传输:无需外设触发,可自主搬运大块数据。
  • 链表模式:支持多个传输块(Block)串联,自动切换源/目的地址。
  • 双缓冲:在传输过程中可动态更新缓冲区,实现流水线操作。

相比普通 DMA,MDMA 更适合 Flash 编程场景,因为它能处理非对齐数据、支持 32 位带宽,且不占用 CPU 周期。

2.2 双 Bank Flash 交错写入

STM32H7 的 Flash 分为两个 Bank(如 Bank1 和 Bank2),每个 Bank 独立擦除和编程。当向 Bank1 写入日志时,可同时擦除 Bank2 的旧数据,反之亦然。通过 MDMA 交替向两个 Bank 搬运数据,可隐藏擦除时间,实现连续写入。

2.3 零拷贝实现

传统流程:日志缓冲区 -> CPU 拷贝 -> Flash 编程缓冲区 -> 等待擦写。 零拷贝流程:日志缓冲区(内存) -> MDMA 直接搬运 -> Flash 编程寄存器,CPU 仅需配置 MDMA 和 Flash 控制器,然后继续执行主任务。

3. 硬件与软件准备

  • 硬件:STM32H743 开发板(或其他 H7 系列),外部晶振 25MHz。
  • 软件:STM32CubeIDE 1.15+,HAL 库(含 MDMA 和 Flash 驱动)。
  • 关键外设:MDMA、FLASH、RCC(时钟)。

4. 配置步骤

4.1 时钟配置

确保 MDMA 和 Flash 时钟已使能,且 Flash 等待周期正确(H7 在 480MHz 下需 7 个等待周期)。

RCC->AHB3ENR |= RCC_AHB3ENR_MDMAEN;  // 使能 MDMA 时钟
FLASH->ACR |= FLASH_ACR_LATENCY_7WS; // 设置等待周期

4.2 MDMA 配置

使用 HAL 库初始化 MDMA,配置为内存到内存模式,传输方向为内存到 Flash 编程地址。

MDMA_HandleTypeDef hmdma;

void MDMA_Init(void) {
    hmdma.Instance = MDMA_Channel0;
    hmdma.Init.Request = MDMA_REQUEST_MEM2MEM;  // 内存到内存
    hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER; // 块传输
    hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
    hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;   // 源地址递增
    hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD; // 目的地址递增(Flash 编程地址)
    hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
    hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
    hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
    hmdma.Init.BufferTransferLength = 1024; // 每次传输 1024 字(4KB)
    HAL_MDMA_Init(&hmdma);
}

4.3 Flash 编程配置

使用 HAL 的 Flash 编程接口,但需注意:MDMA 写入 Flash 时,Flash 必须处于编程模式,且地址对齐。

void Flash_Init(void) {
    HAL_FLASH_Unlock();
    // 设置编程宽度为 32 位(双字)
    FLASH->CR1 |= FLASH_CR1_PSIZE_1; // 32-bit programming
}

4.4 双 Bank 切换逻辑

定义两个 Bank 的基地址和大小,通过变量切换当前写入 Bank。

#define BANK1_ADDR 0x08000000
#define BANK2_ADDR 0x08020000  // 假设每个 Bank 128KB
#define LOG_SIZE   4096        // 每次日志块大小

uint32_t current_bank = 0; // 0: Bank1, 1: Bank2

void Switch_Bank(void) {
    if (current_bank == 0) {
        // 擦除 Bank2(准备下一次写入)
        HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK2_ADDR, 1, NULL);
        current_bank = 1;
    } else {
        HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK1_ADDR, 1, NULL);
        current_bank = 0;
    }
}

5. 完整代码示例

以下示例演示如何将日志缓冲区中的数据通过 MDMA 写入当前 Bank,并在后台切换 Bank。

#include "stm32h7xx_hal.h"

// 日志缓冲区(内存)
uint32_t log_buffer[1024]; // 4KB

// MDMA 句柄
extern MDMA_HandleTypeDef hmdma;

// 当前写入地址
uint32_t write_addr;

// 初始化 MDMA 和 Flash
void Log_Init(void) {
    MDMA_Init();
    Flash_Init();
    write_addr = BANK1_ADDR; // 从 Bank1 开始
    current_bank = 0;
    // 预擦除 Bank2
    HAL_FLASHEx_Erase(FLASH_TYPEERASE_MASS, BANK2_ADDR, 1, NULL);
}

// 启动 MDMA 搬运(非阻塞)
void Log_Write(uint32_t* data, uint32_t size) {
    // 配置 MDMA 传输长度(以字为单位)
    hmdma.Init.BufferTransferLength = size;
    HAL_MDMA_Init(&hmdma);
    
    // 启动 MDMA,源为 data,目的为 Flash 编程地址
    HAL_MDMA_Start(&hmdma, (uint32_t)data, write_addr, size);
    
    // 等待传输完成(实际应用中可用中断或轮询)
    HAL_MDMA_PollForTransfer(&hmdma, HAL_MDMA_FULL_TRANSFER, HAL_MAX_DELAY);
    
    // 写入完成后,切换 Bank 并更新地址
    write_addr = (current_bank == 0) ? BANK2_ADDR : BANK1_ADDR;
    Switch_Bank();
}

// 主函数示例
int main(void) {
    HAL_Init();
    SystemClock_Config();
    Log_Init();
    
    while (1) {
        // 模拟日志数据
        for (int i = 0; i < 1024; i++) {
            log_buffer[i] = i + HAL_GetTick();
        }
        Log_Write(log_buffer, 1024);
        HAL_Delay(1000); // 每秒写一次
    }
}

6. 注意事项

  • Flash 编程时序:MDMA 写入 Flash 时,Flash 控制器必须处于编程状态,且地址需按 32 位对齐。若数据未对齐,需先拷贝到对齐缓冲区(会损失零拷贝优势)。
  • 擦除与写入冲突:双 Bank 交错时,确保擦除操作在 Bank 切换前完成,否则可能写入失败。建议使用中断或 DMA 完成回调来触发切换。
  • 缓存一致性:如果日志缓冲区位于可缓存区域(如 SRAM),需在 MDMA 启动前执行 SCB_CleanDCache(),否则可能读到旧数据。
  • MDMA 错误处理:务必检查 MDMA 传输错误标志(如 HAL_MDMA_GetError),并在回调中恢复。
  • 性能优化:增大 MDMA 单次传输长度(如 8KB)可减少启动开销,但需权衡内存占用。

7. 总结

通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志数据的零拷贝并行搬运,CPU 负载几乎为零,且写入速度接近 Flash 最大编程速率。该方法适用于高频日志记录、数据采集等场景,是嵌入式高性能存储的典范。开发者可根据实际需求调整缓冲区大小和 Bank 数量,进一步优化系统。