ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GD32远程升级实战:IAP双工程Boot+App设计要点与避坑指南

GD32远程升级实战:IAP双工程Boot+App设计要点与避坑指南 简介面向 GD32 与 STM32 等 ARM Cortex-M 系列单片机的远程升级FOTA需求这套源码工程完整拆分了 IAP 引导程序与 App 应用程序两个独立工程开发者可基于它理解固件下载、完整性校验、闪存擦写、跳转和异常回滚等关键链路适合正在做 IoT 设备固件在线更新的中高级嵌入式工程师参考。资源包为 rar 压缩包共 771 个文件除核心的 C 源码与 H 头文件外还包含 uvprojx/uvoptx 工程文件、编译链接生成的 axf/hex/bin 映像文件以及 map/lst 等辅助分析文件在 17.35MB 包体内即可获得可直接打开、编译与烧录的完整工程。已有 4114 人学习下载IAP 与 App 两个工程分目录组织结构清晰便于对照阅读从固件存放地址规划、中断向量重映射、启动文件配置到升级失败后的恢复策略均有代码级呈现稍作适配即可迁移到 STM32 平台是一份实践性很强的 MCU 远程升级参考实现。 做嵌入式产品最怕的一件事就是设备已经发货到现场结果发现固件有bug或者客户要加新功能。这时候如果产品没有预留升级通道就只能派人带着烧录器跑现场拆壳、接线、烧录一来一回成本高得吓人。我手里这个GD32远程升级项目就是专门解决这个问题的。整个工程包含独立的IAP引导程序和应用程序两个工程底层逻辑和STM32完全同源玩过STM32的人可以直接参考移植。先说下这套方案的适用范围。如果你正在做GD32、STM32系列的MCU产品手头有串口、CAN、以太网或者无线模块想给设备加上远程固件升级能力那这套工程就是给你准备的。没接触过IAP的新手也能看我会把跳转原理、Flash分区、中断向量偏移这些关键点都拆开讲明白同时也会把那些坑——比如跳转后HAL_Delay卡死、找不到芯片目标这类问题——一次说透。1. 方案设计为什么一上来就要做BootApp双工程很多初学者问到远程升级第一反应是“直接在应用程序里接收固件然后写进Flash不就完了”这个思路成立但有个致命问题如果应用程序在升级过程中断电、跑飞或者写入的固件本身就是坏的那设备就彻底变砖了连补救的机会都没有。所以工业级、产品级的远程升级必须拆成两个独立工程也就是Boot和App分离。1.1 IAP到底解决了什么问题IAP的全称是In-Application Programming在应用编程说白了就是MCU在运行过程中自己给自己写Flash。这套机制的核心价值有两个一是让设备在不拆机的情况下更新固件二是让升级过程具备“容错”能力。Boot工程也就是IAP引导程序承担三个任务上电后检查是否有升级请求有就接收固件并写入App区没有升级请求就直接跳转到App运行同时负责在App异常时做兜底防止设备变砖。App工程就是正常的业务逻辑代码但它内部要预留一个触发条件比如收到串口命令、按键按下、云端指令等然后软复位进入Boot进入升级模式。这种设计放在产品上的真实场景是这样的设备在现场跑着服务器推送了一个版本更新包App收到后先校验包完整性然后写一个升级标志位到Flash参数区接着执行软复位。MCU重新上电后Boot先起来一查标志位发现有升级请求就进入接收模式通过串口或网络把新固件收下来、写完、校验通过、清标志位最后跳进新App。如果中途断电也没关系标志位还在下次上电Boot还会尝试继续升级。1.2 双工程的划分与Flash布局GD32和STM32一样内部Flash都是从0x08000000开始编址。双工程方案要把Flash切成几个区我的划分方式是所有同类产品通用的做法Flash分区地址范围存放内容Boot区0x08000000 - 0x0800FFFFIAP引导程序64KBApp区0x08010000 - 0x0807FFFF应用程序448KB参数区0x08080000 - 0x08080FFF升级标志、版本号等参数4KB这种分法兼顾了两个工程的空间余量。GD32F103系列的主频、Flash容量和STM32F103是兼容的512KB Flash的芯片这么划分很宽裕。如果用的是256KB或者128KB的芯片可以把Boot区压缩到32KBApp区相应缩小核心原则是Boot区要足够稳定、尽量少更新App区要容纳得下正常业务代码。1.3 为什么GD32工程可以给STM32参考GD32是国内厂商兆易创新的产品内核同样是Cortex-M3/M4外设寄存器层面和STM32高度兼容标准外设库的函数命名几乎一模一样。所以这套工程的架构、协议和跳转逻辑完全可以用在STM32上差别主要体现在三个地方一是Flash扇区大小不同GD32F103的扇区是4KB一页STM32F103是1KB一页擦写函数的地址对齐逻辑要改二是时钟树配置有差异GD32的主频可以跑到108MHz以上部分外设时钟源选择不同三是部分外设中断向量和寄存器偏移略有区别但IAP相关的Flash操作和中断控制部分几乎可以平级替换。2. 核心原理跳转背后不能踩的坑IAP最核心的环节就是Boot跳转到App的过程。很多人在这一步翻车根本原因是没理解Cortex-M3/M4的启动机制。我先把原理讲透后面再给代码。2.1 跳转流程向量表、MSP、Reset_HandlerCortex-M3内核上电后硬件会自动从地址0x08000000处取出初始栈指针MSP赋给SP寄存器从0x08000004处取出复位向量赋给PC寄存器然后跳过去执行。这意味着MCU的启动流程严格依赖向量表放在0x08000000处。所以Boot跳转App的时候至少要做三件事判断App的栈指针和复位向量是否合法这两个值必须在Flash地址范围内否则说明App区是空的或者固件损坏。把SP寄存器设成App的初始栈顶地址。把PC指向App的复位向量地址Reset_Handler。核心跳转代码大概长这样typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_msp *(volatile uint32_t *)app_addr; uint32_t app_reset *(volatile uint32_t *)(app_addr 4); pFunction jump_func; // 合法性检查 if ((app_msp 0xFFF00000) 0x20000000) { __disable_irq(); // 关闭全局中断 jump_func (pFunction)app_reset; __set_MSP(app_msp); // 设置主栈指针 jump_func(); // 跳转不再返回 } }这里有个容易被忽略的点__disable_irq()只关了全局中断但外设本身的中断使能位可能还是开着的。如果Boot里用了串口、定时器这些中断源仍然处于使能状态跳到App后一旦某个中断触发而App的中断处理函数还没初始化好就会直接进HardFault。所以正规做法是跳转前把用过的外设全部DeInit把中断挂起标志清干净。2.2 为什么跳转后HAL_Delay会卡死这个话题在搜索热词里出现了很多次原因非常典型。HAL_Delay函数依赖SysTick中断而SysTick是Boot代码里初始化并启动的。跳转到App后App的SystemInit和HAL_Init的确会重新配置SysTick但问题是跳转前Boot可能把SysTick关掉了或者App里的HAL_Init没有正确重新使能SysTick导致HAL_GetTick()永远返回0HAL_Delay就死循环了。这个问题的根源还有另一层跳转前__disable_irq()把PRIMASK置1如果跳转到App后没有重新使能全局中断整个系统就处于中断屏蔽状态SysTick永远无法触发HAL_Delay自然卡死。所以跳转函数执行完之后App的第一条代码路径上要尽早执行__enable_irq()或者在使用HAL_Delay之前确认中断已经打开。我在工程里的处理方式是跳转前不做__disable_irq()而是用更精细的中断控制先把Boot用到的串口中断、定时器中断单独关闭再调用__set_PRIMASK(0)确保中断打开状态可控同时在App的main函数最前面加上int main(void) { HAL_Init(); // 这里会配置SysTick __enable_irq(); // 确认全局中断打开 SystemClock_Config(); // 其他初始化... }这套组合拳打下来HAL_Delay卡死的问题基本不会再出现。2.3 中断向量表的偏移设置App的Flash地址从0x08010000开始但Cortex-M3内核的硬逻辑规定向量表默认在0x08000000。这就导致App运行起来后一旦有中断发生CPU还是从0x08000000处取中断向量取到的却是Boot的向量App的中断服务函数根本不会被执行。解决办法是通过VTOR寄存器向量表偏移寄存器把向量表重定位到App的起始地址。标准外设库和HAL库各有各的写法// 标准外设库 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x10000); // HAL库 SCB-VTOR 0x08010000;注意一点向量表要天然对齐到Flash页大小我的App起始地址是0x08010000偏移量0x10000是64KB满足Cortex-M3要求的2的幂次对齐。如果你把App放在0x08008000这种位置要确认对齐属性否则中断跳转依然会出问题。3. Bootloader工程实现细节Boot工程是整个升级系统的地基。它不负责具体业务只做三件事检查升级标志、接收固件、写入Flash。下面我按工程实际结构拆开讲。3.1 工程配置与Boot主流程Boot工程的构建和普通MCU工程一样没太多特殊之处但有几个配置必须注意启动文件用标准启动文件即可不需要修改启动文件内容。链接脚本把Flash起始地址设为0x08000000长度设成0x1000064KB。优化等级建议选-O2或者-OsBoot区要尽量精简给App区留空间。编译生成的烧录文件是.hex用烧录器直接烧到0x08000000就行。Boot的主流程就是一条路走到底的循环int main(void) { // 1. 初始化时钟、串口、Flash、看门狗如有 // 2. 读取参数区的升级标志 if (check_update_flag() UPDATE_REQUEST) { // 3. 进入升级模式串口/USB/CAN接收固件 receive_firmware_and_write_flash(); // 4. 校验App区固件完整性 if (verify_app_checksum() PASS) { clear_update_flag(); // 清升级标志 jump_to_app(APP_ADDR); } else { // 校验失败可能请求重传 } } else { // 5. 无升级请求直接跳转 jump_to_app(APP_ADDR); } }看门狗这里必须多说一句。如果Boot升级过程中喂狗策略设计不好会出现两种尴尬一种是不喂狗导致接收固件超时被复位进入升级和复位的死循环另一种是升级等待时一直喂狗导致代码卡死在Boot里无人发现。我的做法是只在串口每收到一包数据时喂狗如果接收超时超过5秒直接软复位回正常模式避免设备卡死在升级状态。这个设计在远程升级场景里极其重要因为我见过太多因为升级中断而一直停在Boot的现场设备。3.2 升级协议设计分帧传输与CRC校验远程升级的固件包不是一次性塞进串口的MCU的RAM装不下串口缓冲也容不下。所以固件要分包发送Boot一包一包地收一包一包地写入Flash。我用的协议帧格式如下帧头命令字包序号数据长度数据CRC320xAA 0x551字节2字节2字节N字节4字节命令字定义0x01表示开始升级附带固件总长度和CRC0x02表示固件数据包0x03表示升级完成0x04表示取消升级。包序号从0开始递增Boot收到后回ACK发送端没有收到ACK会重发当前包。CRC32覆盖从命令字到数据末尾的所有字节保证数据传输不出错。开始升级帧的数据结构是成员固件总长度4字节加固件整体CRC324字节Boot收到后先擦除App区。注意擦除Flash要花时间期间不能接收串口数据所以我推荐的做法是收到开始升级命令后先发一个ACK给上位机然后才执行Flash整片擦除。上位机收到ACK再开始发数据帧避免擦除期间数据丢失。3.3 Flash擦写实现与写保护处理GD32和STM32的Flash操作流程类似但有一个坑芯片出厂时Flash读保护RDP可能是使能的或者调试模式下Flash被锁定写入时会报错。我踩过这个坑后在Boot里加了一个解锁判断void flash_unlock_check(void) { if (FLASH-CR FLASH_CR_LOCK) { FLASH_Unlock(); // 解锁Flash控制寄存器 } }Flash写入的常见流程是这样的先判断目标地址是不是对齐到字4字节非对齐写入会触发硬件错误然后检查该地址所在的扇区是不是需要擦除Flash只能从1写0如果之前写过非0xFF的数据必须整页擦除后再写写完一个页后可以选择性地读回来校验。一个小技巧是Boot里每次写完一个扇区后对该扇区数据做CRC累积计算全部写完后和上位机发送的整体CRC做比对这样比逐个字节比对快很多代码也简单。GD32的每个扇区是4KBIAP数据包一般是512字节或1KB所以8包左右就写完一个扇区每次写满一个扇区就校验一次能尽早发现写入异常。4. APP工程改造把业务代码挪到新的起始地址Boot工程写好后App工程并不是简单编译一下就行它必须“知道”自己运行在0x08010000而不是默认的0x08000000。这部分一共改三个地方。4.1 修改链接脚本与工程选项在Keil或IAR工程里Options for Target - Target页面把IROM1的起始地址改成0x08010000大小改成0x70000448KB。如果用的是GCC工具链要修改链接脚本中的FLASH段起始地址MEMORY { FLASH (rx) : ORIGIN 0x08010000, LENGTH 448K RAM (xrw) : ORIGIN 0x20000000, LENGTH 64K }这里有一个常见的理解偏差很多人以为改了链接脚本就行了但实际上编译出的Hex文件起始地址仍然可能是0x08000000因为Keil会把Hex的启动地址和链路信息写入文件头部。烧录的时候一定要确认烧录算法里的基地址是0x08010000或者直接烧录bin文件而不是hex文件然后把bin文件的烧录起始地址设为0x08010000。这也是本工程里同时提供两个工程文件的另一个原因避免开发者弄混烧录地址把App烧到Boot区去。4.2 中断向量表重映射和系统初始化App启动流程里系统时钟初始化和向量表重映射的顺序是有讲究的。有的教程把SCB-VTOR放到main函数第一个执行有的放在SystemInit里这两种做法在GD32上都能跑但在STM32上有细微差别。HAL库的SystemInit函数会调用SystemCoreClockUpdate这个过程如果执行得太早而Flash等待周期还没设置好可能读取出错。我在App工程里是这样处理的void SystemInit(void) { // 使能FPU如果内核带FPU // 设置向量表偏移 SCB-VTOR APP_BASE_ADDR; // 0x08010000 }这个SystemInit是启动文件里在调用main之前就执行的所以向量表在App的main函数跑起来之前已经生效之后的中断就能正常路由到App的中断处理函数了。那么App里原来的中断函数需要改动吗不需要。中断处理函数名还是USART1_IRQHandler、TIM2_IRQHandler那些编译链接后这些函数被放在App区向量表里的入口地址也是App区的地址跳转过去后中断自然按新向量表执行。4.3 App如何触发进入升级模式远程升级的“远程”两个字核心就在这个触发机制上。无非两种常见形式一种是本地触发。App收到串口命令“upgrade”或者按键长按3秒先把升级标志写入参数区然后执行软复位NVIC_SystemReset()。参数区我放在Flash的最后一页这样即使用户反复升级也不会把参数区和代码区互相覆盖。写入标志位的具体实现要注意写入前必须先读一下该地址的数据如果是非0xFF要整页擦除再写否则写入不生效。另一种是网络触发。服务器下发升级指令App通过TCP/UDP/MQTT收到指令校验通过后把固件先缓存到外部Flash或者SD卡这也解释了为什么会有人搜GD32 FATFS就是干这个用的然后写标志位软复位。Boot上电后发现标志位为升级请求先从外部存储读固件写内部Flash。这种方式的优势是升级包可以在线续传网络断了等恢复后重传就行不会因为Boot区空间不足而写不下整个固件。5. 远程升级实测与问题排查工程写完不能烧进去跑一下就完事远程升级场景里各种现场问题层出不穷。我挑几个典型的排查过程分享一下。5.1 本地模拟远程升级完整流程我拿到这套工程后是这样验证的用串口把电脑和开发板连起来模拟远程服务器发送固件包。流程如下先把Boot固件通过烧录器烧进0x08000000。把App工程编译生成的bin文件通过上位机工具分包发送每包512字节帧格式按上面协议走。Boot收到开始升级命令后擦除App区发ACK。上位机逐包发送固件数据Boot边收边写Flash每写完一扇区回一个进度ACK。全部数据发完后上位机发升级完成命令Boot对App区做整体CRC校验。校验通过清升级标志跳转到AppApp的串口打印“App started”即说明升级成功。我这套流程实测下来一个128KB的App固件在115200波特率下大概2分钟左右传完包括擦除和写入时间。如果换成CAN或者网口速度还能快一个量级。5.2 常见问题速查表我在调试过程中遇到过的问题整理成一张表基本覆盖了新手会踩的大部分坑故障现象可能原因排查方法跳转后HAL_Delay卡死全局中断未打开/SysTick未启动检查跳转函数中__enable_irq()是否调用确认App的HAL_Init先执行跳转后进入HardFault外设中断未关闭、向量表偏移未设置跳转前DeInit所有外设检查SCB-VTOR是否在SystemInit里设置跳转后App串口打印乱码波特率时钟配置不一致检查App和Boot的时钟树配置是否相同APB分频是否一致Flash写入失败Flash未解锁、地址未对齐、扇区未擦除加入flash_unlock_check()检查写入地址4字节对齐先擦后写升级中途断电后无法再升级升级标志未清除或未重新置位确认标志位写入时机建议标志位放在独立扇区先擦后写保证原子性连接仿真器找不到目标Flash进入读保护、芯片进入低功耗检查RDP等级用STM32CubeProgrammer或J-Flash解除读保护Boot和App都正常但跳转后不执行App跳转地址错误或App区固件为空检查app_addr参数是否和链接脚本一致读0x08010000处的值确认栈指针合法这里单说“连接仿真器找不到目标”这个问题热搜词里也出现了error: no stm32 target found。很多情况下不是芯片坏了而是IAP程序一旦跑起来Flash被加上读保护调试接口就被锁定仿真器连接不上。解决办法是用STM32CubeProgrammer的Connect Under Reset模式连接电平拉低复位脚再点连接进去后再把读保护等级设置为0。GD32的ISP串口烧录模式也是同理BOOT0拉高再上电走ISP协议整片擦除。5.3 我从这套工程里总结的几条经验最后说几个实际项目里的心得。第一串口升级协议一定要做应答超时和重传机制但重传次数不能设成无限次否则网络模块异常时设备会一直卡在升级状态。我设的是单包重传3次3次失败就中止升级等下一次升级请求。第二App里建议加入软件版本号升级完成后Boot读App区里的版本号然后上报给服务器这样远程升级系统能自动确认是升级成功还是失败不用靠现场人员反馈。这个信息放到App的固定偏移位置就行比如0x080100000x200处放4字节版本号。第三如果你用的是GD32和STM32同为Cortex-M3的F103系列这套工程基本是平级替换的但如果是M4内核的GD32F407、STM32F407注意硬件中断向量表地址和FPU初始化代码略有差异跳转函数本身逻辑不变但系统初始化部分要按M4的启动文件来改。我一直觉得远程升级是嵌入式产品走向成熟的一道分水岭。没有升级能力的产品出厂就是最终形态有了这套IAP机制产品才真正具备了持续迭代、远程修复的能力。这套GD32双工程源码的核心设计思路不受具体芯片型号限制你拿着它去套STM32或者任何Cortex-M内核的单片机把Flash分区、链接脚本、向量表偏移这三样改对剩下的就是通信协议和业务逻辑的事情了。本文还有配套的精品资源点击获取
返回列表