文章目录STM32H5 Bootloader与IAP升级机制深度解析一、概述二、Flash 内存布局2.1 物理分区2.2 关键地址宏定义2.3 两个独立的链接脚本三、FirmwareInfo固件信息结构体3.1 数据结构3.2 bEnterBootloader 标志的三态语义3.3 读写函数四、Bootloader 启动流程4.1 启动流程图4.2 isBootloader() —— 我是谁4.3 isNeedToUpdate() —— 需要升级吗4.4 main() 中的决策逻辑4.5 start_app() —— 汇编实现的跳转五、IAP 固件升级协议5.1 升级触发方式升级主控自身channel 05.2 固件传输协议Modbus Write File Record文件头record_no 0数据包record_no 15.3 Flash 擦除操作5.4 Flash 编程操作六、升级过程中的状态保护6.1 Flash 编程互斥6.2 升级期间暂停通道数据采集6.3 紧急命令的先回复后复位策略七、多级升级主控作为升级代理7.1 三级升级体系7.2 固件转发流程八、启动与升级的完整时序8.1 首次烧录空芯片8.2 正常运行中远程升级九、关键设计要点总结✅ 设计亮点⚠️ 注意事项与潜在改进十、总结STM32H5 Bootloader与IAP升级机制深度解析一、概述IAPIn-Application Programming即在应用编程是一种在系统运行时通过通信接口对用户程序进行擦除和重新编程的技术。本项目基于 STM32H563RIVx实现了一套完整的双区Bootloader 多级IAP升级框架不仅支持主控自身升级还能作为升级代理向下级传感器转发固件。项目详情芯片STM32H563RIVx (Cortex-M33)Flash 容量2MB (双Bank各1MB)Bootloader位于 Bank1 起始区域256KBApplication从 Bank1 中部起始可跨越到 Bank2最大约 1.75MB固件信息区位于 Bank2 最末尾 8KB0x081F_E000升级协议Modbus Write File Record功能码 0x15二、Flash 内存布局2.1 物理分区0x0800_0000 ┌──────────────────────────────────────┐ │ Bootloader │ │ (256KB) │ 0x0804_0000 ├──────────────────────────────────────┤ │ │ │ Application │ │ (起始于 Bank1, 可跨越到 Bank2) │ │ │ │ │ 0x081F_E000 ├──────────────────────────────────────┤ │ 固件信息配置区 FirmwareInfo (8KB) │ 0x0820_0000 └──────────────────────────────────────┘ Flash 总容量 2MB地址从 0x0800_0000 到 0x081F_FFFF 连续编址。 Bank1 0x0800_0000 ~ 0x080F_FFFF (1MB) Bank2 0x0810_0000 ~ 0x081F_FFFF (1MB) APP 链接脚本声明加载区域 0x08040000 2MB 可跨越 Bank1/Bank2 边界最大用到 0x081F_DFFF 固件信息区 CFG_OFFSET 0x081FE000 之前。2.2 关键地址宏定义#defineAPP_LOAD_ADDR0x08040000// APP 加载地址Bootloader 结束后#defineCFG_OFFSET0x081FE000// 固件信息结构体存储地址Bank2 末尾#defineSECTOR_SIZE(8*1024)// 扇区大小 8KB2.3 两个独立的链接脚本本项目的 Bootloader 和 Application 是两个独立的工程/镜像各有自己的链接脚本Bootloader 链接脚本加载地址 0x0800_0000LR_IROM1 0x08000000 0x00200000 { ER_IROM1 0x08000000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x000a0000 { .ANY (RW ZI) } }Application 链接脚本加载地址 0x0804_0000LR_IROM1 0x08040000 0x00200000 { ER_IROM1 0x08040000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x000a0000 { .ANY (RW ZI) } }两者的区别仅在于ER_IROM1的起始地址——Bootloader 从0x0800_0000开始APP 从0x0804_0000开始。三、FirmwareInfo固件信息结构体3.1 数据结构固件信息存储在 Flash 的固定地址CFG_OFFSET (0x081FE000)是 Bootloader 和 App 之间通信的桥梁typedefstructFirmwareInfo{uint32_tversion;// 固件版本号保留当前为0uint32_tfile_len;// 固件文件长度字节uint32_tload_addr;// 固件加载地址固定为 APP_LOAD_ADDRuint32_tcrc32;// CRC32 校验值保留当前为0未使用uint8_tfile_name[16];// 固件文件名uint32_tbEnterBootloader;// ★ 核心标志是否进入 Bootloader}FirmwareInfo,*PFirmwareInfo;3.2bEnterBootloader标志的三态语义值含义说明0xFFFF_FFFF未初始化/无固件Flash 擦除后的默认值表示从未烧写过固件1需要进入 Bootloader上位机要求进入升级模式下次启动停留在 Bootloader 等待固件0正常启动 APP固件已烧录完毕正常引导到 APP3.3 读写函数读取直接从 Flash 地址CFG_OFFSET读取如果内容全0xFF则返回失败staticintGetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo){volatilePFirmwareInfo ptFlashInfo(PFirmwareInfo)CFG_OFFSET;if(ptFlashInfo-file_len0xFFFFFFFF)// 从未烧写过return-1;*ptFirmwareInfo*ptFlashInfo;// 拷贝到内存return0;}写入先擦除固件信息区所在扇区8KB再逐 16 字节写入staticintWriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo){// 1. 解锁 Flash 控制器HAL_FLASH_Unlock();// 2. 擦除 CFG_OFFSET 所在扇区tEraseInit.BanksFLASH_BANK_2;tEraseInit.Sector(CFG_OFFSET-0x08000000-0x100000)/SECTOR_SIZE;tEraseInit.NbSectors1;HAL_FLASHEx_Erase(tEraseInit,SectorError);// 3. 逐 16 字节写入 FirmwareInfofor(inti0;isizeof(FirmwareInfo);i16)HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,flash_addr,(uint32_t)src_buf);HAL_FLASH_Lock();}四、Bootloader 启动流程4.1 启动流程图上电 / 复位 │ ▼ Cortex-M33 从 0x0800_0000 取向量表 → 执行 Reset_Handler │ ▼ __main() → 进入 main() │ ▼ ┌── main() 入口 ────────────────────┐ │ │ │ isBootloader()? │ │ (检查函数链接地址) │ │ │ │ │ 是 Bootloader │ │ │ │ │ ▼ │ │ isNeedToUpdate() │ │ │ │ │ ┌──┴──┐ │ │ │ │ │ │ 无需 需要 │ │ 升级 升级 │ │ │ │ │ │ ▼ ▼ │ │ 跳转 停留在 │ │ APP Bootloader │ │ (调 (外设初始化 │ │ 用 USB Modbus │ │ start 等待上位机 │ │ _app) 下发固件) │ │ │ └────────────────────────────────────┘4.2isBootloader()—— 我是谁这是一个非常巧妙的链接时自识别技术。通过判断当前函数自身的链接地址来决定运行的是 Bootloader 还是 APPintisBootloader(void){uint32_tlink_addr(uint32_t)isBootloader;if(link_addrAPP_LOAD_ADDR)// 0x08040000return1;// 链接地址 APP 起始地址 → 是 Bootloaderelsereturn0;// 链接地址 APP 起始地址 → 是 APP}原理Bootloader 镜像链接在0x0800_0000区域isBootloader函数地址 0x0804_0000APP 镜像链接在0x0804_0000区域isBootloader函数地址 0x0804_0000无需依赖任何外部标志仅通过函数本身的链接地址即可区分注意这个函数名的选取很讲究——如果它在Bootloader中被调用且链接地址0x08040000就返回1。该函数在两个镜像中同时存在但由于链接地址不同返回值不同。4.3isNeedToUpdate()—— 需要升级吗intisNeedToUpdate(void){FirmwareInfo tFirmwareInfo;if(GetLocalFirmwareInfo(tFirmwareInfo))// ① 没有固件信息Flash全FFreturn1;// → 需要升级if(tFirmwareInfo.bEnterBootloader1)// ② 标志明确要求进入Bootloaderreturn1;// → 需要升级if(tFirmwareInfo.bEnterBootloader0)// ③ 标志明确要求启动APPreturn0;// → 无需升级return1;// ④ 其他情况比如标志既不是0也不是1保守起见进入升级}4.4 main() 中的决策逻辑intmain(void){// 在 HAL 初始化之前执行if(isBootloader()!isNeedToUpdate()){// 情况当前是 Bootloader且无需升级// → 直接跳转到 APP不再执行 HAL_Init() 等初始化start_app(get_app_vector());}// 以下两种情况会执行到// ① 当前是 APPisBootloader() 返回 0// ② 当前是 Bootloader 且需要升级HAL_Init();SystemClock_Config();// ... 外设初始化 ...// ... 启动 FreeRTOS ...}为什么跳转放在 HAL_Init() 之前因为 APP 有自己的向量表和中断服务函数如果在 Bootloader 中初始化了外设和中断跳转到 APP 后中断状态不一致可能导致异常。所以发现无需升级就立即跳转不碰任何外设。4.5start_app()—— 汇编实现的跳转start_app PROC EXPORT start_app ; 参数 r0 APP 向量表基地址 (0x08040000) ; 1. 设置 VTOR向量表偏移寄存器 ; 告诉 CPU 中断向量表的新位置 LDR R1, 0xE000ED08 ; VTOR 寄存器地址 STR R0, [R1] ; VTOR 0x08040000 ; 2. 读取 APP 向量表的第一个字 → 主栈指针 MSP LDR R1, [R0] ; R1 *(0x08040000) 新的 MSP MOV SP, R1 ; 设置主栈指针 ; 3. 读取 APP 向量表的第二个字 → Reset_Handler 地址 LDR R1, [R0, #4] ; R1 *(0x08040004) Reset_Handler BX R1 ; 跳转到 APP 的 Reset_Handler ENDPARM Cortex-M 向量表规范向量表基地址 (VTOR) 0x00: 栈顶指针 (MSP) ← start_app 中 MOV SP, R1 0x04: Reset_Handler ← start_app 中 BX R1 0x08: NMI_Handler 0x0C: HardFault_Handler ...跳转到 APP 的Reset_Handler后APP 会重新执行完整的启动流程包括再次初始化 HAL、系统时钟、外设等相当于一次热启动。五、IAP 固件升级协议5.1 升级触发方式上位机通过 Modbus 协议发送Write Single Register请求向MODBUS_UPDATE_REG_ADDR寄存器地址 0写入私有命令写入值宏定义含义0x55MODBUS_PRIVATE_CMD_ENTER_BOOT进入 Bootloader准备升级0xAAMODBUS_PRIVATE_CMD_ENTER_APP启动 Application升级主控自身channel 0// process_emergency_cmd() 中if(valMODBUS_PRIVATE_CMD_ENTER_BOOT){modbus_reply(ctx,msg,msg_len,mb_mapping);// 先回复PCResetToBootloader();// 写 FirmwareInfo 并软复位}ResetToBootloader()的完整流程voidResetToBootloader(void){FirmwareInfo tFirmwareInfo;memset(tFirmwareInfo,0xff,sizeof(FirmwareInfo));GetLocalFirmwareInfo(tFirmwareInfo);// 读取当前固件信息tFirmwareInfo.bEnterBootloader1;// 设置需要进入BootloaderWriteFirmwareInfo(tFirmwareInfo);// 写入FlashSoftReset();// 软件复位}5.2 固件传输协议Modbus Write File Record固件数据通过Modbus Write File Record功能码0x15传输分为文件头和数据包两种类型。文件头record_no 0Modbus PDU: ┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐ │ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │ │ 0x15 │ 0x06 │ 0x0100 │ 0x0000 │ 0x00?? │ FileInfo 结构体 │ └──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘ ↑ ↑ file_no0x0100 record_no0 表示文件头 channel0(主控) file_type1(固件)burn_firmware()收到文件头后解析FileInfo获取文件名和文件总长度调用EraseFlash(APP_LOAD_ADDR, file_len)擦除目标区域调用EraseFlash(CFG_OFFSET, SECTOR_SIZE)擦除固件信息区重置接收计数器recv_len 0和flash_addr APP_LOAD_ADDR数据包record_no 1Modbus PDU: ┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐ │ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │ │ 0x15 │ 0x06 │ 0x0100 │ 0x0001 │ 0x00?? │ 固件数据块 (N字节) │ └──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘ ↑ record_no1,2,3...burn_firmware()收到数据包后voidburn_firmware(uint8_t*msg,uint16_tmsg_len){if(record_no0){// 文件头解析 FileInfo擦除 FlashEraseFlash(APP_LOAD_ADDR,tFileInfo.file_len);EraseFlash(CFG_OFFSET,SECTOR_SIZE);recv_len0;flash_addrAPP_LOAD_ADDR;}else{// 数据包写入 Flashcur_lenmsg[2]-7;// 计算有效数据长度recv_lencur_len;WriteFirmware(msg[10],cur_len,flash_addr);// 写入 Flashflash_addrcur_len;// 地址递增if(recv_lentFileInfo.file_len){// 固件接收完毕写入 FirmwareInfotFirmwareInfo.bEnterBootloader0;// 下次启动进入 APPtFirmwareInfo.file_lentFileInfo.file_len;tFirmwareInfo.load_addrAPP_LOAD_ADDR;strcpy((char*)tFirmwareInfo.file_name,tFileInfo.file_name);WriteFirmwareInfo(tFirmwareInfo);}}}5.3 Flash 擦除操作STM32H5 的 Flash 分为Bank1和Bank2各 1MB。擦除时需要考虑跨 Bank 的情况intEraseFlash(uint32_tflash_addr,uint32_tlen){uint32_tsectors(lenSECTOR_SIZE-1)/SECTOR_SIZE;uint32_tflash_offsetflash_addr-0x08000000;HAL_FLASH_Unlock();// 如果起始地址在 Bank1 范围内if(flash_offset0x100000){tEraseInit.BanksFLASH_BANK_1;tEraseInit.Sectorflash_offset/SECTOR_SIZE;bank_sectors(0x100000-flash_offset)/SECTOR_SIZE;erased_sectorsmin(sectors,bank_sectors);tEraseInit.NbSectorserased_sectors;HAL_FLASHEx_Erase(tEraseInit,SectorError);}// 如果还有剩余扇区要擦除进入 Bank2sectors-erased_sectors;if(sectors){tEraseInit.BanksFLASH_BANK_2;// ...HAL_FLASHEx_Erase(tEraseInit,SectorError);}HAL_FLASH_Lock();}5.4 Flash 编程操作写入时采用Quad Word16字节编程模式这是 STM32H5 支持的最快编程方式staticintWriteFirmware(uint8_t*firmware_buf,uint32_tlen,uint32_tflash_addr){HAL_FLASH_Unlock();len(len15)~15;// 向上对齐到16的倍数for(inti0;ilen;i16){HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,flash_addr,(uint32_t)firmware_buf);flash_addr16;firmware_buf16;}HAL_FLASH_Lock();}FLASH_TYPEPROGRAM_QUADWORD一次写入 16 字节相比单字4字节编程速度提升约 4 倍。六、升级过程中的状态保护6.1 Flash 编程互斥g_bFlashProgramming标志用于指示是否正在编程 Flashstaticintg_bFlashProgramming0;intisFlashProgramming(void){returng_bFlashProgramming;}在EraseFlash()和WriteFirmware()中该标志在操作开始时置 1操作结束后清 0。6.2 升级期间暂停通道数据采集当主控正在升级或向下转发固件时通道任务暂停对传感器数据采集// control.cstaticintg_bUpdating0;voidSetUpdateStatus(inton){g_bUpdatingon;}intisUpdating(void){returng_bUpdating;}在 CHn_Task 中while(1){if(!isUpdating())// 不在升级中 → 正常采集{loop_once(ctx,ptChannelInfo-channel,mb_mapping);}else// 正在升级 → 暂停等待{wait_cnt;if(wait_cnt1000)// 超时100秒强制退出升级状态SetUpdateStatus(0);}vTaskDelay(100);}6.3 紧急命令的先回复后复位策略当上位机写入0x55进入Bootloader时如果直接复位Modbus 回复帧就来不及发出上位机会收到超时错误。所以代码中if(valMODBUS_PRIVATE_CMD_ENTER_BOOT){modbus_reply(ctx,msg,msg_len,mb_mapping);// ★ 先回复ResetToBootloader();// 再复位}七、多级升级主控作为升级代理7.1 三级升级体系级别升级路径函数说明L1上位机 → 主控burn_firmware()升级主控自身固件L2上位机 → 主控 → 传感器send_firmware_to_device()主控转发固件给下级传感器L3上位机远程复位控制process_emergency_cmd()通过私有命令控制主控/传感器复位7.2 固件转发流程当file_no中channel ! 0时process_file_record()识别出这是需要转发的固件包staticintsend_firmware_to_device(uint8_t*msg,uint16_tmsg_len){file_no((uint16_t)msg[4]8)|msg[5];// 如 0x2103channel(file_no12)0xf;// channel 2 → 走 CH2dev_addr(file_no)0xff;// dev_addr 3 → 从机地址3file_no(file_no8)0xf;// file_no 1 → 固件数据ptChannelInfoget_channelinfo(channel);xSemaphoreTake(ptChannelInfo-xMutex,portMAX_DELAY);modbus_set_slave(ptChannelInfo-ctx,dev_addr);// 选中目标从机rcmodbus_write_file_record(ptChannelInfo-ctx,file_no,// 1:固件record_no,// 分包序号msg[10],len);// 数据xSemaphoreGive(ptChannelInfo-xMutex);}八、启动与升级的完整时序8.1 首次烧录空芯片上电 → Flash全FF → isBootloader()1(从0x08000000启动) → isNeedToUpdate()1(无FirmwareInfo) → 停留在Bootloader → 上位机通过USB连接 → 下发文件头(record_no0) → 擦除APP区配置区 → 下发固件数据包(record_no1~N) → 写入Flash → 固件接收完毕 → 写入FirmwareInfo(bEnterBootloader0) → 上位机写0x55 → ResetToBootloader() → 复位 → isBootloader()1 → isNeedToUpdate()0(bEnterBootloader0) → start_app(0x08040000) → 跳转到APP8.2 正常运行中远程升级APP 正常运行 → 上位机通过Modbus写寄存器0 0x55 → process_emergency_cmd()检测到紧急命令 → modbus_reply()先回复PC → ResetToBootloader() → 写FirmwareInfo(bEnterBootloader1) → SoftReset() → 复位 → isBootloader()1 → isNeedToUpdate()1(bEnterBootloader1) → 停留在Bootloader → 上位机下发新固件(同上) → 固件接收完毕 → FirmwareInfo(bEnterBootloader0) → 上位机写0xAA → ResetToApplication() → 写FirmwareInfo(bEnterBootloader0) → SoftReset() → 复位 → isBootloader()1 → isNeedToUpdate()0(bEnterBootloader0) → start_app(0x08040000) → 跳转新APP九、关键设计要点总结✅ 设计亮点设计说明双区独立镜像Bootloader 和 APP 各有一个链接脚本编译成两个独立的.axf/.hex文件链接自识别isBootloader()通过函数链接地址判断当前运行的是 BL 还是 APP无需外部标志提前跳转main()中在HAL_Init()之前就判断是否跳转避免外设状态冲突先回复后复位紧急命令先回 Modbus 应答再复位避免上位机超时多级升级主控不仅能升级自己还能向下转发固件给传感器状态保护升级期间暂停数据采集防止 Flash 操作与通信冲突⚠️ 注意事项与潜在改进问题现状改进建议CRC校验crc32字段保留但未使用固件接收完成后进行 CRC32 校验防止损坏的固件被写入固件备份无备份升级失败可能变砖保留一份回退固件在 Bank2 中升级失败自动回滚超时保护wait_cnt100秒超时较粗暴根据固件大小和波特率动态计算合理超时安全升级无加密/签名验证增加固件签名验证防止恶意固件被烧录十、总结本项目实现了一套工业级、生产可用的双区 IAP 升级方案。其核心设计可以概括为一个结构体 两个独立镜像 三个升级层级一个结构体FirmwareInfo存储在 Bank2 末尾作为 Bootloader 和 APP 之间的通信桥梁两个独立镜像Bootloader0x0800_0000和 APP0x0804_0000各有独立链接脚本三个升级层级主控自升级 → 转发传感器升级 → 远程复位控制这套框架适用于需要远程固件管理的工业物联网设备如数据采集网关、边缘计算节点、分布式传感器网络等场景。
STM32H5_Bootloader与IAP升级机制深度解析
文章目录STM32H5 Bootloader与IAP升级机制深度解析一、概述二、Flash 内存布局2.1 物理分区2.2 关键地址宏定义2.3 两个独立的链接脚本三、FirmwareInfo固件信息结构体3.1 数据结构3.2 bEnterBootloader 标志的三态语义3.3 读写函数四、Bootloader 启动流程4.1 启动流程图4.2 isBootloader() —— 我是谁4.3 isNeedToUpdate() —— 需要升级吗4.4 main() 中的决策逻辑4.5 start_app() —— 汇编实现的跳转五、IAP 固件升级协议5.1 升级触发方式升级主控自身channel 05.2 固件传输协议Modbus Write File Record文件头record_no 0数据包record_no 15.3 Flash 擦除操作5.4 Flash 编程操作六、升级过程中的状态保护6.1 Flash 编程互斥6.2 升级期间暂停通道数据采集6.3 紧急命令的先回复后复位策略七、多级升级主控作为升级代理7.1 三级升级体系7.2 固件转发流程八、启动与升级的完整时序8.1 首次烧录空芯片8.2 正常运行中远程升级九、关键设计要点总结✅ 设计亮点⚠️ 注意事项与潜在改进十、总结STM32H5 Bootloader与IAP升级机制深度解析一、概述IAPIn-Application Programming即在应用编程是一种在系统运行时通过通信接口对用户程序进行擦除和重新编程的技术。本项目基于 STM32H563RIVx实现了一套完整的双区Bootloader 多级IAP升级框架不仅支持主控自身升级还能作为升级代理向下级传感器转发固件。项目详情芯片STM32H563RIVx (Cortex-M33)Flash 容量2MB (双Bank各1MB)Bootloader位于 Bank1 起始区域256KBApplication从 Bank1 中部起始可跨越到 Bank2最大约 1.75MB固件信息区位于 Bank2 最末尾 8KB0x081F_E000升级协议Modbus Write File Record功能码 0x15二、Flash 内存布局2.1 物理分区0x0800_0000 ┌──────────────────────────────────────┐ │ Bootloader │ │ (256KB) │ 0x0804_0000 ├──────────────────────────────────────┤ │ │ │ Application │ │ (起始于 Bank1, 可跨越到 Bank2) │ │ │ │ │ 0x081F_E000 ├──────────────────────────────────────┤ │ 固件信息配置区 FirmwareInfo (8KB) │ 0x0820_0000 └──────────────────────────────────────┘ Flash 总容量 2MB地址从 0x0800_0000 到 0x081F_FFFF 连续编址。 Bank1 0x0800_0000 ~ 0x080F_FFFF (1MB) Bank2 0x0810_0000 ~ 0x081F_FFFF (1MB) APP 链接脚本声明加载区域 0x08040000 2MB 可跨越 Bank1/Bank2 边界最大用到 0x081F_DFFF 固件信息区 CFG_OFFSET 0x081FE000 之前。2.2 关键地址宏定义#defineAPP_LOAD_ADDR0x08040000// APP 加载地址Bootloader 结束后#defineCFG_OFFSET0x081FE000// 固件信息结构体存储地址Bank2 末尾#defineSECTOR_SIZE(8*1024)// 扇区大小 8KB2.3 两个独立的链接脚本本项目的 Bootloader 和 Application 是两个独立的工程/镜像各有自己的链接脚本Bootloader 链接脚本加载地址 0x0800_0000LR_IROM1 0x08000000 0x00200000 { ER_IROM1 0x08000000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x000a0000 { .ANY (RW ZI) } }Application 链接脚本加载地址 0x0804_0000LR_IROM1 0x08040000 0x00200000 { ER_IROM1 0x08040000 0x00200000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x000a0000 { .ANY (RW ZI) } }两者的区别仅在于ER_IROM1的起始地址——Bootloader 从0x0800_0000开始APP 从0x0804_0000开始。三、FirmwareInfo固件信息结构体3.1 数据结构固件信息存储在 Flash 的固定地址CFG_OFFSET (0x081FE000)是 Bootloader 和 App 之间通信的桥梁typedefstructFirmwareInfo{uint32_tversion;// 固件版本号保留当前为0uint32_tfile_len;// 固件文件长度字节uint32_tload_addr;// 固件加载地址固定为 APP_LOAD_ADDRuint32_tcrc32;// CRC32 校验值保留当前为0未使用uint8_tfile_name[16];// 固件文件名uint32_tbEnterBootloader;// ★ 核心标志是否进入 Bootloader}FirmwareInfo,*PFirmwareInfo;3.2bEnterBootloader标志的三态语义值含义说明0xFFFF_FFFF未初始化/无固件Flash 擦除后的默认值表示从未烧写过固件1需要进入 Bootloader上位机要求进入升级模式下次启动停留在 Bootloader 等待固件0正常启动 APP固件已烧录完毕正常引导到 APP3.3 读写函数读取直接从 Flash 地址CFG_OFFSET读取如果内容全0xFF则返回失败staticintGetLocalFirmwareInfo(PFirmwareInfo ptFirmwareInfo){volatilePFirmwareInfo ptFlashInfo(PFirmwareInfo)CFG_OFFSET;if(ptFlashInfo-file_len0xFFFFFFFF)// 从未烧写过return-1;*ptFirmwareInfo*ptFlashInfo;// 拷贝到内存return0;}写入先擦除固件信息区所在扇区8KB再逐 16 字节写入staticintWriteFirmwareInfo(PFirmwareInfo ptFirmwareInfo){// 1. 解锁 Flash 控制器HAL_FLASH_Unlock();// 2. 擦除 CFG_OFFSET 所在扇区tEraseInit.BanksFLASH_BANK_2;tEraseInit.Sector(CFG_OFFSET-0x08000000-0x100000)/SECTOR_SIZE;tEraseInit.NbSectors1;HAL_FLASHEx_Erase(tEraseInit,SectorError);// 3. 逐 16 字节写入 FirmwareInfofor(inti0;isizeof(FirmwareInfo);i16)HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,flash_addr,(uint32_t)src_buf);HAL_FLASH_Lock();}四、Bootloader 启动流程4.1 启动流程图上电 / 复位 │ ▼ Cortex-M33 从 0x0800_0000 取向量表 → 执行 Reset_Handler │ ▼ __main() → 进入 main() │ ▼ ┌── main() 入口 ────────────────────┐ │ │ │ isBootloader()? │ │ (检查函数链接地址) │ │ │ │ │ 是 Bootloader │ │ │ │ │ ▼ │ │ isNeedToUpdate() │ │ │ │ │ ┌──┴──┐ │ │ │ │ │ │ 无需 需要 │ │ 升级 升级 │ │ │ │ │ │ ▼ ▼ │ │ 跳转 停留在 │ │ APP Bootloader │ │ (调 (外设初始化 │ │ 用 USB Modbus │ │ start 等待上位机 │ │ _app) 下发固件) │ │ │ └────────────────────────────────────┘4.2isBootloader()—— 我是谁这是一个非常巧妙的链接时自识别技术。通过判断当前函数自身的链接地址来决定运行的是 Bootloader 还是 APPintisBootloader(void){uint32_tlink_addr(uint32_t)isBootloader;if(link_addrAPP_LOAD_ADDR)// 0x08040000return1;// 链接地址 APP 起始地址 → 是 Bootloaderelsereturn0;// 链接地址 APP 起始地址 → 是 APP}原理Bootloader 镜像链接在0x0800_0000区域isBootloader函数地址 0x0804_0000APP 镜像链接在0x0804_0000区域isBootloader函数地址 0x0804_0000无需依赖任何外部标志仅通过函数本身的链接地址即可区分注意这个函数名的选取很讲究——如果它在Bootloader中被调用且链接地址0x08040000就返回1。该函数在两个镜像中同时存在但由于链接地址不同返回值不同。4.3isNeedToUpdate()—— 需要升级吗intisNeedToUpdate(void){FirmwareInfo tFirmwareInfo;if(GetLocalFirmwareInfo(tFirmwareInfo))// ① 没有固件信息Flash全FFreturn1;// → 需要升级if(tFirmwareInfo.bEnterBootloader1)// ② 标志明确要求进入Bootloaderreturn1;// → 需要升级if(tFirmwareInfo.bEnterBootloader0)// ③ 标志明确要求启动APPreturn0;// → 无需升级return1;// ④ 其他情况比如标志既不是0也不是1保守起见进入升级}4.4 main() 中的决策逻辑intmain(void){// 在 HAL 初始化之前执行if(isBootloader()!isNeedToUpdate()){// 情况当前是 Bootloader且无需升级// → 直接跳转到 APP不再执行 HAL_Init() 等初始化start_app(get_app_vector());}// 以下两种情况会执行到// ① 当前是 APPisBootloader() 返回 0// ② 当前是 Bootloader 且需要升级HAL_Init();SystemClock_Config();// ... 外设初始化 ...// ... 启动 FreeRTOS ...}为什么跳转放在 HAL_Init() 之前因为 APP 有自己的向量表和中断服务函数如果在 Bootloader 中初始化了外设和中断跳转到 APP 后中断状态不一致可能导致异常。所以发现无需升级就立即跳转不碰任何外设。4.5start_app()—— 汇编实现的跳转start_app PROC EXPORT start_app ; 参数 r0 APP 向量表基地址 (0x08040000) ; 1. 设置 VTOR向量表偏移寄存器 ; 告诉 CPU 中断向量表的新位置 LDR R1, 0xE000ED08 ; VTOR 寄存器地址 STR R0, [R1] ; VTOR 0x08040000 ; 2. 读取 APP 向量表的第一个字 → 主栈指针 MSP LDR R1, [R0] ; R1 *(0x08040000) 新的 MSP MOV SP, R1 ; 设置主栈指针 ; 3. 读取 APP 向量表的第二个字 → Reset_Handler 地址 LDR R1, [R0, #4] ; R1 *(0x08040004) Reset_Handler BX R1 ; 跳转到 APP 的 Reset_Handler ENDPARM Cortex-M 向量表规范向量表基地址 (VTOR) 0x00: 栈顶指针 (MSP) ← start_app 中 MOV SP, R1 0x04: Reset_Handler ← start_app 中 BX R1 0x08: NMI_Handler 0x0C: HardFault_Handler ...跳转到 APP 的Reset_Handler后APP 会重新执行完整的启动流程包括再次初始化 HAL、系统时钟、外设等相当于一次热启动。五、IAP 固件升级协议5.1 升级触发方式上位机通过 Modbus 协议发送Write Single Register请求向MODBUS_UPDATE_REG_ADDR寄存器地址 0写入私有命令写入值宏定义含义0x55MODBUS_PRIVATE_CMD_ENTER_BOOT进入 Bootloader准备升级0xAAMODBUS_PRIVATE_CMD_ENTER_APP启动 Application升级主控自身channel 0// process_emergency_cmd() 中if(valMODBUS_PRIVATE_CMD_ENTER_BOOT){modbus_reply(ctx,msg,msg_len,mb_mapping);// 先回复PCResetToBootloader();// 写 FirmwareInfo 并软复位}ResetToBootloader()的完整流程voidResetToBootloader(void){FirmwareInfo tFirmwareInfo;memset(tFirmwareInfo,0xff,sizeof(FirmwareInfo));GetLocalFirmwareInfo(tFirmwareInfo);// 读取当前固件信息tFirmwareInfo.bEnterBootloader1;// 设置需要进入BootloaderWriteFirmwareInfo(tFirmwareInfo);// 写入FlashSoftReset();// 软件复位}5.2 固件传输协议Modbus Write File Record固件数据通过Modbus Write File Record功能码0x15传输分为文件头和数据包两种类型。文件头record_no 0Modbus PDU: ┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐ │ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │ │ 0x15 │ 0x06 │ 0x0100 │ 0x0000 │ 0x00?? │ FileInfo 结构体 │ └──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘ ↑ ↑ file_no0x0100 record_no0 表示文件头 channel0(主控) file_type1(固件)burn_firmware()收到文件头后解析FileInfo获取文件名和文件总长度调用EraseFlash(APP_LOAD_ADDR, file_len)擦除目标区域调用EraseFlash(CFG_OFFSET, SECTOR_SIZE)擦除固件信息区重置接收计数器recv_len 0和flash_addr APP_LOAD_ADDR数据包record_no 1Modbus PDU: ┌──────┬──────────┬──────────┬──────────┬──────────┬──────────────────────┐ │ FC │ RefType │ FileNo │ RecordNo │ Len │ Data │ │ 0x15 │ 0x06 │ 0x0100 │ 0x0001 │ 0x00?? │ 固件数据块 (N字节) │ └──────┴──────────┴──────────┴──────────┴──────────┴──────────────────────┘ ↑ record_no1,2,3...burn_firmware()收到数据包后voidburn_firmware(uint8_t*msg,uint16_tmsg_len){if(record_no0){// 文件头解析 FileInfo擦除 FlashEraseFlash(APP_LOAD_ADDR,tFileInfo.file_len);EraseFlash(CFG_OFFSET,SECTOR_SIZE);recv_len0;flash_addrAPP_LOAD_ADDR;}else{// 数据包写入 Flashcur_lenmsg[2]-7;// 计算有效数据长度recv_lencur_len;WriteFirmware(msg[10],cur_len,flash_addr);// 写入 Flashflash_addrcur_len;// 地址递增if(recv_lentFileInfo.file_len){// 固件接收完毕写入 FirmwareInfotFirmwareInfo.bEnterBootloader0;// 下次启动进入 APPtFirmwareInfo.file_lentFileInfo.file_len;tFirmwareInfo.load_addrAPP_LOAD_ADDR;strcpy((char*)tFirmwareInfo.file_name,tFileInfo.file_name);WriteFirmwareInfo(tFirmwareInfo);}}}5.3 Flash 擦除操作STM32H5 的 Flash 分为Bank1和Bank2各 1MB。擦除时需要考虑跨 Bank 的情况intEraseFlash(uint32_tflash_addr,uint32_tlen){uint32_tsectors(lenSECTOR_SIZE-1)/SECTOR_SIZE;uint32_tflash_offsetflash_addr-0x08000000;HAL_FLASH_Unlock();// 如果起始地址在 Bank1 范围内if(flash_offset0x100000){tEraseInit.BanksFLASH_BANK_1;tEraseInit.Sectorflash_offset/SECTOR_SIZE;bank_sectors(0x100000-flash_offset)/SECTOR_SIZE;erased_sectorsmin(sectors,bank_sectors);tEraseInit.NbSectorserased_sectors;HAL_FLASHEx_Erase(tEraseInit,SectorError);}// 如果还有剩余扇区要擦除进入 Bank2sectors-erased_sectors;if(sectors){tEraseInit.BanksFLASH_BANK_2;// ...HAL_FLASHEx_Erase(tEraseInit,SectorError);}HAL_FLASH_Lock();}5.4 Flash 编程操作写入时采用Quad Word16字节编程模式这是 STM32H5 支持的最快编程方式staticintWriteFirmware(uint8_t*firmware_buf,uint32_tlen,uint32_tflash_addr){HAL_FLASH_Unlock();len(len15)~15;// 向上对齐到16的倍数for(inti0;ilen;i16){HAL_FLASH_Program(FLASH_TYPEPROGRAM_QUADWORD,flash_addr,(uint32_t)firmware_buf);flash_addr16;firmware_buf16;}HAL_FLASH_Lock();}FLASH_TYPEPROGRAM_QUADWORD一次写入 16 字节相比单字4字节编程速度提升约 4 倍。六、升级过程中的状态保护6.1 Flash 编程互斥g_bFlashProgramming标志用于指示是否正在编程 Flashstaticintg_bFlashProgramming0;intisFlashProgramming(void){returng_bFlashProgramming;}在EraseFlash()和WriteFirmware()中该标志在操作开始时置 1操作结束后清 0。6.2 升级期间暂停通道数据采集当主控正在升级或向下转发固件时通道任务暂停对传感器数据采集// control.cstaticintg_bUpdating0;voidSetUpdateStatus(inton){g_bUpdatingon;}intisUpdating(void){returng_bUpdating;}在 CHn_Task 中while(1){if(!isUpdating())// 不在升级中 → 正常采集{loop_once(ctx,ptChannelInfo-channel,mb_mapping);}else// 正在升级 → 暂停等待{wait_cnt;if(wait_cnt1000)// 超时100秒强制退出升级状态SetUpdateStatus(0);}vTaskDelay(100);}6.3 紧急命令的先回复后复位策略当上位机写入0x55进入Bootloader时如果直接复位Modbus 回复帧就来不及发出上位机会收到超时错误。所以代码中if(valMODBUS_PRIVATE_CMD_ENTER_BOOT){modbus_reply(ctx,msg,msg_len,mb_mapping);// ★ 先回复ResetToBootloader();// 再复位}七、多级升级主控作为升级代理7.1 三级升级体系级别升级路径函数说明L1上位机 → 主控burn_firmware()升级主控自身固件L2上位机 → 主控 → 传感器send_firmware_to_device()主控转发固件给下级传感器L3上位机远程复位控制process_emergency_cmd()通过私有命令控制主控/传感器复位7.2 固件转发流程当file_no中channel ! 0时process_file_record()识别出这是需要转发的固件包staticintsend_firmware_to_device(uint8_t*msg,uint16_tmsg_len){file_no((uint16_t)msg[4]8)|msg[5];// 如 0x2103channel(file_no12)0xf;// channel 2 → 走 CH2dev_addr(file_no)0xff;// dev_addr 3 → 从机地址3file_no(file_no8)0xf;// file_no 1 → 固件数据ptChannelInfoget_channelinfo(channel);xSemaphoreTake(ptChannelInfo-xMutex,portMAX_DELAY);modbus_set_slave(ptChannelInfo-ctx,dev_addr);// 选中目标从机rcmodbus_write_file_record(ptChannelInfo-ctx,file_no,// 1:固件record_no,// 分包序号msg[10],len);// 数据xSemaphoreGive(ptChannelInfo-xMutex);}八、启动与升级的完整时序8.1 首次烧录空芯片上电 → Flash全FF → isBootloader()1(从0x08000000启动) → isNeedToUpdate()1(无FirmwareInfo) → 停留在Bootloader → 上位机通过USB连接 → 下发文件头(record_no0) → 擦除APP区配置区 → 下发固件数据包(record_no1~N) → 写入Flash → 固件接收完毕 → 写入FirmwareInfo(bEnterBootloader0) → 上位机写0x55 → ResetToBootloader() → 复位 → isBootloader()1 → isNeedToUpdate()0(bEnterBootloader0) → start_app(0x08040000) → 跳转到APP8.2 正常运行中远程升级APP 正常运行 → 上位机通过Modbus写寄存器0 0x55 → process_emergency_cmd()检测到紧急命令 → modbus_reply()先回复PC → ResetToBootloader() → 写FirmwareInfo(bEnterBootloader1) → SoftReset() → 复位 → isBootloader()1 → isNeedToUpdate()1(bEnterBootloader1) → 停留在Bootloader → 上位机下发新固件(同上) → 固件接收完毕 → FirmwareInfo(bEnterBootloader0) → 上位机写0xAA → ResetToApplication() → 写FirmwareInfo(bEnterBootloader0) → SoftReset() → 复位 → isBootloader()1 → isNeedToUpdate()0(bEnterBootloader0) → start_app(0x08040000) → 跳转新APP九、关键设计要点总结✅ 设计亮点设计说明双区独立镜像Bootloader 和 APP 各有一个链接脚本编译成两个独立的.axf/.hex文件链接自识别isBootloader()通过函数链接地址判断当前运行的是 BL 还是 APP无需外部标志提前跳转main()中在HAL_Init()之前就判断是否跳转避免外设状态冲突先回复后复位紧急命令先回 Modbus 应答再复位避免上位机超时多级升级主控不仅能升级自己还能向下转发固件给传感器状态保护升级期间暂停数据采集防止 Flash 操作与通信冲突⚠️ 注意事项与潜在改进问题现状改进建议CRC校验crc32字段保留但未使用固件接收完成后进行 CRC32 校验防止损坏的固件被写入固件备份无备份升级失败可能变砖保留一份回退固件在 Bank2 中升级失败自动回滚超时保护wait_cnt100秒超时较粗暴根据固件大小和波特率动态计算合理超时安全升级无加密/签名验证增加固件签名验证防止恶意固件被烧录十、总结本项目实现了一套工业级、生产可用的双区 IAP 升级方案。其核心设计可以概括为一个结构体 两个独立镜像 三个升级层级一个结构体FirmwareInfo存储在 Bank2 末尾作为 Bootloader 和 APP 之间的通信桥梁两个独立镜像Bootloader0x0800_0000和 APP0x0804_0000各有独立链接脚本三个升级层级主控自升级 → 转发传感器升级 → 远程复位控制这套框架适用于需要远程固件管理的工业物联网设备如数据采集网关、边缘计算节点、分布式传感器网络等场景。