news 2026/9/8 1:35:24

GD32远程升级实战:IAP BootLoader与Flash分区详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GD32远程升级实战:IAP BootLoader与Flash分区详解

简介:面向STM32/GD32开发者的远程升级参考工程,内含IAP引导加载与App应用程序两个独立工程,适合在物联网设备中实现FOTA固件升级的嵌入式工程师。源码基于GD32编写,因两系列芯片架构相近,也可直接移植到STM32平台,解决设备分批部署后的固件迭代与维护难题。压缩包共771个文件,大小17.35MB,以C/H源码、O/CRF中间编译文件、HEX/BIN固件、MAP映射及工程配置为主,便于查看烧录内容与编译链接关系。已有4114人学习下载。该工程的价值在于提供一套可裁剪的远程升级框架:IAP工程包含通信接收、固件校验、Flash擦写与跳转逻辑,App工程演示业务应用如何配合引导程序;同时附带备份文件和创建信息,可辅助对比编译前后差异,快速定位启动地址、中断向量表偏移等常见坑点。 做嵌入式产品,尤其是批量发货之后,设备摆在客户现场,想升级固件却发现还得派人提着仿真器上门,这场景有多酸爽,做过的人都知道。最近我整理了一套GD32远程升级程序源码,工程里同时包含IAP(In-Application Programming)裁剪后的BootLoader和应用程序两个独立工程,GD32系列可以直接拿去用,做STM32远程升级的朋友也可以参考这套架构思路,两者在IAP上的玩法大同小异。这篇文章就把这套工程的核心设计思路、Flash分区逻辑、跳转关键代码、编译配置和踩坑重点全部拆开讲一遍。适合正在做BootLoader、被远程升级方案折磨过、或者跳转后卡死一调就是三五天的同学认真看一遍。

1. 方案设计拆解:为什么非要做独立BootLoader

1.1 IAP和ICP的本质区别

一开始很多人会把IAP和ICP搞混,其实两者的分工完全不同。ICP(In-Circuit Programming)要靠仿真器,比如J-Link、ST-Link,通过SWD或者JTAG接口直接往Flash里写程序,这种方案在开发阶段很好用,但设备部署到现场之后就很难受,总不能人手一个仿真器跑去拆机。

IAP的思路则是让芯片里的程序自己擦写自己的Flash。BootLoader先运行,从串口、USB、CAN或者网络把新的固件包收进来,拷到Flash里,然后跳转到App区执行新程序。整个过程只需要通信接口,不用仿真器,也不需要拆机,这就是远程升级的基础。

我这些年做产品,一个深刻的体会是:凡是要出货的产品,升级功能在设计阶段就要留好口子,否则后期只能拆机返厂,那个成本真的能把人搞崩溃。

1.2 BootLoader和APP两个工程怎么分工

这套源码工程的核心是双工程方案。BootLoader单独一个工程,负责通信接收、固件解析、Flash擦写、跳转管理;APP是另一个工程,只管业务逻辑,比如传感器采集、电机控制、协议上报,同时预留一个“收到升级指令就跳回BootLoader”的入口。

为什么要拆成两个独立工程而不是写在一起?原因有三点。第一,两个工程可以分别编译、分别烧录,BootLoader烧进去之后可以锁定保护起来,只升级APP,避免不小心把BootLoader也冲掉。第二,资源边界清晰,BootLoader的大小受限于分区规划,如果和APP混在一起,代码膨胀之后很容易互相覆盖。第三,BootLoader尽量保持稳定,代码越少出错的概率越低,升级功能本身的可靠性是关键。

1.3 GD32和STM32的适配性问题

标题里写了GD32远程升级,同时说明STM32也可以参考,这里面的逻辑其实在于两者都属于Cortex-M内核,IAP的原理完全一样,都是靠修改向量表偏移、切换PC指针来实现。但是注意:外设寄存器地址、固件库函数名、Flash写入时序这些和芯片强相关,不能直接拿GD32的源码往STM32上复制粘贴。

