# 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 个等待周期)。 ```c RCC->AHB3ENR |= RCC_AHB3ENR_MDMAEN; // 使能 MDMA 时钟 FLASH->ACR |= FLASH_ACR_LATENCY_7WS; // 设置等待周期 ``` ### 4.2 MDMA 配置 使用 HAL 库初始化 MDMA,配置为内存到内存模式,传输方向为内存到 Flash 编程地址。 ```c 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 必须处于编程模式,且地址对齐。 ```c void Flash_Init(void) { HAL_FLASH_Unlock(); // 设置编程宽度为 32 位(双字) FLASH->CR1 |= FLASH_CR1_PSIZE_1; // 32-bit programming } ``` ### 4.4 双 Bank 切换逻辑 定义两个 Bank 的基地址和大小,通过变量切换当前写入 Bank。 ```c #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。 ```c #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 数量,进一步优化系统。