/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
STM32H7双Bank Flash架构下的零停机OTA升级实战指南
在嵌入式系统开发中,固件升级是一个无法回避的核心需求。无论是工业网关、医疗设备还是消费电子产品,都需要在设备生命周期内进行功能迭代或漏洞修复。传统的固件升级方式往往需要设备停机、进入特殊的引导模式,这不仅影响用户体验,更在关键任务系统中可能造成严重后果。想象一下,一台正在执行精密操作的工业机器人,或者一台正在监护患者生命体征的医疗设备,如果因为固件升级而中断服务,其后果可能是灾难性的。
STM32H7系列微控制器凭借其独特的双Bank Flash架构,为这一难题提供了优雅的解决方案。这种架构允许我们在一个Bank中运行现有固件的同时,在另一个Bank中安全地写入新固件,实现真正的“零停机”在线升级。这不仅仅是技术上的优化,更是产品可靠性和用户体验的质的飞跃。本文将深入剖析这一架构,并提供一套完整的、经过实战检验的OTA升级方案,涵盖从底层原理到上层策略,从代码实现到异常处理的每一个细节。
1. 深入理解STM32H7的双Bank Flash架构
要充分利用双Bank架构实现零停机升级,首先必须透彻理解其硬件特性和工作原理。STM32H7的Flash存储器并非简单的线性存储空间,而是被精心设计为两个完全独立的物理存储区——Bank0和Bank1。每个Bank通常容量相等(例如各1MB),拥有独立的地址空间、控制寄存器和访问接口。
1.1 硬件架构的核心优势
双Bank设计的精妙之处在于其并行操作能力。当CPU从Bank0执行代码时,我们可以同时对Bank1进行擦除和编程操作,反之亦然。这种并行性是通过硬件实现的,两个Bank拥有独立的读写接口和状态机,彼此之间几乎没有干扰。
关键硬件特性:
- 独立操作:每个Bank可以独立执行擦除、编程和读取操作
- 并行处理:一个Bank执行代码时,另一个Bank可进行写操作
- 地址映射固定:Bank0起始地址为0x08000000,Bank1起始地址为0x08100000
- 页(Page)结构:每个Bank进一步划分为多个页(通常为128KB),支持页级擦除
注意:虽然两个Bank可以并行操作,但如果在同一个Bank内执行代码的同时对该Bank进行写操作,CPU会暂停执行直到写操作完成。这是理解零停机升级的关键——我们必须确保运行代码和写入新固件不在同一个Bank。
1.2 内存映射与启动配置
STM32H7的启动机制非常灵活,支持从多个地址启动。系统复位后,CPU会根据BOOT引脚状态或选项字节的设置,从特定地址开始执行代码。对于双Bank OTA升级,我们需要精心规划内存布局:
/* Flash Bank地址定义 */
#define FLASH_BANK0_BASE 0x08000000
#define FLASH_BANK1_BASE 0x08100000
#define FLASH_PAGE_SIZE 0x20000 /* 128KB */
/* 典型的双Bank布局规划 */
typedef struct {
uint32_t bootloader_addr; /* 引导程序地址 */
uint32_t app_bank0_addr; /* Bank0应用起始地址 */
uint32_t app_bank1_addr; /* Bank1应用起始地址 */
uint32_t config_data_addr; /* 配置数据存储区 */
uint32_t ota_flag_addr; /* OTA状态标志地址 */
} FlashLayout_t;
/* 实际布局实例 */
static const FlashLayout_t flash_layout = {
.bootloader_addr = FLASH_BANK0_BASE,
.app_bank0_addr = FLASH_BANK0_BASE + 0x10000, /* 跳过引导程序区域 */
.app_bank1_addr = FLASH_BANK1_BASE,
.config_data_addr = FLASH_BANK1_BASE + 0xF0000, /* Bank1末尾保留区域 */
.ota_flag_addr = FLASH_BANK1_BASE + 0xFF000 /* OTA标志存储位置 */
};
这种布局确保了引导程序、应用程序和配置数据都有明确的归属,避免了地址冲突和数据损坏的风险。
1.3 性能考量与访问时序
在实际项目中,我发现很多开发者忽略了Flash访问时序对系统性能的影响。STM32H7的Flash控制器支持不同的等待状态配置,这直接关系到CPU访问Flash的速度。特别是在高主频下,不合理的等待状态设置会导致性能下降甚至运行错误。
等待状态配置建议:
| ≤ 70 MHz | 0等待 | 70 MHz |
| ≤ 140 MHz | 1等待 | 140 MHz |
| ≤ 210 MHz | 2等待 | 210 MHz |
| ≤ 225 MHz | 3等待 | 225 MHz |
配置Flash等待状态的代码示例:
void FLASH_SetWaitState(uint32_t system_clock_mhz)
{
FLASH_AdvOBProgramInitTypeDef adv_config = {0};
if (system_clock_mhz <= 70) {
adv_config.WaitStates = FLASH_WAIT_STATE_0;
} else if (system_clock_mhz <= 140) {
adv_config.WaitStates = FLASH_WAIT_STATE_1;
} else if (system_clock_mhz <= 210) {
adv_config.WaitStates = FLASH_WAIT_STATE_2;
} else {
adv_config.WaitStates = FLASH_WAIT_STATE_3;
}
HAL_FLASHEx_AdvOBProgram(&adv_config);
}
2. 零停机OTA升级的系统设计
实现真正的零停机升级,需要一套完整的系统设计方案。这不仅仅是技术实现,更是工程思维的体现。我在多个工业级项目中验证了以下架构的可靠性。
2.1 三阶段升级流程
一个健壮的OTA系统应该包含三个阶段:准备阶段、传输阶段和切换阶段。每个阶段都有明确的任务和错误处理机制。
第一阶段:准备与验证 在这个阶段,系统需要完成以下工作:
typedef struct {
uint32_t magic_number; /* 魔数,用于识别结构有效性 */
uint32_t firmware_size; /* 固件大小 */
uint32_t firmware_crc; /* 固件CRC校验值 */
uint32_t version_major; /* 主版本号 */
uint32_t version_minor; /* 次版本号 */
uint32_t version_patch; /* 修订版本号 */
uint32_t target_bank; /* 目标Bank (0或1) */
uint32_t reserved[2]; /* 保留字段 */
} FirmwareMetadata_t;
/* 准备升级的函数实现 */
OTA_Status_t OTA_PrepareUpgrade(FirmwareMetadata_t *metadata)
{
OTA_Status_t status = OTA_STATUS_OK;
/* 验证元数据有效性 */
if (metadata->magic_number != FIRMWARE_MAGIC) {
return OTA_STATUS_INVALID_METADATA;
}
/* 检查目标Bank是否可用 */
uint32_t target_addr = (metadata->target_bank == 0) ?
flash_layout.app_bank0_addr :
flash_layout.app_bank1_addr;
/* 确保目标Bank不是当前运行Bank */
uint32_t current_pc = (uint32_t)__current_pc();
uint32_t current_bank = (current_pc >= FLASH_BANK0_BASE &&
current_pc < FLASH_BANK1_BASE) ? 0 : 1;
if (metadata->target_bank == current_bank) {
/* 需要先复制当前固件到另一个Bank */
status = OTA_CopyCurrentFirmware(1 – current_bank);
if (status != OTA_STATUS_OK) {
return status;
}
}
/* 擦除目标Bank(保留引导程序和配置区域) */
status = FLASH_EraseBank(metadata->target_bank,
FLASH_ERASE_RETAIN_BOOTLOADER);
/* 设置升级状态标志 */
if (status == OTA_STATUS_OK) {
OTA_SetStatus(OTA_STATUS_PREPARED);
}
return status;
}
第二阶段:数据传输与写入 这是最关键的阶段,需要处理数据接收、校验和写入。我推荐使用分块传输和写入的方式,每完成一个数据块就进行校验,避免一次性写入失败导致整个Bank损坏。
/* 分块写入的典型实现 */
OTA_Status_t OTA_WriteFirmwareBlock(uint32_t offset, uint8_t *data, uint32_t size)
{
HAL_StatusTypeDef hal_status;
uint32_t target_addr = g_ota_context.target_address + offset;
/* 解锁Flash */
HAL_FLASH_Unlock();
/* 以32字节为单位写入(STM32H7要求) */
for (uint32_t i = 0; i < size; i += 32) {
uint32_t write_size = (size – i) > 32 ? 32 : (size – i);
uint64_t write_data[4] = {0};
/* 准备32字节数据 */
memcpy(write_data, data + i, write_size);
/* 执行编程操作 */
hal_status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_FLASHWORD,
target_addr + i,
(uint32_t)write_data);
if (hal_status != HAL_OK) {
HAL_FLASH_Lock();
return OTA_STATUS_WRITE_ERROR;
}
/* 立即验证写入的数据 */
if (memcmp((void*)(target_addr + i), data + i, write_size) != 0) {
HAL_FLASH_Lock();
return OTA_STATUS_VERIFY_ERROR;
}
}
HAL_FLASH_Lock();
return OTA_STATUS_OK;
}
第三阶段:验证与切换 完成固件写入后,需要进行全面的验证,然后安全地切换到新固件。
2.2 Bank切换策略
Bank切换是整个升级过程中最 delicate 的环节。错误的切换时机或方式都可能导致系统崩溃。我总结了三种可靠的切换策略: