STM32H7 双 Bank Flash 交替执行与 MDMA 零等待代码搬运:配置细节深度解析
👁 2 阅读 · 2026-08-27 · 嵌入式
在 STM32H7 高性能应用中,从 Flash 执行代码常受等待状态限制,影响实时性。本文深入探讨利用双 Bank Flash 的交替执行特性,结合 MDMA(存储器直接内存访问)实现代码零等待搬运的机制。文章详细解析了 Flash 双 Bank 架构、MDMA 配置流程、代码重映射技巧,并提供完整示例代码与关键注意事项,助力开发者突破 Flash 访问瓶颈,释放 H7 极致性能。
# STM32H7 双 Bank Flash 交替执行与 MDMA 零等待代码搬运:配置细节深度解析
## 1. 为什么需要零等待代码搬运?
STM32H7 系列主频高达 480MHz(甚至 550MHz),但内嵌 Flash 的访问速度受限于其物理特性,通常需要插入等待状态(如 7 个等待周期)。这意味着直接从 Flash 取指时,CPU 会频繁停顿,严重降低实时性。虽然 ART(自适应实时加速器)和缓存能缓解,但在确定性要求极高的场景(如电机控制、音频处理)中,仍可能出现抖动。
**解决方案**:将关键代码(如中断服务函数、实时算法)搬运到 RAM 中执行。但传统 DMA 搬运需要 CPU 介入,且搬运期间代码无法执行。利用 STM32H7 的双 Bank Flash 架构,我们可以实现**交替执行**:CPU 从 Bank1 执行代码,同时 MDMA 将 Bank2 中的新代码搬运至 RAM;完成后切换执行源,实现无缝更新。
## 2. 双 Bank Flash 架构基础
STM32H7 的 Flash 分为两个 Bank(Bank1 和 Bank2),每个 Bank 容量可达 1MB(部分型号)。关键特性:
- **独立读访问**:两个 Bank 可同时被访问(如 CPU 读 Bank1,MDMA 读 Bank2)。
- **编程/擦除并行**:对一个 Bank 进行写操作时,另一个 Bank 可正常读。
- **映射切换**:通过选项字节或寄存器,可将 Bank2 映射到地址 0x08000000,实现无缝切换。
**交替执行原理**:
1. 初始状态:CPU 从 Bank1(地址 0x08000000)执行代码。
2. MDMA 将 Bank2 中的新代码(地址 0x08100000)搬运至 RAM(如 AXI SRAM)。
3. 搬运完成后,设置一个标志,CPU 跳转到 RAM 中的代码执行(或通过函数指针调用)。
4. 同时,MDMA 可将 Bank1 中的另一段代码搬运至另一块 RAM,准备下一次切换。
这样,CPU 始终执行 RAM 中的“最新”代码,而 Flash 的等待状态被完全规避。
## 3. MDMA 配置关键点
MDMA(Master DMA)是 STM32H7 的高性能 DMA,支持 128 位数据传输、链接列表、4GB 地址空间。配置步骤:
### 3.1 时钟使能
```c
__HAL_RCC_MDMA_CLK_ENABLE();
```
### 3.2 配置 MDMA 通道
使用 HAL 库或 LL 库。以下为 HAL 示例:
```c
MDMA_HandleTypeDef hmdma;
void MDMA_Config(void) {
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_SW; // 软件触发,便于控制
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; // 32位
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = 0; // 稍后设置
hmdma.Init.SyncMode = MDMA_SYNC_FULL;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
// 注意:以上重复字段仅示意,实际只需设置一次。
// 简化配置:
hmdma.Init.Request = MDMA_REQUEST_SW;
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
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 = 0;
hmdma.Init.SyncMode = MDMA_SYNC_FULL;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
hmdma.Init.SyncPolarity = MDMA_SYNC_RISING;
hmdma.Init.SyncRequest = MDMA_SYNC_SW;
hmdma.Init.SyncSignals = MDMA_SYNC_SIGNAL_0;
hmdma.Init.SyncValue = 0;
hmdma.Init.SyncOrder = MDMA_SYNC_ORDER_0;
hmdma.Init.SyncEndianness = MDMA_SYNC_LITTLE_ENDIAN;
hmdma.Init.SyncDataSize = MDMA_SYNC_DATASIZE_WORD;
hmdma.Init.SyncInc = MDMA_SYNC_INC_OFF;
hmdma.Init.SyncAlignment = MDMA_SYNC_DATAALIGN_PACK;
hmdma.Init.SyncTransferMode = MDMA_SYNC_BUFFER_TRANSFER;
hmdma.Init.SyncBurst = MDMA_SYNC_BURST_1BEAT;
// 实际配置请精简,此处为示意。
HAL_MDMA_Init(&hmdma);
}
```
**注意**:MDMA 配置项较多,务必参考参考手册逐一核对。建议使用 CubeMX 生成基础配置,再手动调整。
### 3.3 启动传输
```c
void MDMA_StartTransfer(uint32_t src, uint32_t dst, uint32_t size) {
hmdma.Init.BufferTransferLength = size / 4; // 以字为单位
HAL_MDMA_ConfigTransfer(&hmdma, src, dst, size);
HAL_MDMA_Start(&hmdma, src, dst, size);
}
```
## 4. 双 Bank 交替执行实现步骤
### 4.1 链接脚本设置
将需要搬运的代码段放入独立区域,例如:
```c
/* 在链接脚本中定义 */
__RAM_EXEC_START = 0x24000000; /* AXI SRAM 起始地址 */
__RAM_EXEC_SIZE = 0x10000; /* 64KB */
SECTIONS {
.ram_exec : {
*(.ram_exec)
} > RAM_EXEC
}
```
在代码中,使用 `__attribute__((section(".ram_exec")))` 标记关键函数。
### 4.2 初始化流程
```c
void SystemInit_Exec(void) {
// 1. 使能 MDMA 时钟
__HAL_RCC_MDMA_CLK_ENABLE();
MDMA_Config();
// 2. 将 Bank2 中的代码搬运到 RAM
uint32_t src = 0x08100000; // Bank2 起始地址
uint32_t dst = (uint32_t)&__RAM_EXEC_START;
uint32_t size = (uint32_t)&__RAM_EXEC_SIZE;
MDMA_StartTransfer(src, dst, size);
// 3. 等待传输完成
while (HAL_MDMA_GetState(&hmdma) != HAL_MDMA_STATE_READY);
// 4. 跳转到 RAM 中的函数(例如 main_ram)
void (*ram_main)(void) = (void (*)(void))dst;
ram_main();
}
```
### 4.3 交替执行策略
- 使用两个 RAM 缓冲区(如 AXI SRAM 和 D2 SRAM),交替作为执行区。
- 当 CPU 执行缓冲区 A 时,MDMA 将下一段代码搬运到缓冲区 B;完成后切换。
- 通过中断或轮询标志实现同步。
## 5. 完整示例:搬运一个中断服务函数
```c
// 定义在 Flash Bank2 中的函数
__attribute__((section(".ram_exec"))) void ISR_Handler(void) {
// 实时处理代码
GPIOE->ODR ^= (1 << 1);
}
// 主程序
int main(void) {
HAL_Init();
SystemClock_Config();
// ... 初始化外设
// 搬运 ISR_Handler 到 RAM
extern uint32_t __ram_exec_start;
extern uint32_t __ram_exec_end;
uint32_t size = (uint32_t)&__ram_exec_end - (uint32_t)&__ram_exec_start;
MDMA_StartTransfer((uint32_t)ISR_Handler, (uint32_t)&__ram_exec_start, size);
while (HAL_MDMA_GetState(&hmdma) != HAL_MDMA_STATE_READY);
// 设置中断向量表指向 RAM(可选)
SCB->VTOR = (uint32_t)&__ram_exec_start;
// 使能中断,ISR 将从 RAM 执行
HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);
while (1) {
// 主循环
}
}
```
## 6. 注意事项与常见陷阱
- **缓存一致性**:MDMA 搬运后,需确保 CPU 缓存与 RAM 内容一致。使用 `SCB_CleanDCache()` 和 `SCB_InvalidateDCache()` 或配置 MPU 区域为非缓存。
- **对齐要求**:MDMA 要求源和目的地址按 32 位对齐,传输长度也需为 4 的倍数。
- **中断安全**:在搬运过程中,避免中断访问正在被写入的 RAM 区域,否则可能读到半更新数据。
- **Flash 编程冲突**:如果同时进行 Flash 编程(如写入日志),需确保 MDMA 读操作不与编程冲突,建议使用互斥机制。
- **链接脚本**:确保 `.ram_exec` 段正确放置,且起始地址在 RAM 区域内。
- **调试**:使用调试器时,注意断点可能影响时序,建议在发布版本中测试。
## 7. 总结
通过双 Bank Flash 交替执行与 MDMA 搬运,STM32H7 可以完全规避 Flash 等待状态,实现确定性的零等待代码执行。本文详细介绍了架构原理、MDMA 配置、实现步骤及注意事项,为高性能嵌入式应用提供了有力参考。实际项目中,可根据需求调整缓冲区大小和切换策略,以达到最佳性能。