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,实现无缝切换。
交替执行原理:
- 初始状态:CPU 从 Bank1(地址 0x08000000)执行代码。
- MDMA 将 Bank2 中的新代码(地址 0x08100000)搬运至 RAM(如 AXI SRAM)。
- 搬运完成后,设置一个标志,CPU 跳转到 RAM 中的代码执行(或通过函数指针调用)。
- 同时,MDMA 可将 Bank1 中的另一段代码搬运至另一块 RAM,准备下一次切换。
这样,CPU 始终执行 RAM 中的“最新”代码,而 Flash 的等待状态被完全规避。
3. MDMA 配置关键点
MDMA(Master DMA)是 STM32H7 的高性能 DMA,支持 128 位数据传输、链接列表、4GB 地址空间。配置步骤:
3.1 时钟使能
__HAL_RCC_MDMA_CLK_ENABLE();
3.2 配置 MDMA 通道
使用 HAL 库或 LL 库。以下为 HAL 示例:
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 启动传输
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 链接脚本设置
将需要搬运的代码段放入独立区域,例如:
/* 在链接脚本中定义 */
__RAM_EXEC_START = 0x24000000; /* AXI SRAM 起始地址 */
__RAM_EXEC_SIZE = 0x10000; /* 64KB */
SECTIONS {
.ram_exec : {
*(.ram_exec)
} > RAM_EXEC
}
在代码中,使用 __attribute__((section(".ram_exec"))) 标记关键函数。
4.2 初始化流程
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. 完整示例:搬运一个中断服务函数
// 定义在 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 配置、实现步骤及注意事项,为高性能嵌入式应用提供了有力参考。实际项目中,可根据需求调整缓冲区大小和切换策略,以达到最佳性能。