# STM32H7 零拷贝日志存储实战:MDMA 与双 Bank Flash 的深度配置 ## 1. 为什么需要零拷贝日志存储? 传统日志存储通常依赖 CPU 将日志数据从缓冲区拷贝到 Flash 写入缓冲区,再通过 Flash 编程接口写入。这种方式存在两大痛点: - **CPU 占用高**:每次拷贝和编程都占用 CPU 时间,影响实时任务。 - **阻塞风险**:Flash 编程期间 CPU 无法响应中断,可能导致系统卡顿。 STM32H7 的 MDMA(Master DMA)支持内存到内存、内存到外设的高速传输,且具备双 Bank Flash 的并行编程能力,使得日志数据可以直接从 RAM 缓冲区搬运到 Flash,无需 CPU 介入,实现真正的零拷贝。 ## 2. 硬件基础:MDMA 与双 Bank Flash ### 2.1 MDMA 关键特性 - **双通道传输**:支持 8/16/32 位数据宽度,可配置突发传输(Burst)。 - **链表模式**:支持多个传输描述符(LLI),实现连续传输。 - **中断触发**:传输完成或错误可触发中断,便于软件处理。 ### 2.2 双 Bank Flash 架构 STM32H7 的 Flash 分为 Bank1 和 Bank2,每个 Bank 可独立擦除和编程。通过配置 `FLASH_CR` 寄存器的 `DBANK` 位,可启用双 Bank 模式,允许同时擦写两个 Bank,显著提升写入吞吐量。 ## 3. 系统设计思路 目标:将日志数据从 RAM 缓冲区(如 `uint8_t log_buffer[1024]`)通过 MDMA 传输到 Flash 的指定区域,传输完成后自动擦除并写入下一块区域,全程无需 CPU 参与。 设计要点: - 使用 MDMA 的 LLI 链表,将日志数据分块传输到 Flash 编程数据寄存器(如 `FLASH_CR` 和 `FLASH_DR`)。 - 利用双 Bank 交替写入,避免擦除等待。 - 通过中断通知日志系统传输完成,更新写指针。 ## 4. 配置步骤 ### 4.1 使能 MDMA 和 Flash 时钟 ```c __HAL_RCC_MDMA_CLK_ENABLE(); __HAL_RCC_FLASH_CLK_ENABLE(); ``` ### 4.2 配置 Flash 双 Bank 模式 ```c FLASH_OBProgramInitTypeDef ob_init; HAL_FLASHEx_OBGetConfig(&ob_init); ob_init.Banks = FLASH_BANK_BOTH; ob_init.OptionType = OPTIONBYTE_WRP; // 启用双 Bank(需在解锁后设置 DBANK 位) FLASH_OB_Unlock(); FLASH_OB_EnableDualBank(); FLASH_OB_Lock(); // 注意:修改后需复位生效 ``` ### 4.3 配置 MDMA 传输 使用 STM32CubeMX 或直接代码配置 MDMA。以下为关键配置: ```c MDMA_HandleTypeDef hmdma; void MDMA_Config(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; // 目标地址递增 hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD; hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD; hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK; hmdma.Init.BufferTransferLength = 256; // 每次传输 256 字(1KB) HAL_MDMA_Init(&hmdma); // 配置 LLI 链表(用于连续传输多个块) MDMA_LLIInitTypeDef lli; lli.SrcAddress = (uint32_t)log_buffer; lli.DstAddress = (uint32_t)flash_write_addr; lli.NextLLI = (uint32_t)&lli_node[1]; lli.BufferTransferLength = 256; HAL_MDMA_ConfigLLI(&hmdma, &lli); } ``` ### 4.4 启动 MDMA 传输 ```c HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)flash_write_addr, 256); ``` ### 4.5 中断处理 在中断回调中更新日志写指针,并触发下一次传输(如果需要)。 ```c void HAL_MDMA_IRQHandler(MDMA_HandleTypeDef *hmdma) { if (hmdma->Instance == MDMA_Channel0) { // 传输完成,更新写指针 log_write_index += 1024; if (log_write_index >= LOG_BUFFER_SIZE) { log_write_index = 0; } // 可在此处触发下一次传输 } } ``` ## 5. 完整代码示例 以下是一个简化的日志存储模块,演示了 MDMA 传输和双 Bank 擦写配合: ```c #include "stm32h7xx_hal.h" #define LOG_BUFFER_SIZE 4096 #define FLASH_LOG_ADDR_BANK1 0x08020000 // 假设 Bank1 起始地址 #define FLASH_LOG_ADDR_BANK2 0x08060000 // Bank2 起始地址 uint8_t log_buffer[LOG_BUFFER_SIZE]; volatile uint32_t log_write_index = 0; MDMA_HandleTypeDef hmdma; void Flash_Erase_Bank(uint32_t bank) { FLASH_EraseInitTypeDef erase; uint32_t sector_error = 0; erase.TypeErase = FLASH_TYPEERASE_SECTORS; erase.Banks = bank; erase.Sector = 0; erase.NbSectors = 8; // 根据实际扇区数调整 erase.VoltageRange = FLASH_VOLTAGE_RANGE_3; HAL_FLASH_Unlock(); HAL_FLASHEx_Erase(&erase, §or_error); HAL_FLASH_Lock(); } void Log_Init(void) { // 初始化 MDMA MDMA_Config(); // 擦除两个 Bank 的日志区域 Flash_Erase_Bank(FLASH_BANK_1); Flash_Erase_Bank(FLASH_BANK_2); // 启动第一次传输 HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK1, LOG_BUFFER_SIZE/4); } void Log_Write(uint8_t *data, uint32_t len) { // 将数据拷贝到 log_buffer(此处为示例,实际可设计为环形缓冲区) memcpy(log_buffer + log_write_index, data, len); log_write_index += len; // 当缓冲区满时,启动 MDMA 传输(此处简化,实际需处理并发) if (log_write_index >= LOG_BUFFER_SIZE) { HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK1, LOG_BUFFER_SIZE/4); log_write_index = 0; } } void HAL_MDMA_IRQHandler(MDMA_HandleTypeDef *hmdma) { if (hmdma->Instance == MDMA_Channel0) { // 传输完成,可切换 Bank 或继续 static uint8_t bank_toggle = 0; if (bank_toggle == 0) { // 下次写入 Bank2 HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)FLASH_LOG_ADDR_BANK2, LOG_BUFFER_SIZE/4); bank_toggle = 1; } else { bank_toggle = 0; } } } ``` ## 6. 注意事项 - **Flash 编程时序**:MDMA 传输到 Flash 时,必须确保 Flash 已解锁,且编程地址对齐(通常为 32 位)。 - **双 Bank 擦除**:擦除操作会阻塞 CPU,建议在系统空闲时进行,或使用 Flash 的 RWW(Read-While-Write)特性。 - **MDMA 配置**:LLI 链表长度需与缓冲区大小匹配,否则可能传输不完整。 - **中断优先级**:MDMA 中断应设置为高优先级,避免日志丢失。 - **缓存一致性**:如果使用 D-Cache,需在传输前执行 `SCB_CleanDCache()`,确保数据从缓存写入 RAM。 ## 7. 总结 通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志存储的零拷贝,CPU 负载大幅降低,系统响应更加实时。本文提供的配置方法和代码可直接应用于实际项目,开发者可根据需求调整缓冲区大小和传输策略。希望这篇实战指南能帮助你在嵌入式开发中更高效地利用 STM32H7 的硬件资源。