资讯动态

GD32F103 IAP升级全解析:Bootloader跳转与Flash读写避坑指南

发布时间:2026/9/9 20:47:29 来源:尧图企业网站定制
简介面向嵌入式开发者提供GD32F103芯片的在线升级引导程序完整工程。该芯片与STM32采用相同内核升级流程和关键机制基本一致因此使用STM32的开发者同样可以借鉴。在线升级也称IAP常用于产品出厂后的固件更新与维护本工程正是围绕这一需求设计。工程基于GD32F103C8T6片内只读存储器共64K扇区大小为1K。存储空间划分清晰引导程序从存储器起始位置占用前30K用户程序紧随其后占用余下34K两个区域地址连续且不重叠。资源共164个文件以75个头文件和61个C源文件为主体还包含启动汇编、工程配置文件、链接脚本、生成二进制程序的批处理脚本以及编译好的hex与bin文件压缩包约624K可直接用配套工具打开编译并烧录验证。目前已有5169人学习下载。读者可以从中理解引导跳转、中断向量重定向、闪存擦写、地址分区等核心实现快速搭建属于自己的固件升级方案适合需要动手实践的初中级嵌入式工程师。1. 为什么自己写IAP升级一个避不开的坑做嵌入式开发的朋友应该都遇到过这种场景产品发出去几百台突然发现固件有个小bug要么派工程师带着J-Link到现场拆壳烧录要么让客户把设备寄回来一来一回成本和时间直接失控。GD32F103作为国产Cortex-M3里用量很大的片子IAP升级基本是量产项目的必修课。我这边把调通的一套GD32F103 IAP升级源代码拆开讲清楚Bootloader、App跳转、Flash读写还有网上问得最多的“跳转后卡死”“HAL_Delay失效”这类问题一次性说明白。1.1 IAP到底解决了什么问题IAP全称In-Application Programming中文叫“在应用编程”核心思路是芯片里同时放两段程序一段是Bootloader上电先跑一段是用户App。Bootloader负责通过串口、CAN、USB或者以太网接收新的固件包写到Flash里然后跳转到App执行。这样以后升级固件就不需要打开设备、不需要连调试器只要把新固件发给设备就行。GD32F103的内部Flash支持按页擦除、按半字16位编程这给IAP提供了硬件基础。我们只需要在Bootloader里控制FMCFlash Memory Controller把接收到的固件写入指定地址再通过函数指针跳到App的入口。整个过程涉及“Flash分区规划”“固件传输协议”“中断向量表重映射”“跳转时对CPU状态的合理处置”四个关键点任何一个环节没处理好轻则升级失败重则设备变砖。1.2 这套源代码的设计目标我最初做这套GD32F103 IAP升级源代码目标很直接单串口能升级、断电不损坏老固件、上位机不用装特殊软件。所以在设计上做了几个取舍使用Bootloader App双分区Bootloader固定放在Flash起始地址App偏移到后面。新固件先写入下载暂存区全部接收完成并校验通过后再擦除App区并拷贝过去避免中途断电导致App区损坏。通信协议用简化版XMODEM配合SecureCRT的YMODEM发送功能就能传固件不用为上位机单独花时间。每次固件包附带CRC32校验能识别传输过程中丢字节、改字节的情况。技术方案不追求花哨但每一步都要可靠。毕竟IAP是给已经出货的产品用的出了问题没法挨个拆机。2. 整体设计与代码框架2.1 Flash分区表以GD32F103C8T6为例它内部Flash总共64KB页大小是1KB。分区方式影响后面所有代码建议按下面这张表规划分区名起始地址大小用途Bootloader0x0800000016KB0x4000启动、固件接收、Flash搬运App区0x08004000不固定用户应用程序参数区Flash末尾留1KB1KB保存升级标志、版本号、CRC等Bootloader分16KB看起来奢侈但编译出来通常只有6~8KB留余量方便以后加协议。App从0x08004000开始意味着App工程里的IROM1起始地址也要改成0x08004000。这里最容易出错如果App工程还是从0x08000000编译即使Bootloader跳转成功很多功能也可能异常。参数区我习惯放在Flash最后1KB专门存“是否需要升级”的标志字。这个标志字可以简单设成两个连续32位值例如0xA5A5A5A5和0x5A5A5A5A擦写时保证写入的完整性读取时只有两个值同时匹配才认为有效。2.2 源码结构与通信协议这一版源码保持了精简的层次主要文件就三个bootloader.c主流程、升级标志判断、跳转逻辑。flash_if.cFMC解锁、擦除、编程、整页拷贝。xmodem.cXMODEM接收、CRC校验、分包处理。通信协议层面XMODEM本身已经解决了分包、应答、超时重传的问题一包128字节每包都有CRC校验对串口传输来说足够可靠。需要注意的坑是GD32F103的串口接收要用中断或者DMA不能在阻塞等待中丢字符。我实际用的方案是串口中断接收把数据放进环形缓冲区XMODEM状态机在主循环里调度。很多资料喜欢自己定义“帧头长度CRC32”的私有协议不是不行但工作量会多不少。XMODEM是1980年代的老协议正因为简单可靠至今还在嵌入式Bootloader里广泛使用。只要你稍微改一下让每一包能携带“偏移地址”或者“固件总长度”信息就能变成很实用的升级通道。2.3 Bootloader主流程Bootloader的main函数逻辑很直接先判断有没有升级请求有就进入固件接收流程没有就校验App是否有效有效则跳转。int main(void) { system_clock_config(); uart_init(115200); led_init(); if (check_upgrade_flag()) { update_firmware(); // 接收固件写入临时区校验后搬运到App区 clear_upgrade_flag(); } if (app_valid()) { jump_to_app(APP_START_ADDR); } /* App无效就在这里死循环方便继续接收固件 */ while (1) { update_firmware(); } }实际产品里app_valid()不只要判断0x08004000地址的前4字节是不是合法栈顶最好再判断入口地址是否落在App区间。比如reset_vector必须在0x08004000到0x0800FC00之间否则可能跳到未知地址直接HardFault。3. 核心代码实现3.1 跳转函数完整实现跳转是整个IAP里最微妙的地方网上关于“跳转后卡死”的提问有一大半都出在这里。一个能稳定工作的跳转函数至少要做三件事关闭全局中断、停掉SysTick、设置主堆栈指针。下面是我在GD32F103上调通的版本typedef void (*app_entry_t)(void); #define APP_START_ADDR 0x08004000u void jump_to_app(uint32_t app_addr) { uint32_t msp_value *(volatile uint32_t *)app_addr; uint32_t reset_value *(volatile uint32_t *)(app_addr 4u); app_entry_t app_entry; /* 栈顶地址必须在RAM范围防止跳到非法地址 */ if ((msp_value 0xFFF00000u) ! 0x20000000u) { return; } __disable_irq(); /* 彻底停掉SysTick避免跳转后SysTick_Handler错乱 */ SysTick-CTRL 0u; SysTick-LOAD 0u; SysTick-VAL 0u; /* 设置MSP后再取出复位向量 */ __set_MSP(msp_value); app_entry (app_entry_t)reset_value; app_entry(); }为什么非要用msp_value 0xFFF00000做合法性判断因为Cortex-M3的RAM地址一般在0x20000000开头而栈顶指针必然落在RAM里。如果读出来是0xFFFFFFFF或者0x00000000说明这里没有固件直接返回比硬跳安全得多。关中断的顺序也很有讲究必须在读取复位向量之后、调用函数指针之前。因为一旦跳过去App可能立刻会开中断我们得保证从“关闭中断”到“新向量表生效”之间没有中断钻进来。3.2 App端中断向量表重映射跳转只是第一步App本身必须知道自己不在0x08000000而在0x08004000。Cortex-M3的中断控制器有一个VTOR寄存器用来告诉CPU中断向量表放在哪里。GD32F103和STM32F103在这一点上寄存器布局一致配置方法有两种。如果用GD32标准外设库可以直接调用nvic_vector_table_set(NVIC_VECTTAB_FLASH, 0x4000u);如果在App里用的是STM32 HAL库或者类似结构可以在main最开始设置SCB-VTOR 0x08004000u;关键点是这条语句必须在任何中断使能之前执行最好放在main的第一行。如果你用的是STM32 HAL库还要记得在system_stm32f1xx.c里把VECT_TAB_OFFSET改成0x4000u否则SystemInit()会在你写上那句赋值之前又把VTOR清成0。这里有个容易忽略的细节跳转完成后PC一下就执行到App的Reset_Handler但这时候外设寄存器还是Bootloader里的状态。所以App的启动文件里那段“拷贝数据段、清BSS段、调用SystemInit”的流程必须完整跑一遍外设时钟配置也必须从头初始化不能指望Bootloader帮你做过。3.3 Flash擦写与固件接收GD32F103的Flash编程不能像RAM一样直接用指针写必须先解锁FMC按页擦除再按半字写入。一个典型写入函数长这样void flash_write_page(uint32_t addr, uint8_t *buf, uint32_t len) { fmc_unlock(); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR); /* 先擦除整页 */ fmc_page_erase(addr); fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR); /* 按半字(16bit)写入 */ for (uint32_t i 0u; i len; i 2u) { uint16_t data buf[i] | ((uint16_t)buf[i 1] 8u); fmc_halfword_program(addr i, data); while (fmc_flag_get(FMC_FLAG_BUSY) ! RESET) { } fmc_flag_clear(FMC_FLAG_END | FMC_FLAG_WPERR | FMC_FLAG_PGERR); } fmc_lock(); }这里有几个坑值得展开说。第一擦除函数和写数据函数的地址参数都是“绝对地址”不是偏移。很多人把App偏移量0x4000直接传进去导致擦到Bootloader区域当场变砖。第二写入前一定要确保这一页已经被擦除。Flash编程只能把1写成0不能把0写成1如果不擦除写入后数据会错乱。第三写完一页后强烈建议回读校验对比缓冲区内容和Flash实际内容。虽然这会让升级时间变长几十毫秒但能提前发现Flash老化、电压不稳、地址不对等问题。固件接收部分用的是XMODEMXMODEM的发送方一包一包发接收方每收到一包就回一个ACK。Bootloader收到第一包时就能从包头解析出总长度然后按页暂存到下载暂存区。等所有包都收完CRC校验通过再执行“擦除App区拷贝”的动作。这样安排的好处是App区在拷贝完成之前始终保持上一版固件即使中途断电重新上电后Bootloader发现没有有效新固件依旧会把旧的App跑起来。4. 常见问题跳转后卡死与HAL_Delay失效4.1 跳转后卡死的原因“从Bootloader跳到App程序就死掉”是最常见的现象原因无非下面几类。一是中断向量表没偏移。App里没有设置VTOR或者设置的位置不对中断一旦发生CPU会跑去Bootloader的向量表找入口但Bootloader的中断服务函数里处理的是Bootloader的业务逻辑自然没法正常工作。表现就是跳转过去后一打开中断就死。二是跳转前外设中断没处理干净。比如串口接收中断正在挂起跳过去一瞬间App的向量表还没准备好这个中断先到了CPU直接进HardFault。解决办法是在跳转前关闭全局中断必要时清掉挂起位。三是App的编译地址和实际存放地址不一致。App编译时IROM1起始地址还是0x08000000但你把它放在0x08004000运行中断向量表和字符串常量都会有问题程序跑飞是常事。四是堆栈指针检查没做读了错误的Flash内容跳转到非法地址。4.2 HAL_Delay失效的根因网上很多人问“跳转后HAL_Delay卡死”这个问题非常典型。HAL_Delay的原理是不断查询uwTick这个全局变量而uwTick是在SysTick中断里加的。如果你的App里调用了HAL_Delay但SysTick中断没有正确执行uwTick永远不变HAL_Delay会一直卡在while循环里。SysTick中断不执行通常是两个原因叠加造成的App工程没有正确设置中断向量表偏移导致SysTick中断进来后找不到App的SysTick_Handler或者Bootloader跳转前没有把SysTick停掉跳转后SysTick还残留着上一次的加载值时序完全对不上。我调过的一个项目就卡在这上面。Bootloader跳转前没有执行SysTick-CTRL 0App的VTOR也忘了设置结果跳过去后HAL_Delay直接卡死。代码里去掉HAL_Delay就正常加上就死排查了一个多小时才定位到是SysTick的问题。修复需要两头做。Bootloader跳转前把SysTick停掉并清空计数代码章节里的SysTick-CTRL 0那三行就是干这个的。App侧在main最前面设置SCB-VTOR 0x08004000然后再调HAL_Init()和SystemClock_Config()顺序不能反。4.3 问题速查表现象可能原因解决办法跳转后立即HardFault栈顶地址非法、复位向量异常、App地址不对检查App起始地址处的前8字节加栈顶合法性判断HAL_Delay卡死VTOR未偏移、SysTick没有重新初始化跳转前停SysTickApp里先设VTOR再HAL_Init跳转后串口乱码时钟配置不正确或App和Bootloader用了不同波特率检查HSE_VALUE、PLL参数、串口分频Flash写入失败FMC未解锁、地址越界、页未擦除先调用fmc_unlock正确擦除后再写升级中断电变砖没有暂存区/备份区直接覆盖App用下载暂存区校验通过后再搬运升级完成后旧App还在升级标志判断逻辑反了或者固件传输没完成在固件接收完成并校验成功后才清标志位这张表我每次给同事做培训都会贴出来大多数IAP问题都能在上面找到影子。5. 踩坑记录与实战建议5.1 升级失败后的恢复策略IAP最怕的不是升级失败而是失败后设备变成砖。做一个相对安全的恢复机制需要从设计上保证Bootloader永远可用。一个有效的做法是“三段式”Bootloader区、App区、下载暂存区。新固件先全部收进下载暂存区这里可以放在Flash尾部也可以放在外部SPI Flash或者W25QXX里。收完后对数据进行CRC校验校验通过才允许擦除App区。如果擦除App区之后、拷贝还没完成时掉电那确实会变砖所以更稳妥的方案是拷贝时先写“搬运中”标志重新上电时发现这个标志就继续完成搬运而不是直接跳转。另外一个很实用的习惯是给App加一个“升级失败自动回滚”机制。比如App启动时先记录当前版本号到参数区运行5秒后如果没有收到升级指令就认为本次启动正常如果5秒内被异常复位多次Bootloader就认为App有问题自动回到接收固件模式。这种兜底逻辑不需要多复杂但能省掉大量售后问题。产品在外面你不可能每次都用J-Link去救能靠串口重新引导就是最大的幸运。5.2 调试期最有效的三板斧IAP调试第一件事不是直接烧Bootloader而是先把App单独烧到0x08004000用调试器确认它在偏移地址能独立运行。做法是改App工程的IROM1起始地址然后烧录到0x08004000跑一下基本功能。如果这一步就出问题说明App本身没有按偏移地址编译好跟Bootloader无关。第二件事把Bootloader里的升级功能先屏蔽只保留“检查标志位跳转”。烧进去后确认能从Bootloader跳到App再把升级功能加回来。这样排查问题时能快速缩小范围不会一死机就一头雾水。第三件事串口打印一定要留。Bootloader里每个关键节点都打一条日志比如收到第几包、CRC是否正确、App校验是否通过。产品量大之后你会发现IAP问题最难的往往不是代码而是“现场到底发生了什么”。有了日志客户发一条串口信息回来你马上就能定位。5.3 源码扩展方向这套GD32F103 IAP升级源代码目前是串口接收Flash搬运的裸机方案改造成CAN升级或者网络升级也不难只需要替换传输层。比如用CAN把XMODEM的串口收发函数换成CAN收发协议本身不用动。用网络时记住一个原则底层传输可以换上层“暂存区校验断电恢复”的健壮性逻辑不要动。如果App将来升级到FreeRTOS要注意跳转前必须把RTOS的全局中断屏蔽掉并且确保跳转时处理器处于线程模式使用MSP而不是PSP。裸机代码里这个问题不突出一旦上RTOS很多人直接踩“跳转后不进main”的坑。再往后如果GD32F103的Flash空间不够可以考虑把下载暂存区放到外部SPI Flash固件先通过串口传到外部Flash再慢慢搬到内部Flash。虽然速度慢一点但内部Flash空间压力会小很多。我在实际项目里还有一个小习惯Bootloader做完后会专门写一个2KB的小固件当作“急救固件”平时不启用只有出厂烧录或者售后返修时烧进去。这样即使Bootloader的升级逻辑出了意外也能有个最小系统把设备救回来。这个习惯成本很低但带来的安全感很高。IAP这种东西平时看着不起眼出一次问题就足够记住一辈子。本文还有配套的精品资源点击获取

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

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

免费获取报价