举个例子,GD32的库函数风格是gd32f10x_fmc.c这类,STM32标准库则是stm32f10x_flash.c,函数名、延时参数都有区别。移植的时候重点看Flash擦写函数、中断向量表设置、时钟配置这几块,改成目标芯片对应的固件库即可,整体框架不用动。这就像装修房子的水电图纸,户型变了但布线思路是通用的。

2. Flash分区与地址规划:决定升级稳定性的基石

2.1 一份可以直接照抄的分区表

远程升级最怕什么?最怕写错地址,把BootLoader区擦了,或者APP覆盖到了参数区,那设备就直接变砖。所以第一步必须把Flash分区表定死,这里给出一份适用于256KB Flash示例芯片的分区方案:

区间起始地址大小内容
BootLoader区0x0800000016KBIAP升级程序
参数存储区0x0800400016KB升级标志、设备参数、运行日志
APP区0x08008000224KB业务应用固件

BootLoader选16KB是经过考虑的。一个精简的BootLoader包括串口驱动、Flash驱动、简单的升级协议,代码量其实不大,8KB到16KB足够,留出富余以后加功能也方便。参数区单独划出来非常重要,很多做IAP的人容易忽略,升级标志、设备序列号、校准参数如果和APP塞在一起,每次升级擦写Flash都会对它有影响。APP区的起始地址0x08008000就是本文多次提到的偏移量,编译工程时所有与地址相关的配置都从这里开始算。

2.2 中断向量表重映射:跳转后卡死的真正元凶

如果遇到“IAP跳转后卡死hal_delay”这种问题,十有八九是中断向量表没有重映射。Cortex-M内核有个VTOR寄存器,专门存放向量表基地址。芯片上电时默认从0x08000000取向量,也就是BootLoader区;跳转到APP后,如果APP里没有主动修改VTOR,中断发生时CPU依然会跑到0x08000000去查向量表,自然进不了APP的中断服务函数,表现就是HAL_Delay卡死、串口无响应、系统一进中断就飞。

正确做法是在APP工程的main函数最开头,动手写任何初始化之前,先把VTOR指向APP区:

/* APP工程中,main函数最前面执行 */ #if defined(__GNUC__) SCB->VTOR = FLASH_BASE | 0x8000; /* 0x8000即32KB偏移 */ #else SCB->VTOR = FLASH_BASE | 0x8000; #endif

这里以0x08008000为例,偏移量就是0x8000。如果用的是STM32CubeMX或者标准库工程,有的启动文件里已经支持通过宏定义来设置向量偏移,但很多没有,必须手动加一行,这行代码值万金。

注意:设置VTOR的位置必须在使能任何外设中断之前,包括系统滴答定时器SysTick。放在SystemClock_Config()等时钟配置之前最稳妥,因为时钟配置过程中本身就依赖中断。

2.3 Boot跳APP的标准动作与防呆处理

跳转函数是BootLoader的核心动作,代码量不大,但坑特别多。关键步骤是先判断栈顶地址是否合法,再关掉所有中断,恢复系统时钟到默认状态,最后设置主堆栈指针并跳转:

#define APP_FLASH_BASE 0x08008000 typedef void (*pFunction)(void); void JumpToApp(void) { uint32_t app_stack_addr; /* APP初始栈顶 */ pFunction app_entry; /* APP复位向量 */ app_stack_addr = *(volatile uint32_t *)(APP_FLASH_BASE); /* 非0xFFFFFFFF且按2字节对齐,粗略判断栈顶合法 */ if (app_stack_addr == 0xFFFFFFFF || (app_stack_addr & 0x0002) != 0) { return; } app_entry = (pFunction)*(volatile uint32_t *)(APP_FLASH_BASE + 0x4); /* 跳转前把中断和定时器关干净 */ __disable_irq(); SysTick->CTRL = 0; HAL_DeInit(); /* STM32/HAL库需恢复外设到复位态 */ SystemClock_Config_Reset(); /* 恢复时钟到上电默认状态,可选 */ __set_MSP(app_stack_addr); app_entry(); while (1); }

这个代码里有几个细节很多人会忽略。跳转前一定要把外设恢复到复位状态,比如串口DMA如果还在跑,中断标志位还挂着,一旦跳到APP开了中断,立马触发一次异常中断,直接造成卡死。时钟也要恢复默认,否则APP里重新初始化时钟时会按错误的基准重配,结果就是串口波特率乱掉,逻辑时序也不对。那句栈顶地址判断千万别省,如果APP区是空的,或者擦除了一半,强行跳转就是死机。

