引言

在嵌入式开发中,日志存储常因 Flash 擦写耗时而被诟病。STM32H7 系列(如 H743/H750)内置了 MDMA(Master DMA)和双 Bank Flash,通过巧妙设计,可以让数据搬运(内存→Flash)与 CPU 执行完全解耦,甚至实现双 Bank 交替写入,消除擦除等待。本文面向有 STM32 基础的开发者,深入讲解如何利用 MDMA 与双 Bank 实现零等待日志存储。

原理剖析

1. 为什么需要 MDMA?

传统 DMA 只能在外设与内存间搬运,而 MDMA 支持内存到内存、内存到外设,且具备 64 位带宽、4KB 突发传输和链表模式。在日志场景中,CPU 只需将日志数据写入 RAM 缓冲区,MDMA 自动将其搬运至 Flash 编程地址,CPU 可继续执行其他任务,实现“零等待”写入。

2. 双 Bank Flash 的优势

STM32H7 的 Flash 分为两个 Bank(Bank1 和 Bank2),每个 Bank 可独立擦除/编程。当 Bank1 正在擦除时,MDMA 可向 Bank2 写入数据;反之亦然。通过交替使用两个 Bank,擦除操作被隐藏,日志写入看似连续无延迟。

3. 并行搬运机制

  • MDMA 链表:将多个传输块链接,每个块对应一个 Bank 的写入任务。
  • 中断同步:MDMA 传输完成中断触发 Flash 编程,编程完成中断切换 Bank。
  • 双缓冲:RAM 中设置两个缓冲区,一个用于 CPU 写入日志,另一个用于 MDMA 读取搬运。

硬件与软件准备

  • 硬件:STM32H743 开发板(或 H750),外部晶振 25MHz。
  • 软件:STM32CubeIDE 1.13+,HAL 库。
  • 配置:启用 MDMA、Flash 双 Bank 模式(需注意 H7 的 Flash 选项字节)。

配置步骤

1. 使能 MDMA 时钟并配置

在 CubeMX 中,选择 MDMA,添加一个通道,配置如下:

  • 传输方向:内存到内存
  • 数据宽度:32 位(或 64 位)
  • 突发大小:4 或 8(提高效率)
  • 循环模式:禁用(使用链表)
  • 中断:传输完成中断使能

2. 配置 Flash 双 Bank 模式

H7 默认工作在单 Bank 模式。需通过选项字节将 Flash 配置为双 Bank 模式(DBANK=1)。在代码中可调用 HAL_FLASH_OB_Unlock()HAL_FLASH_OB_Launch() 实现,或直接在 CubeMX 中设置。

3. 编写 MDMA 链表配置

// 定义 MDMA 节点结构体
MDMA_LinkNodeTypeDef Node1, Node2;

void MDMA_Config(void)
{
    // 初始化 MDMA 句柄
    hmdma.Init.Request = MDMA_REQUEST_SW;
    hmdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
    hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS;
    hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
    hmdma.Init.DestInc = 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 = 1024; // 每次传输 1024 字
    hmdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
    hmdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
    hmdma.Init.SourceBlockAddressOffset = 0;
    hmdma.Init.DestBlockAddressOffset = 0;
    HAL_MDMA_Init(&hmdma);

    // 配置链表节点
    Node1.SourceAddress = (uint32_t)buffer1;
    Node1.DestAddress = FLASH_BANK1_BASE;
    Node1.BlockDataLength = 1024;
    Node1.NextNodeAddress = (uint32_t)&Node2;

    Node2.SourceAddress = (uint32_t)buffer2;
    Node2.DestAddress = FLASH_BANK2_BASE;
    Node2.BlockDataLength = 1024;
    Node2.NextNodeAddress = 0; // 链表结束

    // 将链表链接到 MDMA
    HAL_MDMA_LinkNode(&hmdma, &Node1);
}

4. Flash 编程与 Bank 切换

// 擦除 Bank1 并等待完成
void EraseBank1(void)
{
    FLASH_EraseInitTypeDef erase;
    uint32_t error = 0;
    erase.TypeErase = FLASH_TYPEERASE_SECTORS;
    erase.Banks = FLASH_BANK_1;
    erase.Sector = 0;
    erase.NbSectors = 8;
    erase.VoltageRange = FLASH_VOLTAGE_RANGE_3;
    HAL_FLASH_Unlock();
    HAL_FLASHEx_Erase(&erase, &error);
    HAL_FLASH_Lock();
}

// 编程函数(由 MDMA 中断调用)
void FlashProgram(uint32_t dest, uint32_t *src, uint32_t size)
{
    HAL_FLASH_Unlock();
    for (uint32_t i = 0; i < size; i++) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, dest + i*4, src[i]);
    }
    HAL_FLASH_Lock();
}

5. 主循环与中断处理

volatile uint8_t bank_ready = 0;

void MDMA_IRQHandler(void)
{
    HAL_MDMA_IRQHandler(&hmdma);
}

void HAL_MDMA_XferCpltCallback(MDMA_HandleTypeDef *hmdma)
{
    // 根据当前使用的 Bank 切换
    if (current_bank == 1) {
        FlashProgram(FLASH_BANK2_BASE, buffer2, 1024);
        EraseBank1(); // 擦除 Bank1 为下次使用
        current_bank = 2;
    } else {
        FlashProgram(FLASH_BANK1_BASE, buffer1, 1024);
        EraseBank2();
        current_bank = 1;
    }
    bank_ready = 1;
}

int main(void)
{
    // 初始化...
    MDMA_Config();
    EraseBank1();
    EraseBank2();
    while (1) {
        // 等待缓冲区满
        if (log_ready) {
            HAL_MDMA_Start_IT(&hmdma, (uint32_t)buffer1, FLASH_BANK1_BASE, 1024);
            log_ready = 0;
        }
    }
}

完整代码示例

限于篇幅,以上为核心代码。完整工程可在 GitHub 获取(示例链接)。关键点:

  • 使用双缓冲:buffer1buffer2 交替。
  • MDMA 链表自动切换源/目的地址。
  • 中断中执行 Flash 编程和 Bank 擦除。

注意事项

  • Flash 编程时间:H7 的 Flash 编程需要约 20-40us/字,MDMA 传输 1024 字约 10us,因此编程是瓶颈。务必确保 MDMA 传输完成后再编程,否则数据丢失。
  • Bank 擦除时间:擦除一个 Bank(1MB)约 1-2s,因此日志数据量不宜过大,建议每 Bank 存储 512KB 日志,交替使用。
  • 缓存一致性:MDMA 访问 RAM 时,需确保 CPU 缓存已刷新(SCB_CleanDCache()),否则可能读到脏数据。
  • 中断优先级:MDMA 中断应设为最高优先级,避免日志丢失。
  • 双 Bank 配置:修改选项字节后需复位生效,且注意备份用户代码。

总结

通过 MDMA 与双 Bank Flash 的配合,STM32H7 实现了日志存储的“零等待”写入,CPU 几乎零开销。本文提供的方案可直接应用于工业数据记录、故障分析等场景。实际项目中,可根据日志大小调整缓冲区与链块数量,进一步优化性能。希望本文能帮助你解锁 H7 的极致存储能力!