资讯动态

GD32远程升级IAP源码解析:Bootloader与APP分区跳转避坑指南

发布时间:2026/10/15 1:31:23 来源:尧图企业网站定制
简介GD32远程升级程序源码内含IAP引导工程与APP应用程序工程面向STM32/GD32开发者及物联网设备维护场景用于实现基于引导程序的固件在线更新。包内共771个文件以C源文件与H头文件为主另含HEX/BIN固件镜像、工程配置文件、链接脚本、MAP映射文件等整体约17.35MB源码与头文件便于阅读移植固件文件可辅助核对编译产物帮助理解引导程序与应用程序的烧录地址分配。已有4130人学习下载。IAP工程覆盖新固件接收、完整性校验、Flash擦除与写入、跳转执行等核心环节APP工程可作为业务固件模板二者协同展示远程升级的完整流程由于GD32与STM32硬件架构相近该源码可直接作为STM32远程升级的设计参考且工程结构清晰便于对比两套程序的启动流程与地址映射关系。源码在中断向量表偏移、APP起始地址配置、升级失败后的跳转保护等方面也给出具体实现适合需要二次开发引导加载、扩展签名校验或故障回滚机制的团队使用可显著缩短量产设备FOTA功能的开发周期。1. GD32 远程升级为什么绕不开 IAP一张 Flash 地图的事做单片机开发的同行看到GD32 远程升级程序源码这个压缩包时第一反应通常是这不就是 IAP 吗对GD32 远程升级的核心就是 IAPIn-Application ProgrammingSTM32 远程升级也基本沿同一套路。MCU 远程升级的本质不是网络传输而是 Bootloader 把收到的固件写到 Flash、再做一次可靠的跳转。这套源码把 IAP 工程和应用程序工程拆成两个独立项目从 Flash 分区到跳转实现都铺好了路适合正在为产品加 OTA 功能、或者想从串口升级升级到远程升级的工程师直接参考。2. IAP 双工程方案Bootloader 与 APP 怎么划分才能不打架2.1 Flash 分区是第一张地图远程升级首先要解决的是代码往哪放。GD32 和 STM32 的 Flash 都是从 0x08000000 开始编址IAP 方案的本质就是把这块空间切成几段各段干各段的活。拿到这个源码包后我第一件事不是打开 KEIL而是先把工程里的链接脚本和分区宏翻出来把地址地图画到纸上。常见做法是 Bootloader 占最前面APP 排在中间最后一页留给升级标志和固件信息。区域起始地址示例大小作用Bootloader0x080000008KB上电检查、接收固件、校验、跳转APP0x08002000剩余空间减 1 页业务功能代码参数区Flash 末尾 1 页1KB升级标志、版本号、CRC 校验值这个源码包默认的 APP 起始地址就定在 Bootloader 之后具体偏移要看工程的链接脚本。GD32F10x 系列的页大小通常是 1KB但不同型号有差异F105/F107 和 F103 就不完全一样分区时必须以手头型号的数据手册为准。APP 起始地址必须页对齐否则擦除操作会连 Bootloader 的代码一起抹掉这点后面避坑章节还要重点说。2.2 双工程的启动顺序与跳转实现上电后 CPU 从 0x08000000 取向量表也就是先跑 Bootloader。Bootloader 里要执行的任务很简单检查参数区有没有升级请求标志有就准备接收新固件没有就验证 APP 区的头部是否有效有效就跳过去。真正的跳转不是一句goto而是改栈顶指针、取复位向量、再改 PC 寄存器。void jump_to_app(uint32_t app_addr) { /* 1. APP 起始地址的前 4 字节是初始栈顶指针 */ uint32_t sp *(volatile uint32_t *)app_addr; /* 2. 紧接着 4 字节是复位向量地址 */ uint32_t reset_addr *(volatile uint32_t *)(app_addr 4); /* 跳转前把现场清干净关中断、关 SysTick避免 APP 起来后被干扰 */ __disable_irq(); SysTick-CTRL 0; /* 3. 先设置主栈指针再跳转到 APP 复位向量 */ __set_MSP(sp); ((void (*)(void))reset_addr)(); /* 理论上不会走到这里 */ while (1); }代码里每一句都有讲究。关中断和关 SysTick 是我在实际调试中吃过亏后补上的如果 Bootloader 开了某个外设中断跳转时现场没清干净APP 启动的瞬间就可能误入中断服务函数PC 指到错误位置直接 HardFault。__set_MSP(sp)之后可以看到sp的取值来自 APP 烧录时的首地址只要烧录工具和链接脚本一致这个值就不会错。2.3 Bootloader 大小和 APP 预留的权衡Bootloader 做多大是个常见的纠结点。这个源码包的 Bootloader 只负责接收、校验、跳转功能很简单8KB 完全够用但如果你后面要加固件加密、A/B 双备份、回滚逻辑8KB 会立刻捉襟见肘。我一般建议直接把 Bootloader 分区留到 16KB多出来的空间可以放协议解析缓冲和日志也不会导致 APP 区明显变小。APP 区大小则要结合产品功能估算。做过一次远程升级的同事应该都有这种体验第一版 APP 只用了 64KB跑一年加需求加到了 90KB回头看分区预留少了必须挪 APP 起始地址重新烧 Bootloader。所以分区时别只看当前工程编译出来的大小要按产品生命周期里最大可能体积留余量。这个源码包的分区是按 256KB Flash 的型号设计的如果你的 GD32 只有 128KB那就要把 Bootloader 和 APP 的偏移整体重新算一遍。3. 源码包的两个工程Bootloader 与 APP 各自要改的地方3.1 Bootloader 侧核心逻辑怎么动Bootloader 工程的代码量其实不大核心就是一个主循环加几个外设初始化。拿到源码包后建议先看三个地方串口初始化、Flash 擦写函数、跳转函数。串口初始化部分是远程升级的入口固件数据一般是从串口进来如果产品接的是 4G 模组或 WiFi 模组模组把数据透传给 MCU 的串口Bootloader 这边不需要关心网络协议只面对串口数据流。/* Bootloader 主循环伪代码 */ int main(void) { uart_init(115200); /* 升级通道串口注意与上位机一致 */ flash_init(); /* Flash 控制器初始化 */ if (check_upgrade_flag() 1) /* 参数区有升级标志进入升级模式 */ { upgrade_process(); /* 分包接收、写 Flash、校验 */ } else if (check_app_valid() 1) /* 没有升级请求验证 APP 是否完整 */ { jump_to_app(APP_ADDR); /* 合法则跳转 */ } else { /* 两个条件都不满足进入命令行等待方便演示和调试 */ uart_send_string(boot: waiting for upgrade command\r\n); } }这里的check_upgrade_flag()是关键设计点。升级标志不应该只存在 RAM 里而应该写到参数区的固定地址这样断电重启后 Bootloader 还能认出上次升级没完成或用户主动请求升级。我调过一些工程把标志只放在全局变量里复位后就丢了Bootloader 永远跳不到升级模式只能靠按住按键进 Bootloader这在产品部署现场根本不可行。3.2 APP 侧中断向量偏移不偏移就复位很多第一次做 IAP 的人会把注意力全放在 Bootloader 上结果 APP 烧进去一运行就死机或频繁复位最经典的翻车原因就是中断向量表没偏移。APP 不在 0x08000000 启动中断向量表也不在默认位置CPU 一旦产生中断去 0x08000000 取向量表取到的是 Bootloader 的中断服务地址整个程序就乱套了。/* APP 工程 main() 最前面必须加的设置 */ int main(void) { /* 把向量表从 Flash 起始地址搬到 APP 实际起始地址 */ NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x2000); /* 或者直接用寄存器操作SCB-VTOR APP_ADDR; */ /* 部分 GD32 固件库里没有 NVIC_SetVectorTable直接写寄存器最稳妥 */ SCB-VTOR 0x08002000; /* 后续正常初始化 */ SystemInit(); /* ... */ }这段代码必须在任何外设中断使能之前执行否则中断一旦触发向量表还是旧的0x08000000。另外要注意SCB-VTOR的地址要求按 0x40 对齐对 IAP 场景来说 APP 起始地址是页对齐的自然满足。很多源码包在system_gd32f10x.c里会有一句预编译宏控制是否偏移编译 APP 工程时打开宏编译 Bootloader 时关闭着重要检查这个宏别问我是怎么知道的。3.3 编译链接脚本与烧录顺序双工程各自的链接脚本要独立设置。Bootloader 的起始地址是0x08000000长度是分区大小APP 的起始地址要改成0x08002000长度按剩余分区设置。以 KEIL 为例Options for Target 里的 IROM1 起始地址和大小必须和分区表一一对应烧录时先烧 Bootloader再烧 APP顺序错了会导致 Bootloader 被 APP 覆盖。# 示意用命令行烧录工具按顺序烧两个固件 # 先烧 Bootloader再烧 APP不要反过来 JLink.exe -device GD32F103C8 -if SWD -speed 4000 \ -CommanderScript flash_bootloader.jlink JLink.exe -device GD32F103C8 -if SWD -speed 4000 \ -CommanderScript flash_app.jlinkflash_bootloader.jlink脚本里通常是loadbin bootloader.bin, 0x08000000flash_app.jlink里则是loadbin app.bin, 0x08002000。这里有个容易忽略的细节如果app.bin是通过 KEIL 生成的生成的是从0x08002000开始的纯二进制数据直接用loadbin烧到对应地址就行但有些人会拿 Hex 文件去烧Hex 里自带地址信息烧录工具可能按文件里的地址分配容易烧错位置。所以远程升级用的固件包我建议一律转成 Bin 文件和 Flash 地址一一对应避免地址歧义。4. 远程升级全流程走通协议、分包、擦写与状态机4.1 升级帧结构与分包策略固件数据不能一大坨直接丢给 MCUFlash 写入是按页写的串口缓冲区也有限所以传输层要分包加校验。常见做法是自定义一帧升级协议帧头固定、命令字区分升级开始 / 数据帧 / 升级结束每帧带序列号和 CRC。帧结构长度字节说明帧头 HEAD2固定 0xAA 0x55用于识别有效数据起始命令 CMD10x01 开始升级、0x02 数据帧、0x03 结束帧序列号 SEQ2当前包序号从 0 计数用于丢包检测数据长度 LEN2本帧有效数据长度最大 1024 字节数据 DATALEN固件分片数据CRC 校验2对上述全部字段做 CRC16分包大小怎么定我一般取 1024 字节原因和 Flash 页大小有关GD32F10x 一页通常是 1KB一帧数据正好擦一页写一页逻辑最清晰。如果用的型号页大小是 2KB分包大小可以跟着调。串口波特率也是个配套因素115200 波特率下 1KB 数据大约要传 90ms下位机 Flash 擦写还要占时间帧和帧之间留个小延时别让缓冲区溢出。4.2 Flash 擦写与页缓冲的工程代码GD32 的 Flash 控制器和 STM32 不完全一样库函数和寄存器位各有各的叫法。源码包里如果直接用的是fmc_erase_page这类函数注意参数的页地址要按实际型号的页大小对齐。下面是常见的写一页数据的流程我建议先擦后写、写完立即读出比对。/* 擦除一页并写入数据返回 0 表示成功 */ int flash_write_page(uint32_t page_addr, uint8_t *buf, uint16_t len) { uint16_t i; uint32_t word_buf 0; /* 1. 擦除目标页 */ fmc_erase_page(page_addr); /* GD32 库函数 */ /* 等待擦除完成检查忙标志位超时要报错 */ while (fmc_busy()); /* 2. 按 32 位字写入不足部分补 0xFF */ fmc_unlock(); /* 解锁 Flash 控制器 */ for (i 0; i len; i 4) { word_buf 0xFFFFFFFF; memcpy(word_buf, buf i, (len - i 4) ? 4 : (len - i)); fmc_word_program(page_addr i, word_buf); /* 写入一个字 */ while (fmc_busy()); } fmc_lock(); /* 重新上锁 */ /* 3. 写完后整页回读逐一比对 */ for (i 0; i len; i) { uint8_t rd *(volatile uint8_t *)(page_addr i); if (rd ! buf[i]) return -1; /* 回读不一致写失败 */ } return 0; }这段代码里最值得抠的是补 0xFF 的逻辑。固件文件的大小不一定是 4 字节对齐的最后一帧可能只有 300 字节但 Flash 写入是整字操作的不足的部分要补0xFFFlash 擦除后本来就是 0xFF否则会把页里旧数据残留一起写进去回读比对必挂。还有一点fmc_unlock()在每次写操作前都要执行写完立即fmc_lock()这是 Flash 控制器的保护机制漏掉unlock的话写操作会直接被控制器拒绝。4.3 从收到升级指令到跳转 APP 的完整时序把上面的零件串起来远程升级的完整流程是这样推进的上位机发送开始升级命令帧Bootloader 收到后回复确认并进入升级模式上位机逐帧发送固件数据每帧带序列号Bootloader 每收一帧校验 CRC正确则按序列号对应的地址擦写 Flash全部数据传完后上位机发送结束升级帧Bootloader 在参数区写入一个新的 APP 有效标志Bootloader 复位或者直接跳转到 APP 起始地址APP 启动后主动上报自己的版本号上位机比对版本号确认升级成功。typedef enum { ST_IDLE 0, /* 空闲等待升级命令 */ ST_BEGIN, /* 已收到升级开始命令 */ ST_DATA, /* 正在接收固件数据帧 */ ST_END, /* 收到结束帧做最终校验 */ ST_JUMP /* 校验完成跳转 APP */ } upgrade_state_t; upgrade_state_t state ST_IDLE; void upgrade_process(void) { uint8_t cmd get_uart_cmd(); switch (state) { case ST_IDLE: if (cmd CMD_START) state ST_BEGIN; /* 收到开始清空缓存和序号 */ break; case ST_BEGIN: state ST_DATA; /* 准备收数据帧 */ break; case ST_DATA: /* 每帧按序写入序号不连续则要求重传 */ if (check_seq_and_crc()) flash_write_page(...); if (cmd CMD_END) state ST_END; break; case ST_END: /* 全部数据写完置升级完成标志 */ if (calc_total_crc() 0) state ST_JUMP; break; case ST_JUMP: set_upgrade_flag(0); /* 清标志避免反复升级 */ jump_to_app(APP_ADDR); break; } }这个状态机的推进逻辑是没有确认就不前进。序列号不连续或者 CRC 错当前帧直接丢弃并回 NACK上位机超时重传同一序列号这个机制能挡住绝大多数串口噪声和丢字节问题。时序上有个小陷阱置升级完成标志和清升级请求标志是两回事升级完成标志在跳转前写升级请求标志要清掉否则每次上电 Bootloader 都以为你要升级永远跳不进 APP。这个顺序写反的人不在少数我排查过好几个每次复位都进升级模式的案例原因就是标志没按顺序处理。5. IAP 远程升级避坑记录跳转、擦写与回滚的典型翻车现场5.1 跳转后直接进 HardFault 或反复重启现象Bootloader 打印跳转地址后APP 一启动就进 HardFault或者运行几秒后反复复位。 原因最常见的是 APP 工程没设中断向量偏移SCB-VTOR还指向 0x08000000一有中断就取到 Bootloader 的中断向量。其次是跳转前没有关闭外设中断跳的时候 SysTick 或串口中断正挂起APP 起来先响应一个旧中断现场一乱就崩。 解决先在 APP 的main()最前面加SCB-VTOR APP_ADDR;排除向量表问题。再检查 Bootloader 跳转前的现场清理至少关全部中断、清挂起位、把 SysTick 关掉。如果还不行用仿真器在 APP 的 HardFault_Handler 里打断点看 PC 停在哪个函数再回头看是不是外设时钟没重新配置。5.2 擦除操作把 Bootloader 一起抹了变砖只能重新烧录现象升级过程中上位机显示擦除失败或写失败复位后单片机完全没反应仿真器都连不上。 原因分区起始地址算错了。比如 Bootloader 占 8KBAPP 起始地址应该是 0x08002000手一抖写成 0x080010008KB 其实是 0x2000 字节不是 0x1000擦除 APP 页时把 Bootloader 后半段全擦了代码瞬间消失。 解决这个源码包的工程里分区偏移是写好的但移植到其他型号时架构一定要复核。我的习惯是写一个小工具函数每次启动时打印 Bootloader 末尾地址和 APP 起始地址用串口确认中间没有交集。另外烧录时先备份一份 Bootloader 的 Bin 文件一旦发现地址问题先用 JLink 强制恢复 Bootloader再研究代码问题。 提示选型时优先考虑 Bootloader 和 APP 分区地址能被页大小整除GD32F10x 页 1KB起始地址后三位应该是 0如果算出来不是 0赶紧检查是不是算错了。5.3 升级到一半掉电重启后反复升级失败现象现场升级时断电重新上电后 Bootloader 一直提示升级失败进不了 APP也没有回滚到旧固件的办法。 原因升级标志在收到开始升级命令时就写了固件才传了一半Flash 里既没有完整的新固件Bootloader 又认了这个升级请求于是一直尝试从半截数据里恢复结果就是死循环。 解决不要把请求升级和进入升级模式混成同一个标志。正确做法是收到开始命令先清标志固件全部接收完成并且 CRC 通过后再写新固件有效标志。掉电重启时 Bootloader 检查标志如果有效标志没置位说明升级没完成优先跳转旧 APP 继续工作而不是反复接收。如果旧 APP 已经被覆盖了那就必须靠双备份或者预留回滚区这个源码包里如果只做了单分区我建议你在参数区多加一个旧固件有效标志升级失败时还能跳回旧版。5.4 上位机、下位机波特率/分包大小不一致固件传不完现象升级数据传了一部分下位机回复乱码或者没回复上位机重试一段时间后放弃。 原因串口波特率不匹配最直接115200 和 921600 混用会立刻乱码。分包大小不一致更多是隐性问题下位机按 1KB 分包上位机发来一个 2KB 的帧缓冲区溢出一部分序列号全乱。 解决先固定一个系统内通用的升级参数波特率 115200、8 数据位、1 停止位、无校验分包 1KB超时 500ms 重试。参数统一后大部分传输问题就消失了。如果产品部署在不同现场升级参数最好存在参数区Bootloader 启动时从 Flash 读而不是写死。5.5 仿真器连不上目标板错以为变砖现象代码一跑起来仿真器就断开连接或者连接时报错 Cannot access target。 原因APP 初始化时把调试下载脚SWDIO/SWCLK复用成了 GPIO。GD32 和 STM32 都有这个问题SWD 引脚一旦被 APP 代码重新配置为普通 IO调试器就抓不到芯片。 解决如果 Flash 里现在跑的就是占用了 SWD 的 APP先按住复位键在仿真器连接成功的瞬间释放复位趁 Bootloader 还没跳转进 APP 时把程序全擦除再烧回正常版本。代码层面则不要把你的调试引脚随意配成 GPIO至少保留到系统稳定运行后再切否则每一次调试都是痛苦回忆。6. 验证与进阶三层可靠性自检与升级回滚的兜底技巧远程升级做得再好也要在交付前把可靠性验证走到位。我一般把验证拆成三层第一层是协议层用上位机连续模拟升级 50 次随机丢包和加噪声确认丢包重传机制有效统计不成功次数必须为 0第二层是 Flash 层升级完成后整片回读和本地保存的固件 Bin 逐字节比对不比对不放心第三层是运行层APP 启动后先把版本号和 Bootloader 里记录的版本核对一遍不一致就上报异常。三层验证之外还有一个项目和这个源码包适配性极好的进阶技巧升级回滚兜底。做法是 Bootloader 跳转 APP 前在参数区写一行本次启动等待确认标记APP 正常启动 3 秒后主动清掉这个标记。如果 APP 因为新固件问题起不来Bootloader 在下一次复位时发现标记还在就知道新固件没通过自检自动跳转到一个固定存放上次正常固件的回滚区或者直接停在 Bootloader 等上位机重新发固件。/* APP 启动后主动上报版本并确认升级成功 */ uint32_t start_count 0; void app_main_entry(void) { /* 启动后启动一个软定时器或简单系统 tick */ while (1) { os_delay(100); /* 延时 100ms */ start_count; if (start_count 30) /* 3 秒后确认本次启动成功 */ { erase_boot_wait_flag(); /* 清除 Bootloader 里的等待确认标志 */ break; /* 确认完成继续正常业务 */ } } }这是用启动确认换回滚机会的典型做法。远程升级最怕的不是传坏而是 APP 在用户现场跑不起来、又没人能按住复位键去重刷。有了这个标志最坏情况也就是回滚一次不会让设备彻底变砖。从那以后我每次移植这套 IAP 方案都会先画 Flash 分区图、再把双工程编译链和烧录顺序做一遍、然后补上回滚确认三步走完才敢往量产板上写。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