3. 双工程配置与实现细节:能稳定运行的工程长什么样

3.1 APP工程编译偏移设置

编译器并不知道你的APP要运行在0x08008000,默认会从0x08000000开始布局,所以必须显式告诉链接器。Keil MDK里,在Options for Target - Target页面的IROM1起始地址填0x08008000,大小填0x00038000(224KB)。IAR则在Linker配置里把ROM起始地址改掉。改动之后,生成的bin文件就天然是从0x08008000起始的,用户把bin发给设备升级,BootLoader直接搬运即可。

这里有个使用习惯问题:烧录下载调试时,烧录器会擦除整个Flash还是只烧录当前范围?如果是Keil的Flash Download设置里勾选了Erase Full Chip,那调试APP时BootLoader也会被擦掉,所以建议设置成Erase Sectors而不是全片擦除,否则每次烧完APP还得重新把BootLoader烧回来,特别耽误做事。

3.2 BootLoader的数据接收与Flash写入

BootLoader里面最重要的就是通信协议和Flash写入逻辑。协议不建议搞复杂,帧头、命令、长度、数据和CRC校验必须齐全,按包发送、按包确认,这样任何一个包丢了都能及时重传。一个简单可靠的协议格式可以参考:

typedef struct { uint16_t frame_head; /* 帧头固定 0xAA55 */ uint8_t cmd; /* 命令:0x01擦除 0x02写入 0x03校验 0x04跳转 */ uint32_t addr; /* 目标地址 */ uint16_t len; /* 数据长度 */ uint8_t data[256]; /* 数据载荷 */ uint32_t crc32; /* CRC32校验值 */ } upgrade_frame_t;

Flash写入的逻辑看起来简单,实际上对时序非常敏感。写Flash前必须先擦除对应扇区,擦出后可以回读FF来确认擦除成功;写入时按字或者半字写入,不同芯片的写入宽度要求不一样,GD32的FMC模块和STM32的Flash模块在操作时序上有差异,写完后一定要回读比对。如果哪一步返回失败,就立刻中止并向发送端返回错误码,绝对不能稀里糊涂地继续写。

Flash写入最容易出问题的三个点:擦除对象选错导致App区被擦、写入时地址未对齐导致硬件HardFault、没有回读校验导致“写成功了但跑起来是乱的”。开发BootLoader时这三条要作为强制检查项。

3.3 APP侧需要配合的升级入口

升级一定要有“门”,也就是APP如何跳回BootLoader。我见过不少人把跳转做成直接软件复位,复位之后BootLoader每次都等几秒新的升级包,没有就跳回APP,这样做浪费时间不说,而且如果用户在正常使用中复位了一次,还要莫名其妙等几秒。稳妥的做法是“标志位+软复位”:

#define UPGRADE_FLAG_ADDR 0x08004000 /* 参数区固定地址 */ void TriggerBootLoader(void) { /* 写一个特殊标志到参数区,然后软复位 */ *(volatile uint32_t *)UPGRADE_FLAG_ADDR = 0x5A5A5A5A; NVIC_SystemReset(); }

BootLoader里启动后先检查这个标志,如果是0x5A5A5A5A就进入升级流程,否则直接跳转APP。需要注意的是,参数区必须通过备份寄存器或Flash专门划分,不能用普通RAM,RAM一旦掉电或者软件复位就清零了,没法判断。很多产品还会结合按键触发:上电时按住某个按键进入升级模式,这属于硬件触发的备用方案,关键时刻能救命,建议两个方案都做上。

4. 问题排查速查表与踩坑实录

4.1 高频问题速查表

症状原因解决办法
IAP跳转后卡死,HAL_Delay不进中断向量表未重映射或外设中断未关闭APP入口设置SCB->VTOR,跳转前关闭中断并恢复外设
升级写不进去,返回Flash错误扇区未擦除、地址越界、对齐错误先擦后写,回读校验,确认目标地址在APP区范围内
跳转后APP串口乱码跳转前时钟树改动,但App里没有重新初始化跳转前恢复默认时钟或确保APP初始化时钟逻辑完整
升级到一半死机,重启还是旧程序数据帧丢失,半包数据被写入Flash逐包ACK+CRC校验,超时重传,预留帧序号去重
烧录APP调试时BootLoader丢失Keil/仿真器配置成全片擦除Flash Download设置改成Erase Sectors

