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_CRFLASH_DR)。
  • 利用双 Bank 交替写入,避免擦除等待。
  • 通过中断通知日志系统传输完成,更新写指针。

4. 配置步骤

4.1 使能 MDMA 和 Flash 时钟

__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_FLASH_CLK_ENABLE();

4.2 配置 Flash 双 Bank 模式

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。以下为关键配置:

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 传输

HAL_MDMA_Start_IT(&hmdma, (uint32_t)log_buffer, (uint32_t)flash_write_addr, 256);

4.5 中断处理

在中断回调中更新日志写指针,并触发下一次传输(如果需要)。

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 擦写配合:

#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, &sector_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 的硬件资源。