简介:这份资源是面向ARM嵌入式与单片机开发者的GD32F103 IAP在线升级源代码工程,适合已掌握STM32/GD32基础、需要为产品加入固件远程升级能力的工程师参考。由于GD32与STM32内核一致,IAP升级路径主要依赖内核机制,因此该工程对两类芯片的升级方案具有通用借鉴价值。压缩包共164个文件,约624KB,以C源码与头文件为主体(61个.c、75个.h),并包含8个汇编启动文件、Keil工程文件(uvprojx/uvoptx)、分散加载脚本sct、批处理脚本bat,以及可直接烧录验证的hex与bin文件,覆盖从底层驱动到应用层的完整结构。工程针对GD32F103C8T6的64K Flash做了明确划分:bootloader占用0x8000000至0x80077FF共30K,用户程序占用0x8007800至0x800FFFF共34K,扇区大小1024字节,便于读者直接套用或按需调整分区。目前已有5182人学习下载,可作为IAP跳转、固件搬运与升级流程调试的实践起点。
1. GD32F103 IAP 升级源代码:把固件更新这件事从“拆机插线”变成“远程点一下”
去年帮一个做工业采集终端的朋友处理售后,现场设备装在配电柜里,每次改一行采集逻辑,都得派工程师带着笔记本和仿真器跑一趟,拆外壳、接 SWD、烧录、再装回去。后来我把这套 GD32F103 IAP 升级源代码塞进他们的固件框架,产线端只要通过串口发一条升级指令,设备自己就把新固件写进 Flash 并跳转运行。IAP(In-Application Programming)说白了就是让单片机在跑着程序的时候,自己擦写自己的程序区。这份源码面向的是用 GD32F103 做产品、又不想每次升级都拆机的嵌入式开发者,尤其是那些设备已经部署到现场、维护成本比开发成本还高的场景。它不依赖外部烧录器,核心就是 Bootloader + App 两段式结构,配合串口或 CAN 把 bin 文件传进来。
2. 先看清这套 IAP 的骨架:Bootloader 和 App 怎么分家
2.1 两段式 Flash 布局与跳转逻辑
GD32F103 的 Flash 起始地址是 0x08000000,IAP 方案里最忌讳的就是 Bootloader 和 App 抢地盘。这套源码把前 16KB 划给 Bootloader,从 0x08004000 开始才是 App 的入口。Bootloader 上电后先判断升级标志位,如果标志有效就进入接收流程,否则直接跳转到 App 区。跳转前必须做三件事:关总中断、重设栈顶指针、把 PC 指向 App 的复位向量。很多人跳转后跑飞,八成是忘了关中断或者栈指针没重设。
/* bootloader.c —— 跳转到 App 的核心片段 */ typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { pFunction app_entry; /* 检查 App 区栈顶是否合法:必须在 SRAM 范围内 */ if (((*(volatile uint32_t*)app_addr) & 0x2FFE0000) == 0x20000000) { __disable_irq(); /* 关总中断,防止跳转过程中断捣乱 */ SysTick->CTRL = 0; /* 关滴答定时器 */ SysTick->LOAD = 0; SysTick->VAL = 0; /* 重设向量表偏移,让 App 的中断能正确响应 */ SCB->VTOR = app_addr; __set_MSP(*(volatile uint32_t*)app_addr); /* 重设主栈指针 */ app_entry = (pFunction)(*(volatile uint32_t*)(app_addr + 4)); app_entry(); /* 跳过去,不再返回 */ } }这段代码里app_addr就是 0x08004000,SCB->VTOR那行是 GD32F103 和 STM32F103 都通用的向量表重定位寄存器。如果 App 里用了中断,不设 VTOR 的话中断会跑到 Bootloader 的向量表里去,现象就是串口收不到数据、定时器不触发。__set_MSP取的是 App 区第一个字,那是编译时链接脚本里指定的栈顶地址。
2.2 升级标志位的存放与掉电保护
升级标志不能随便找个 RAM 变量存,掉电就没了。这套源码用 Flash 的最后一页(0x0801FC00,具体看芯片容量)专门存标志和固件信息。每次收到升级命令先擦这一页、写标志,再开始收数据。收完校验通过后把标志改成“升级完成”,下次上电 Bootloader 看到这个标志就直接跳 App。这里有个血泪经验:擦写标志页和擦写 App 区不能放在同一个操作里,否则升级中途断电,标志页和 App 区都废了,设备直接变砖。
/* flash_if.c —— 标志页读写 */ #define FLAG_PAGE_ADDR 0x0801FC00 #define FLAG_UPGRADING 0x5A5A5A5A #define FLAG_DONE 0xA5A5A5A5 uint32_t read_upgrade_flag(void) { return *(volatile uint32_t*)FLAG_PAGE_ADDR; } void write_upgrade_flag(uint32_t flag) { fmc_unlock(); fmc_page_erase(FLAG_PAGE_ADDR); /* 先擦后写,Flash 只能 1 变 0 */ fmc_word_program(FLAG_PAGE_ADDR, flag); fmc_lock(); }fmc_unlock/lock是 GD32 标准外设库的 Flash 操作函数,和 STM32 的 HAL_FLASH_Unlock 一个意思。注意fmc_page_erase的粒度是 1KB 或 2KB,取决于具体型号,擦之前一定确认地址对齐,否则会误擦相邻区域。
2.3 通信协议:帧头、长度、CRC 一个都不能少
传输层这套源码默认走串口,协议格式是:帧头(2字节 0xAA 0x55)+ 命令字(1字节)+ 数据长度(2字节)+ 数据 + CRC16(2字节)。命令字区分“进入升级”“传输数据”“结束升级”“跳转 App”。每包数据建议不超过 1KB,因为 Bootloader 的接收缓冲区一般就开 1~2KB,包太大容易溢出。CRC16 校验是必须的,工业现场串口受干扰的概率不低,没有校验的话写进去的固件可能某几个字节是错的,跑起来就是随机死机,查起来非常玄学。
/* protocol.c —— 解析一帧数据 */ typedef struct { uint8_t cmd; uint16_t len; uint8_t data[1024]; } frame_t; int parse_frame(uint8_t *buf, uint16_t buf_len, frame_t *out) { if (buf_len < 7) return -1; /* 最短帧:头2+命令1+长度2+CRC2 */ if (buf[0] != 0xAA || buf[1] != 0x55) return -2; /* 帧头不对,丢弃 */ out->cmd = buf[2]; out->len = (buf[3] << 8) | buf[4]; /* 大端长度 */ if (buf_len < 7 + out->len) return -3; /* 数据没收全 */ memcpy(out->data, &buf[5], out->len); uint16_t crc_recv = (buf[5 + out->len] << 8) | buf[6 + out->len]; uint16_t crc_calc = crc16(buf, 5 + out->len); /* 对帧头到数据末尾算 CRC */ if (crc_recv != crc_calc) return -4; /* 校验失败,请求重传 */ return 0; }crc16用的是 Modbus 多项式 0xA001,这个在嵌入式里最常用,查表法或直接计算都行。返回负数表示不同类型的错误,上层根据错误码决定是丢弃还是请求重传。实际调试时我一般会在 PC 端工具里把每包 CRC 打出来,和单片机算的对一下,能快速定位是发送端算错了还是接收端解析错了。
3. 跑通升级流程:从编译配置到串口发第一个包
3.1 Bootloader 和 App 的 Keil 工程配置差异
这套源码给了两个 Keil 工程,一个 Bootloader 一个 App。Bootloader 的 IROM1 起始地址设 0x08000000,大小 0x4000(16KB)。App 的 IROM1 起始地址必须改成 0x08004000,大小相应减少 16KB。很多人编译 App 后直接烧进去发现不运行,就是忘了改这个起始地址,中断向量表还按 0x08000000 链接,跳转后取向量全错。另外 App 的system_gd32f10x.c里如果开了VECT_TAB_OFFSET,要设成 0x4000,和 IROM 起始地址保持一致。
/* system_gd32f10x.c —— App 工程里必须改这一行 */ #define VECT_TAB_OFFSET 0x4000 /* Bootloader 占 16KB,App 从 0x4000 开始 */这个宏最终会写到 SCB->VTOR 里,和 Bootloader 跳转时设的 VTOR 是同一个寄存器。两边设成一样才不会出问题。Bootloader 工程里这个值保持 0x0 不动。
3.2 生成 bin 文件并做固件校验
Keil 默认生成的是 axf 文件,IAP 需要的是纯二进制 bin。在 Keil 的 User 选项卡里加一条 fromelf 命令:fromelf --bin --output=app.bin .\Objects\app.axf。生成的 bin 文件大小就是实际固件大小,传输前 PC 端工具会算一个总 CRC 放在“结束升级”命令里,Bootloader 收完后对整片 App 区算一次 CRC 做比对,一致才写“升级完成”标志。这一步是最后一道防线,少了它,传输过程中丢一包但长度对得上,固件就是坏的。
# Keil 的 After Build 命令里加这一行,自动生成 bin fromelf --bin --output=.\Output\app.bin .\Output\app.axf--bin表示输出纯二进制,--output指定路径。如果工程里用了分散加载文件,bin 的起始地址就是加载域地址,不用额外处理。生成后可以用 J-Flash 或 GD32 的下载工具先手动烧一次验证 App 本身能跑,再测 IAP 流程。
3.3 串口升级的完整交互时序
上电后 Bootloader 先初始化串口(一般 115200-8-N-1),然后等 3 秒。这 3 秒是留给 PC 端发“进入升级”命令的窗口,超时没收到就跳 App。收到命令后回一个 ACK,PC 开始按包发数据,每包 Bootloader 校验 CRC 后回 ACK 或 NAK。全部发完 PC 发“结束升级”,Bootloader 算整片 CRC,通过则写标志、回 ACK、跳 App;不通过则回 NAK,PC 重发。这个时序里最容易翻车的是等待窗口太短,PC 端工具还没打开串口,Bootloader 已经跳 App 了。我一般把等待时间设成 5 秒,或者在 App 里也留一个“软升级”命令,收到后写标志、软复位,这样就不用抢上电那几秒。
/* main.c —— Bootloader 主循环的等待逻辑 */ #define WAIT_UPGRADE_MS 5000 int main(void) { uint32_t tick = 0; boot_init(); /* 时钟、串口、Flash 初始化 */ if (read_upgrade_flag() == FLAG_DONE) { jump_to_app(APP_ADDR); /* 上次升级成功,直接跳 */ } while (tick < WAIT_UPGRADE_MS) { if (recv_upgrade_cmd()) { /* 收到进入升级命令 */ do_upgrade(); /* 进入接收循环,不返回 */ } delay_ms(10); tick += 10; } jump_to_app(APP_ADDR); /* 超时,跳 App */ }do_upgrade里是一个死循环,直到升级成功或失败才退出。失败的话一般让设备复位重来,不要留在 Bootloader 里,否则现场设备就“假死”了,看门狗都会被你喂得没脾气。
4. 避坑与排查:IAP 调试中最容易翻车的五个点
4.1 跳转后 App 不运行,串口无输出
现象:Bootloader 打印跳转成功,但 App 的串口没任何输出,仿真器也连不上。原因通常是 App 的向量表偏移没设对,或者跳转前没关中断导致 HardFault。解决:先确认 App 工程 IROM 起始地址和VECT_TAB_OFFSET都是 0x4000,然后在跳转函数里加__disable_irq(),跳转前把外设(尤其是串口和定时器)反初始化,避免 App 初始化时重复配置出问题。
4.2 升级到一半断电,设备变砖
现象:传输过程中断电,重新上电后 Bootloader 和 App 都不跑。原因:升级标志页和 App 区被同时擦除,标志页空了,Bootloader 读到的标志是 0xFFFFFFFF,既不跳 App 也不进升级。解决:标志页单独擦写,且只在收到“结束升级”且 CRC 通过后才写“完成”标志。升级过程中标志保持“进行中”,上电看到“进行中”就强制进升级模式,等 PC 重发。
4.3 CRC 校验总是不通过
现象:PC 端算的 CRC 和 Bootloader 算的对不上,每包都 NAK。原因:CRC 计算范围不一致,PC 端可能把帧头也算进去了,或者字节序搞反了。解决:明确约定 CRC 只对“命令字+长度+数据”计算,帧头不参与;CRC 结果高字节在前。调试时把两边算的 CRC 值都打印出来,一眼就能看出是范围问题还是字节序问题。
4.4 升级后 App 跑起来但功能异常
现象:升级成功,App 能跑,但某些功能不对,比如定时器周期变了、串口波特率不对。原因:Bootloader 里初始化过的外设没有反初始化,App 再初始化时寄存器状态不对。解决:跳转前调用usart_deinit()、timer_deinit()等库函数把用过的外设复位,或者干脆在跳转前执行一次NVIC_SystemReset(),让 App 从干净状态启动。后者更省事,但要注意复位后标志位还在,别又进升级了。
4.5 串口接收丢包严重
现象:传输大文件时频繁 NAK,重传率很高。原因:Bootloader 接收缓冲区太小,或者串口中断优先级太低被其他中断打断。解决:把接收缓冲区开到 2KB 以上,串口中断优先级设成最高,接收用 DMA + 空闲中断的方式,一次收一包再处理。另外 PC 端发送时每包之间加 10ms 延时,给单片机留出擦写 Flash 的时间,Flash 擦写期间 CPU 是停住的,串口来不及收下一包。
5. 二次开发:把串口升级改成 CAN 或无线,以及双区备份的思路
这套源码的通信层和 Flash 操作层是分开的,protocol.c只负责解析帧,flash_if.c只负责擦写,中间通过一个upgrade_ops结构体对接。想改成 CAN 升级,只需要实现can_send和can_recv两个函数,替换掉串口的收发,协议层和 Flash 层一行不用动。我一般会把这个结构体定义成一个函数指针表,初始化时根据宏选择串口还是 CAN,这样同一套 Bootloader 能适配不同硬件版本。
/* upgrade_ops.h —— 通信抽象层 */ typedef struct { int (*init)(void); int (*send)(uint8_t *buf, uint16_t len); int (*recv)(uint8_t *buf, uint16_t len, uint32_t timeout_ms); } upgrade_ops_t; extern upgrade_ops_t uart_ops; extern upgrade_ops_t can_ops; /* main.c 里根据编译宏选择 */ #if defined(USE_CAN) upgrade_ops_t *ops = &can_ops; #else upgrade_ops_t *ops = &uart_ops; #endif双区备份是另一个值得改的方向。现在的方案是单区覆盖,升级失败就得重来。如果 Flash 够大(GD32F103 有 256KB 或 512KB 的型号),可以划成 Bootloader + AppA + AppB 三个区,升级时写 AppB,写完后把标志指向 AppB,下次上电跳 AppB。AppA 保留作为回滚版本,新固件跑不起来还能退回去。实现上就是在标志页里多存一个“当前活动区”字段,跳转函数根据这个字段决定跳哪个地址。这个改动不大,但现场维护的容错能力提升明显。
最后说一个我踩过的坑:改通信层的时候,别忘了改 PC 端工具的超时时间。串口 115200 下传 100KB 固件大概 10 秒,CAN 500K 下可能要 30 秒以上,PC 端如果还按串口的超时等,会误判升级失败。从那以后我每次改传输介质,都先把 PC 端工具的超时参数和重传次数重新算一遍,再上设备实测。希望帮到你。
本文还有配套的精品资源,点击获取