4.2 印象最深的两个现场故障

第一个是设备升级偶尔卡死,当时排查了很久,发现是上位机发送数据太快,BootLoader里的串口接收缓冲区溢出丢包,导致写入了一包不完整的数据。这个问题加了逐包ACK之后彻底解决:每收到一包校验通过才发ACK,发送端等到ACK才发下一包,速率控制不再依赖定时器,而是靠握手保证。哪怕升级包有几百KB,多花一点时间换稳定性,绝对值。

第二个故障更有意思,现场反馈设备升级后逻辑全乱,但是复位一次又正常。查到最后发现是APP的向量表偏移写对了,但Keil里的IROM1配置没改,编译出来的中断向量依然指向0x08000000,进中断的时候先跑到BootLoader里逛了一圈,行为不可预测。把链接地址修正之后问题消失。所以编译配置和代码配置要同时检查,两边不一致症状非常隐蔽。

4.3 做远程升级的几个实用小技巧

调试这套工程时,建议从最简单场景递进验证。第一步,直接烧录BootLoader和App,验证不经过升级流程也能正常跳转;第二步,用串口助手手动发送升级包,验证接收和Flash写入;第三步,写一个简单的上位机脚本自动分包发送;最后再做断点续传、失败重试这些增强功能。每一步都确认没问题,再进入下一步,别一上来就全套上,出了问题很难定位。

日志输出是调试的一大利器。BootLoader和APP各自在串口输出带前缀的日志,比如Boot的日志加[BOOT]前缀,APP加[APP]前缀,配合LED指示灯区分当前状态,能省下很多时间。跳转前后各打一行日志,能非常直观地确认执行到了哪里。我调跳转卡死问题时就是靠两行日志缩小范围的,那个定位效率远比猜测高。

升级功能做好之后,你还会想这些

写Flash、跳转这些逻辑本身不难,难的是把各种异常场景提前堵好。我个人的经验是:BootLoader代码尽量少动,一旦稳定下来就不再改,APP随便迭代,BootLoader保持绝对稳定;升级包在写入前增加完整性和版本号校验,防止固件版本倒退;升级过程中突然断电做了兜底,因为BootLoader还在,设备可以重新进入升级模式,不至于变砖。这套GD32远程升级工程的思路我觉得是值得反复打磨的,你上手之后建议先把Flash分区吃透,然后动手改一版自己的BootLoader,比看十遍代码都管用。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 1:34:23

Flink实时计算核心机制与生产实践:从状态管理到Kafka数据链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 1:30:35

Mac上使用Homebrew安装与管理pnpm的完整指南

1. Mac 安装 Homebrew 完整指南Homebrew 是 macOS 上最受欢迎的包管理器,它让安装、更新和管理软件变得异常简单。作为一名长期使用 Mac 的开发人员,我几乎每天都会用到 brew 命令。下面分享我在不同网络环境下安装 Homebrew 的经验,包括国内…

作者头像 李华
网站建设 2026/9/8 1:28:40

标签打印机二次开发包对接实战:从指令协议到C#调用完整指南

简介:2200E标签打印机二次开发包V2.072面向需要集成标签打印功能的软件开发人员,提供整套DLL动态库、API接口、示例工程与帮助文档,帮助快速实现标签设计、打印参数配置及二维码/DataMatrix码输出。包内共133个文件,以DLL、EXE、B…

作者头像 李华
网站建设 2026/9/8 1:27:47

Python环境搭建全指南:从零配置解释器、pip与VSCode

我见过太多零基础的人,学Python的第一天就放弃了。不是因为语法看不懂,也不是因为逻辑绕不过来,而是卡在环境搭建上——下载完安装包、双击、下一步、下一步,满怀期待地打开终端敲下一行python,结果屏幕甩回来一句“不…

作者头像 李华