简介:面向 STM32F103C8T6 开发者的串口 IAP 在线升级源码包,解决产品现场通过串口完成固件升级时 Bootloader 与 App 跳转的问题,适合有基础嵌入式开发经验、正在规划量产升级方案的工程师学习参考。资源共 430 个文件,压缩包约 2.23MB,涵盖 h/c 核心源码、启动汇编与链接脚本、Keil/IAR 等多种工程文件,以及 hex/bin 固件产物和说明文档,便于直接对照工程结构进行二次移植。其中大量 h/c 为驱动与应用层实现,s/asm 为启动与底层汇编文件,uvproj/ewp/icf/ld 覆盖主流 IDE 与链接器配置,bin/hex 可直接烧录验证,txt/doc 等文本资料可用于查看说明。目前已有 5076 人学习下载。通过工程源码可理解 IAP 升级流程中的分区规划、串口通信协议、Flash 擦写与跳转实现;利用现成工程和编译产物可快速搭建实验环境,结合硬件板卡验证升级逻辑,节省从零开发 Bootloader 的时间。项目标题: STM32F103C8T6实现串口IAP方式升级固件源码包
项目正文: STM32F103C8T6最小系统板,串口IAP升级,Bootloader+App双区方案,串口调试助手发送bin文件,HAL库实现,支持跳转与Flash擦写。
关键词: STM32F103C8T6, 串口IAP, 固件升级, Bootloader, HAL库
摘要描述: 基于STM32F103C8T6的串口IAP固件升级完整源码包,含Bootloader、App工程与上位机操作流程。
做嵌入式开发几年,谁没被“改个BUG就要拆机烧录”折磨过?板子封进外壳、设备挂在墙上、产线已经出货,这时候想升级固件,总不能拿把螺丝刀挨个拆吧。串口IAP就是干这个的——通过Bootloader在应用运行中接收新固件,直接写进Flash,省掉拆机、省掉烧录器,一根串口线就能完成升级。这套基于STM32F103C8T6的串口IAP源码包,就是为解决这个问题而生的完整方案,适合正在做小批量产品、需要远程或本地升级固件的开发者,也适合刚接触IAP想搞懂原理的初学者直接抄作业。源码包里Bootloader工程、Demo App工程、上位机发送脚本全都有,串口助手能直接配合使用,拿到手改个引脚就能跑。
1. 整体设计与功能拆解
1.1 为什么必须用IAP,而不是ISP
很多初学者会有个疑惑:ST-Link烧录不也能写Flash吗?为什么非要多此一举搞个Bootloader?这里先说结论:ISP适合开发调试阶段,IAP适合产品量产后的维护阶段。ISP(In-System Programming)需要烧录器(ST-Link/J-Link)参与,走的是SWD/JTAG接口,本质还是调试器把固件写进Flash,设备不在手边就无能为力。而IAP(In-Application Programming)是程序自己写自己的Flash,只要有一个能接收数据的通道——串口、网口、USB、甚至无线模块——就能完成固件更新。对于已经出货的产品,串口往往还在(哪怕是预留的调试口),一根USB转TTL线就能现场升级,成本极低。
C8T6这颗芯片做IAP还有个天然优势:它属于中容量产品,Flash按1KB/页组织,擦除和写入都很灵活。64KB的Flash,划分成16KB的Bootloader区 + 48KB的App区,Bootloader负责收数据、验数据、写Flash、跳转,App只管跑业务逻辑,两区互不干扰,这是最常见的双区方案。
1.2 源码包的组成与分工
这套源码包里包含三个独立的部分,逻辑非常清晰:
- Bootloader工程:独立Keil工程,烧录在0x08000000起始的Boot区域,上电后先运行。它负责初始化串口、等待升级指令、接收固件数据并写入App区、校验无误后跳转执行App。
- App Demo工程:一个最简单的LED闪烁程序,编译时把ROM起始地址改成0x08004000,通过特定方式响应跳转,验证整个升级链路是否跑通。
- 上位机发送端:可以直接用XCOM、SSCOM这类串口助手发送bin文件,源码包也附带了一个Python脚本,自动完成握手、分包、发送的过程,不用手点。
三个部分各司其职,你不需要在这套代码里同时看到Bootloader和App的main函数混在一起,因为混在一起反而容易搞混跳转逻辑。分开工程编译、分别烧录,排查问题也方便。
2. 核心原理与关键技术点
2.1 Flash分区:地址是一切的起点
STM32F103C8T6的Flash从0x08000000开始,共64KB。分区方案直接决定了后面所有代码里的地址参数,这一步千万别拍脑袋,建议画一张表:
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Bootloader | 0x08000000 | 16KB (0x4000) | IAP升级程序,上电入口 |
| App | 0x08004000 | 48KB (0xC000) | 业务应用,跳转目标 |
| 预留对齐 | 0x08010000 | - | Flash末尾边界 |
选择16KB给Bootloader,是综合考虑后的结果。单纯串口接收、擦写、跳转的逻辑,编译出来一般就3~5KB,16KB留了充足余量,以后加协议、加加密算法也不会拥挤。48KB给App,对于大部分C8T6的项目足够用了,毕竟这颗芯片定位就是中低复杂度应用。
这里要特别提醒:C8T6的Flash页大小是1KB,不是大容量型号的2KB。所以擦除App区时,起点0x08004000开始的第0页对应Bootloader区的第64页,擦除循环要按页数算准确,擦多擦少都会出问题。
2.2 串口升级协议设计
串口IAP的核心是可靠传输。Bootloader和上位机之间必须约定一套清晰的通信协议,否则数据错位、丢包、校验失败会让人疯掉。这套源码包里的协议采用“帧头 + 命令字 + 长度 + 数据 + 校验”的结构:
帧结构: [0xAA 0x55] [CMD] [LEN_L] [LEN_H] [DATA...] [CRC8]- 帧头:固定0xAA 0x55,用于同步,检测到这两个字节才开始解析帧。
- 命令字CMD:0x01表示握手请求,0x02表示擦除App区,0x03表示固件数据帧,0x04表示跳转执行。
- 长度LEN:数据域长度,最大设成1028(1024数据+4地址),一帧刚好一页Flash数据加4字节的写入地址。
- 数据域:对于0x03数据帧,前4字节是32位目标地址,后面紧跟1024字节固件内容。
- 校验CRC8:对整帧做校验,收到后校验不符直接丢弃并回复错误帧,上位机做超时重传。
用过串口助手的都知道,手动分包发送容易出错。所以源码包里的Python脚本做了两件事:把bin文件拆成N个1024字节的包,逐包发送并等待Bootloader的ACK;收到NACK或超时则重发当前包。这套机制看着简单,但实测下来在115200波特率下,48KB固件大约20秒传完,丢包率极低。
2.3 跳转核心代码解读
Bootloader收到0x04命令后执行跳转,跳转不是简单的一条goto,必须处理好三件事:关闭外设和中断、重映射中断向量表、设置主栈指针。直接贴核心代码:
typedef void (*pFunction)(void); void JumpToApp(uint32_t app_addr) { uint32_t stack_addr = *(volatile uint32_t *)app_addr; // 检查栈顶地址是否在RAM范围内,防止跳到空Flash if ((stack_addr & 0x2FFE0000) != 0x20000000) { return; } // 跳转前必备清理:关闭全局中断,去初始化外设 __disable_irq(); HAL_RCC_DeInit(); HAL_DeInit(); SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0; // 中断向量表重映射到App起始地址 SCB->VTOR = app_addr; // 取复位向量,设置MSP,跳转 pFunction jump = (pFunction)(*(volatile uint32_t *)(app_addr + 4)); __set_MSP(stack_addr); jump(); }这段代码里那个“检查栈顶地址是否在RAM范围内”的if判断非常关键,新手最容易漏。如果App区根本没烧录程序,Flash里全是0xFF,此时读取栈顶地址就是0xFFFFFFFF,直接跳转必然HardFault。加上合法性校验,Bootloader就能稳稳待在原地,不会变成砖头。
3. 实操记录:从CubeMX配置到升级跑通
3.1 硬件连接与准备工作
实际操作时,我用的是最常见的STM32F103C8T6最小系统板,一块CH340 USB转TTL模块,几条杜邦线。连接方式很简单:CH340的TXD接板子的PA10(USART1_RX),RXD接PA9(USART1_TX),GND共地。如果你用的是带3.3V输出的CH340模块,也可以给板子供电,但千万别用5V输出给C8T6供电,虽然芯片的VDD是3.3V,但AM1117稳压后一般没事,可万一模块不是线性稳压方案,很容易烧板。所以最稳的做法是板子USB供电,CH340只连TXD、RXD、GND三根线。
软件方面需要准备:Keil MDK(我用的是5.x版本)、STM32CubeMX(可选,直接改工程也行)、XCOM串口助手或源码包内的Python脚本、CH340驱动。说到CH340驱动,Windows 10以上系统通常免驱,但老系统偶尔会识别成未知设备,装上官方驱动就解决了。
3.2 Bootloader工程配置要点
Bootloader工程我用HAL库写的,CubeMX里配置很直观,几个关键参数:
- 时钟:外部8MHz晶振,PLL倍频到72MHz系统时钟。C8T6默认内部HSI也能跑,但串口波特率精度会受影响,115200下长期跑下来会有偶发乱码,所以外部晶振是首选。
- USART1:异步模式,115200-8-N-1,使能全局中断。收发都用中断方式,不用DMA,因为在Bootloader里简化逻辑比追求速度更重要。
- GPIO:PA9、PA10复用为USART1功能,另外拉一个LED指示引脚(我用的PC13,最小系统板上的LED),Bootloader运行时LED慢闪,升级成功跳转后LED快闪,直观区分Bootloader和App哪个在跑。
- 中断优先级:USART1全局中断分到2组优先级,抢占优先级0,子优先级0。这里不要用默认值,因为Bootloader里没有其他中断源,但高优先级能防止和Flash擦写时的延时冲突。
生成工程后,在main.c里补全IAP的完整状态机:上电先初始化串口和LED,然后发送一个“Bootloader Ready”提示串,等待上位机发0x01握手命令。如果没有收到握手,等一个超时时间(我设了2秒)就自动跳转App;如果收到握手,则进入升级流程,擦除App区、逐包接收数据、CRC校验、最后跳转。
3.3 App工程必须修改的两个地方
App工程看起来和普通工程没区别,但有两个致命点不改,升级必失败。第一是ROM起始地址。在Keil的Options for Target -> Target页里,把IROM1的Start改成0x08004000,Size改成0xC000。如果不改,App还是从0x08000000开始,Bootloader跳转过去执行的就是一堆错乱数据。
第二是中断向量表重映射。Cortex-M3内核的中断向量表默认在0x08000000,App启动后如果不搬到0x08004000,一旦发生任何中断(定时器、串口、外部中断),CPU还是会去0x08000000查向量表,拿到的却是Bootloader的中断服务函数,逻辑彻底错乱。HAL库下写法很简单:
// 在main函数最开始,SystemInit之后立即执行 SCB->VTOR = 0x08004000;有人问:为什么不放在SystemInit里?因为SystemInit是启动文件调用的,那时App的main还没运行,放这里也完全可以。我习惯放在main第一行,清晰直观。
App工程编译生成bin文件时,要在Keil的User页加一句用户命令:
fromelf.exe --bin -o ./App/App.bin ./App/App.axf这样每次编译自动生成App.bin,省去手动用fromelf转换的麻烦。
3.4 上位机操作与升级全流程
我用源码包里的Python脚本做升级,流程很顺畅。先启动Bootloader,串口助手能看到“Bootloader Ready”提示,然后运行脚本,指定串口和bin文件路径,脚本自动完成整个流程:
- 打开串口(我这里用的是COM4),发送0x01握手命令。
- Bootloader回复握手成功,然后擦除0x08004000起48KB Flash,擦除期间LED快速闪烁。
- 脚本把bin文件按1024字节分成48个包,逐包发送,每包等待ACK,超时1秒重发。
- 最后一包发完,脚本发送0x04跳转命令。
- Bootloader校验总计接收字节数,一致则跳转,App启动,LED开始快闪,升级完成。
整个流程不用人工介入,中途断了也不怕,重新运行脚本就能从头再来。实测下来,48KB固件在115200波特率下跑完大约18~22秒,稳定性和速度都比较理想。
4. 典型问题排查与避坑实录
4.1 IAP跳转后卡死,HAL_Delay无响应
这是最经典的问题,网上搜“iap跳转后卡死hal_delay”能出来一大堆。原因在于HAL库里有个隐蔽的坑:跳转前调用了HAL_DeInit()重置所有外设,但SysTick的中断使能会被清掉。App启动后HAL_Init又会重新配置SysTick,可如果Bootloader里用了HAL_Delay,跳转前没有把SysTick的计数器和中断彻底关干净,App里的HAL_Delay直接死循环。
我排查时用调试器看App运行位置,发现卡在HAL_Delay的while (uwTick - tickstart < Delay)里。解决方法是跳转前强制清零SysTick并关闭其中断,代码里那三行SysTick->CTRL = 0; SysTick->LOAD = 0; SysTick->VAL = 0;就是这个作用。另外在App的main函数里,优先执行HAL_Init()再设置VTOR。
4.2 App能跑但串口中断不触发
这个问题和4.1同源,都是中断向量表没改对。症状是现象诡异:LED在闪、主循环正常,但一开串口中断就死机或者中断服务函数不进。我遇到过一次是App里用USART2做通信,但没在App工程里重映射VTOR,结果串口中断一进来就跳到Bootloader的USART1中断处理函数里去了,整个逻辑彻底乱套。解决方式很简单,App main里务必加上SCB->VTOR = 0x08004000;一行,且必须在任何中断使能之前。
4.3 串口升级过程中偶发丢包、校验失败
这个问题我排查了很久,最终定位到两个原因。第一是串口线质量:用的杜邦线太长,在1米以上时高速传输容易出现反射,115200波特率下偶尔就是会错几个字节。换成20公分的短线后问题消失。第二是Bootloader接收缓冲区的处理:我在中断里只把数据存入环形缓冲区,主循环里再解析帧,这样即使某帧解析失败也不会丢下一个帧头。如果图省事在中断里直接解析,极容易在擦写Flash时(Flash擦写会暂停内核运行)把后续数据弄丢。
4.4 生成的bin文件无法升级
bin文件本身没问题,但升级后App起不来,这种情况最常见的原因是生成bin时起始地址不对。fromelf命令默认生成的是从0x08000000开始的地址映射,而我们App是从0x08004000开始的,所以必须在App工程配置里让fromelf知道起始地址。Keil的fromelf命令本身不带地址参数,它从axf文件里读取,而axf的地址又是由IROM1的Start决定的,所以只要Start改对了,bin文件就是对的。还有一种可能是把普通hex文件当成bin发送了,hex有地址偏移,结构完全不同,代码里根本不认。所以务必确认发送的是纯二进制bin,不是hex。
4.5 擦除Flash后串口无响应
偶尔会遇到Bootloader擦除App区后,串口怎么发数据都没有反应。这种情况排查下来,多是Flash擦写操作把USART的时钟配置或者GPIO配置给影响了。HAL库的Flash操作会触发PRECISERR或者总线错误,如果中断不安全,可能会导致程序跑飞。解决方法是擦除Flash前,确保串口发送和接收的缓冲区已经准备好;擦除期间关闭串口中断,等擦除完成、USART重新初始化后再恢复通信。我在代码里特意把擦除操作放在了状态机里,擦除前发一个“Erasing”状态提示,擦完再发“Erase Done”,这样上位机等回复时就知道什么时候可以开始发数据了。
5. 源码包扩展思路与个人建议
一套IAP框架跑通之后,你会发现它的价值远不止“能用串口升级固件”这么简单。这个框架往深了扩展可以做很多事:
- 加密升级:在Bootloader里加AES-128解密,bin文件上传前先加密,防止固件被抄板。C8T6主频72MHz,解密48KB固件也就多花几秒,完全可以接受。
- 回滚机制:预留两个App区,A区升级前先把旧固件备份到B区,新固件跑飞了还能自动切回旧版本。代价是App可用空间减半,适合对可靠性要求高的设备。
- 断点续传:记录最后写入的页数和帧号,掉电后重新握手时从断点继续传,不算复杂但极大提升体验。
- 多通道支持:把串口接收改成类似接口,就能很容易扩展到ESP8266的WiFi空中升级、4G模块的远程升级,核心的Flash写入和跳转逻辑完全复用。
我在实际项目里用的就是这套框架改出来的版本,把串口通信层换成自定义协议后,配合一个带4G模块的网关,实现了现场设备的远程固件更新,省下大量售后差旅成本。
最后再分享一个小技巧:调试Bootloader时,别一上来就搞真升级。先在App工程里写个简单的LED翻转程序,Bootloader也做好,然后反复测试升级和跳转,确认链路通畅后再把真实业务代码集成进来。这样把问题隔离在最小范围,定位Bug会快很多。IAP本身的风险就在于“改错一步变砖”,但一旦跑通,它给你的产品带来的维护便利性,绝对值得投入这点时间。
本文还有配套的精品资源,点击获取