GD32远程升级IAP全解析:双工程、Flash分区与跳转避坑 简介这套源码是一份面向GD32与STM32 MCU开发者的远程升级FOTA参考实现包含独立IAP引导工程与App应用工程适合需要快速为物联网设备增加固件无线更新能力的嵌入式开发人员参考。压缩包共771个文件大小17.35MB主要包含216个h头文件、182个C源文件、100个目标文件和99个交叉引用文件以及hex/bin固件、axf调试文件、uvprojx工程配置等类型可覆盖源码阅读、编译构建与烧录验证的完整链路。源码中IAP部分涵盖新固件接收、完整性校验、Flash擦写与跳转逻辑App部分即正常运行的应用工程二者配合可实现远程升级的整体闭环。目前已有4130人学习对于正在规划Bootloader方案或调试升级失败问题的开发者可直接参考双工程目录划分、启动代码和升级流程设计节省前期预研时间。1. GD32远程升级到底在做什么IAP不是把代码复制过去就完事设备部署在无人值守的机房里程序出了bug要更新跑现场刷一根串口线来回交通加人工成本可能抵得上设备本身的价钱。GD32远程升级要解决的就是这件事让固件通过通信链路直接写入Flash不拆机、不断电、不留人。源码里包含IAP和APP两个工程IAP负责接收固件、擦写Flash、跳转启动APP才是真正的业务程序。STM32开发者看这份工程时只要把GD32的Flash操作接口换成自己芯片的库函数升级框架可以直接平移。适合做物联网网关、工业控制器、车载设备、电力采集终端的嵌入式开发者尤其是那些设备已经部署出去、售后成本压不住的场景。2. 拆解两个工程IAP与APP的分工和Flash分区规划拿到工程先别急着编译第一件事是理解两个工程为什么必须分开。IAP工程编译出来是一个小程序放在Flash的最起始地址上电先跑它APP工程编译出来是真正的业务固件放在Flash的偏移地址由IAP在合适的时机跳转过去。两个工程在同一个MCU里共存靠的是各自独立的编译地址和中断向量表。2.1 编译地址怎么定IROM1偏移是双工程的命根子Cortex-M3内核上电时从地址0x08000000读取前4个字节作为主栈指针再读后4个字节作为复位向量然后开始执行。IAP工程必须躺在0x08000000这样上电才能进入IAP。APP工程必须偏移比如放在0x08008000IAP跳转时就跳到这个地址。在Keil里IAP和APP两个工程的Options对话框Target标签页的IROM1起始地址分别配置成0x08000000和0x08008000大小按Flash容量分配。IAP的RAM起点建议不动APP的RAM同样不动避免栈指针初始化错乱。如果在IAR里改的是链接脚本里ROM区域的origin和length原理一样。编译地址配错的表现很隐蔽单步调试IAP看不出问题但只要跳转执行就HardFault。因为APP编译地址如果仍然是0x08000000IAP跳过去执行的其实是IAP自己逻辑全乱。我一般会在两个工程的链接器输出文件里检查一条关键项APP的__initial_sp和Reset_Handler两个符号的地址必须落在APP分区范围内否则编译地址一定没改对。2.2 Flash分区四段划分与大小估算远程升级不只是把Flash劈成IAP和APP两半至少要四个分区。以256KB Flash的GD32中等容量型号为例分区起始地址大小用途IAP区0x0800000032KB升级引导程序包含串口驱动、Flash驱动、CRC校验、状态机APP区0x0800800096KB业务应用程序正常运行入口升级缓存区0x0802000096KB远程固件先整体传到此处校验通过后再搬运到APP区参数区0x080380004KB保存升级标志、固件长度、CRC值、版本号、升级状态缓存区是关键设计。直接往APP区写固件写了一半掉电设备就变砖了先写缓存区APP区永远是上一次完整的固件断电、断连、校验失败都不会破坏当前可运行的程序。参数区可以利用Flash最后的一个扇区写入次数受Flash擦写寿命限制但存储升级状态这种低频小数据完全够用。大小估算上IAP编译出来一般10~20KB给32KB冗余充足APP区根据业务代码规模定功耗表和算法库多的给128KB也不夸张缓存区最小不得小于APP区大小否则大固件装不下。2.3 为什么STM32可以直接参考这份GD32工程GD32和STM32在Cortex-M3这个内核层面几乎一致向量表机制、中断控制器、Flash写入流程、启动方式全部相同。GD32的固件库和STM32标准外设库的API命名风格接近比如Flash解锁、擦除、编程、读回校验函数名不同但参数和调用顺序大同小异。把GD32的flash_program_half_word换成STM32的FLASH_ProgramHalfWord把gd32_flash_erase换成FLASH_ErasePage整个工程就能编译过。跳转函数、中断向量表偏移、升级状态机这些逻辑代码一行都不用改。真正的差异在Flash的页大小和擦除方式GD32有的型号按页擦除有的按扇区擦除STM32则是按页擦除页大小从1KB到2KB不等移植时把擦除粒度对齐自己的芯片就行。3. 远程升级的传输与协议层分包、重传和CRC一个都不能少远程升级链路不神秘说白了就是IAP通过串口、以太网或者无线模块接收固件字节流。真正要设计的是通信协议固件可能几十KB环境可能丢包设备可能随时断电协议层必须保证数据完整到达并且能卡住错误包。3.1 通信通道选型UART透传最稳以太网和CAN按场景来最常见的做法是MCU通过UART接一个4G模组或WiFi透传模块上位机在云端下发固件模组把字节流原封不动地吐给MCU的串口。这种方案IAP只跟UART打交道传输可靠性由链路模组负责代码最简单。工业现场如果走CAN总线IAP就换成CAN接收中断帧格式从串口字节流换成CAN报文逻辑不变。以太网场景更直接IAP里集成一个轻量级TCP客户端主动拉取固件。通道选择的基本原则是IAP里的通信协议要和业务通信完全独立升级不依赖APP里的任何协议栈。因为APP可能已经跑飞了或者新固件本身有问题IAP必须自带一套可用的通信能力。3.2 帧格式设计帧头、命令字、包序号、CRC一个都不能少远程升级的帧格式并不需要花哨但字段必须齐全。常见设计是一个变长帧数据域最长256字节帧头和帧尾用固定字节对齐typedef struct { uint8_t head[2]; // 0xAA 0x55用于帧同步 uint8_t cmd; // 0x01握手 0x02数据 0x03结束 0x04取消 uint16_t seq; // 包序号从1递增 uint16_t len; // data有效长度 uint16_t crc; // CRC16覆盖cmd到data末尾 uint8_t data[256]; // 固件数据或握手参数 } upgrade_frame_t;帧头固定两个字节配合状态机做同步收到0xAA且下一个字节是0x55才认为帧起始。包序号用来检测重复包和乱序重传机制靠它对齐。CRC16覆盖整个帧的有效区数据在传输中哪怕变一个bit也能被发现。上位机和设备之间的交互逻辑是设备收到合法包后立即回一个ACK帧ACK里带收到的seq和CRC校验结果上位机发完一包后等待ACK超时重发同一包连续重发三次仍无回应就终止升级并上报。3.3 解析状态机与重传策略先传到缓存区校验通过再写APPIAP里接收端应该是一个逐字节驱动的状态机不是一次性接收完整帧。串口中断每次来一个字节状态机根据当前状态决定是找帧头、收数据还是校验CRC。这样可以保证帧长度任意、串口缓冲区再小也不会漏数据。typedef enum { FRAME_WAIT_AA, FRAME_WAIT_55, FRAME_WAIT_CMD, FRAME_WAIT_SEQ_L, FRAME_WAIT_SEQ_H, FRAME_WAIT_LEN_L, FRAME_WAIT_LEN_H, FRAME_WAIT_CRC_L, FRAME_WAIT_CRC_H, FRAME_RX_DATA } frame_state_t; uint8_t rx_buf[263]; uint16_t rx_index; uint16_t rx_crc; frame_state_t fs FRAME_WAIT_AA; void uart_isr_rx(uint8_t byte) { switch (fs) { case FRAME_WAIT_AA: if (byte 0xAA) { fs FRAME_WAIT_55; rx_crc 0; } break; case FRAME_WAIT_55: if (byte 0x55) { fs FRAME_WAIT_CMD; } else { fs FRAME_WAIT_AA; } break; case FRAME_WAIT_CMD: rx_buf[rx_index] byte; fs FRAME_WAIT_SEQ_L; break; // 其余字段依次接收累计到rx_buf并同步计算CRC case FRAME_RX_DATA: rx_buf[rx_index] byte; if (rx_index 2 1 2 2 2 rx_len) { if (calc_crc16(rx_buf 2, rx_index - 2 - 2) rx_crc) { process_frame(rx_buf 2, rx_index - 2 - 2); } fs FRAME_WAIT_AA; rx_index 0; } break; } }状态机一旦把CRC算错或者超时没有新字节到来就重置回FRAME_WAIT_AA丢弃半个包。收完整个固件后IAP对缓存区的完整镜像做一次总体CRC校验确认无误后才开始搬运到APP区。这样每次升级都是一个原子操作要么APP区保持旧版本要么替换成新版本不存在中间态。4. IAP工程核心代码跳转函数、Flash擦写与合法性过滤IAP工程的核心逻辑可以拆成三块上电后判断要不要进升级模式不进就跳转APP进升级模式就收数据写缓存写完后校验、搬运、重启。跳转Flash的细节都在代码注释里照着调工程就能跑通。4.1 跳转函数与中断向量表重映射关中断、设VTOR、改栈顶、跳转跳转APP不是简单地用函数指针调用必须完成四件事关中断、重设向量表偏移、设置主栈指针、跳转到复位向量。如果不关中断跳转瞬间进来的中断会去APP的向量表查询入口但APP的外设和中断初始化还没执行状态完全不确定最常见的现象就是HardFault。#define APP_BASE_ADDR 0x08008000 #define APP_RAM_BASE 0x20000000 typedef void (*p_function)(void); void jump_to_app(void) { uint32_t app_sp *(volatile uint32_t *)APP_BASE_ADDR; uint32_t app_pc *(volatile uint32_t *)(APP_BASE_ADDR 4); p_function app_entry (p_function)app_pc; // 跳转前必须把全局中断关掉并且把挂起的中断全部清掉 __disable_irq(); for (int i 0; i 8; i) { NVIC-ICER[i] 0xFFFFFFFF; // 关闭所有外设中断 NVIC-ICPR[i] 0xFFFFFFFF; // 清除挂起中断 } // 设置向量表偏移APP里所有中断从这里查入口 SCB-VTOR APP_BASE_ADDR; // 重设主栈指针对应APP工程编译出来的初始SP __set_MSP(app_sp); // 跳转APP app_entry(); while (1); }跳转前要确认app_sp落在RAM地址范围0x20000000到0x20000000 RAM大小之间app_pc落在APP区Flash范围之内否则不要执行跳转。如果app_sp异常直接进入升级模式等待重新下发固件而不是盲目跳转。__disable_irq和__set_MSP在Cortex-M3内核对GD32和STM32通用编译器直接内联。4.2 Flash擦除与写入按页擦除、半字写入、擦写后读回校验Flash写入前必须先擦除擦除以页为单位写入按半字16位为单位。GD32和STM32的Flash控制器都不允许字节写入16位是底线。写之前要先解锁Flash控制寄存器写完重新上锁防止误操作。uint8_t flash_write_buffer(uint32_t addr, uint8_t *buf, uint32_t len) { uint32_t addr_end; if ((addr 0x01) || (len 0x01)) { return 1; // 地址或长度未对齐半字直接失败 } addr_end addr len; // 按页擦除页大小根据芯片型号查手册比如1KB while (addr addr_end) { flash_unlock(); flash_erase_page(addr); // 不同GD32型号的擦除API名略有差异 flash_lock(); addr FLASH_PAGE_SIZE; } addr - len; flash_unlock(); while (addr addr_end) { flash_program_half_word(addr, *(uint16_t *)buf); // 立即读回比对防止Flash写入无效或电压不足 if (*(volatile uint16_t *)addr ! *(uint16_t *)buf) { flash_lock(); return 2; } addr 2; buf 2; } flash_lock(); return 0; }擦除动作比较耗时擦除过程中如果代码本身执行在Flash里要确保擦除的区域不是当前正在执行的代码扇区。IAP工程把自己放在0x08000000起始的32KB内擦除APP区和缓存区都不会碰到IAP的代码扇区所以在IAP里直接调擦除函数是安全的。如果擦写函数放在APP里调用就要特别小心最好是搬进RAM执行。4.3 跳转前的合法性过滤魔术字、状态标志、CRC缺一不可IAP每次上电先读取参数区根据参数区的内容决定下一步动作。参数区在Flash末尾用结构体管理读取时直接按地址强转写入时整段擦除再编程typedef struct { uint32_t magic; // 固定魔术字0xA5A5A5A5用于判断参数区是否有效 uint32_t upgrade_flag; // 0表示空闲1表示缓存区有完整固件待搬运 uint32_t fw_size; // 固件总长度 uint32_t fw_crc; // 固件总体CRC32 uint32_t fw_version; // 固件版本号 uint32_t try_count; // 连续启动失败计数 } upgrade_param_t; #define PARAM_BASE_ADDR 0x08038000 #define PARAM_MAGIC 0xA5A5A5A5上电流程在main函数里是这样读参数区如果magic不对则把参数区初始化为全默认值如果upgrade_flag为1说明缓存区有完整固件跳到升级搬运流程否则检查APP区首地址的栈顶指针是否合法合法直接跳转APP。这一套过滤条件看着简单但能挡住绝大多数变砖场景缓存区没有完整固件时永远执行旧APP旧APP被破坏时try_count连续增长超过阈值就自动待在IAP等待重新升级。5. 远程升级避坑清单跳转死机、擦除失败、升级中断全记录远程升级的坑几乎都集中在跳转瞬间和Flash操作两个环节。下面几条是实际调试中最常踩的问题按现象、原因、解决的顺序写清楚排查时可以一条条对照。5.1 跳转到APP后第一个中断就HardFault向量表偏移没生效现象IAP能跑跳转代码执行完就进HardFault但断电重启后APP又能正常运行。原因芯片复位后从Flash起始地址读取向量表APP复位启动时向量表地址天然正确。IAP跳转时如果不重设向量表偏移中断一来CPU仍然去0x08000000找向量表但此时向量表是IAP的中断入口指向IAP函数跳过去检查到地址非法就HardFault。解决跳转前必须设置SCB-VTOR APP_BASE_ADDR。有些GD32型号还需要额外置位一个标志寄存器使VTOR生效务必查芯片手册确认。5.2 跳转后UART中断风暴外设中断没关干净现象跳转瞬间一切正常几微秒后程序陷入死循环调试器停住发现PC在中断处理函数里反复进出。原因跳转前UART、DMA、定时器中断没有关闭跳转到APP后APP的main还没执行到外设初始化此时串口来了一个字节触发中断。中断向量表已经指向APP但APP的中断处理函数访问了尚未初始化的外设寄存器状态未知。解决跳转前遍历NVIC把所有中断关闭并清除挂起位__disable_irq()只是关全局中断不够NVIC里的使能位还在。APP的main函数里第一步就重新配置所有用到的中断和IAP彻底解耦。5.3 Flash擦除时程序跑飞擦除区域和代码执行区重叠现象在升级流程里执行擦除擦到某个地址时程序直接跑飞看门狗复位反复重启。原因擦除的是当前正在执行的Flash扇区CPU取指时发现该扇区已被擦空读到全FF非法指令立即HardFault。升级流程如果放在APP里APP代码所在的扇区恰好和要擦除的扇区重叠就会发生。解决擦写期间执行的代码必须放在不被擦除的区域或者干脆把擦写函数放到RAM里执行。IAP工程天然满足这个条件因为IAP的代码扇区和升级数据区完全隔离这也是为什么远程升级框架必须把IAP独立出来的原因之一。5.4 升级中断一次就变砖没有缓存区和状态回退现象固件传到60%断电重新上电设备完全没反应只能跑现场用烧录器救砖。原因升级流程直接在APP区擦写擦除后还没写完就断电APP区既不是旧固件也不是新固件。解决所有固件先写入升级缓存区只有整体CRC校验通过后才把缓存区内容搬运到APP区。搬运前在参数区写upgrade_flag1搬运完成后写0。上电时IAP看到flag1就重新执行搬运或直接等待新固件下发而不是跳转一个残缺的APP。这套机制哪怕搬运过程中断电旧APP也在只是需要重新下发一遍固件。5.5 固件能写进去但运行异常Flash写入宽度和等待周期现象升级完成后启动APP能跑但部分常量错误有些函数行为怪异断电重启后症状稳定复现。原因Flash写入时地址没按半字对齐或者用了字节写入部分数据没有真正编程成功。另一个常见原因是主频提高后Flash等待周期没配置够Flash读出数据错误。解决写入前检查地址和长度都按2字节对齐用半字编程指令写完立即读回比对。时钟初始化里FLASH_ACR等待周期必须和主频匹配比如主频72MHz时等待周期设2主频108MHz时设3或4具体看芯片手册。这一条最容易在移植到不同型号时不注意到。6. 进阶玩法双Bank启动与断点续传把升级失败变成可回退的事故前面讲的缓存区方案能防止升级中断变砖但还有一种更狠的场景新固件本身有bug跑起来就死机或者疯狂复位。此时APP区已经是新固件IAP每次跳转都失败设备等于废了。进阶做法是双Bank启动把Flash里APP区域拆成Bank A和Bank B当前启动Bank A升级时往Bank B写写完校验通过后翻转启动标志下次跳转Bank B。如果Bank B启动失败IAP检测到连续复位计数超标自动翻回Bank A旧固件随时能顶上。可回退的升级才能真正谈远程无人值守。断点续传是另一个提升体验的点。大固件传到一半网络中断传统做法是全部重传。如果参数区记录着最后成功写入缓存区的包序号重新连接后从断点继续传几十KB的固件能省大量流量和时间。实现上接收ACK时同步更新参数区序号缓存放Flash不用怕掉电丢失只是Flash擦写寿命要吃紧一些。签名校验也建议补上CRC只防随机错误防不了恶意篡改预算允许时对固件头部做摘要校验升级前验证来源合法性。远程升级框架一旦稳定收益非常直观不用跑现场、不用带烧录器、不用约用户时间窗口固件迭代周期从按周变成按小时。我自己的习惯是每次升级前先备份当前固件版本号到日志区升级后连读三次新固件的版本和CRC确认无误才把状态置为完成。这套习惯救过我一次——有一次现场环境温度异常导致Flash写入时序不稳正是靠双Bank和日志回退设备没报废用户甚至没察觉到升级失败过。希望帮到你。本文还有配套的精品资源点击获取