从零实现 STM32F103 串口 IAP 固件升级系统(Boot + APP + 上位机全流程)
最近做了一个 STM32 在线升级(IAP)项目,从零实现了完整方案:Boot 引导程序、App 应用程序、Python 上位机三个部分,全链路实测可用。本文把整个系统的设计思路、通信协议、可靠性和踩过的坑都整理出来,希望能给想学 IAP 的朋友一些参考。
一、项目能做什么
STM32 程序写进 Flash 后,要更新怎么办?传统做法是用 ST-Link/J-Link 重新下载。但如果产品已经装到现场,总不能每台都拆开接下载器吧?
IAP(In-Application Programming,在应用中编程)就是解决这个问题的:程序自己给自己升级。只需要第一次用下载器烧 Boot,之后 App 的所有更新都通过串口完成——“第一次程序靠下载器,之后全靠串口”。
本项目的三个部分:
- Boot 引导程序:上电判断是否进入升级模式,决定收新固件还是跳转现有 App
- App 应用程序:正常产品程序,可通过串口收升级指令,主动请求进 Boot 升级
- Python 上位机:图形界面,选固件文件一键升级,实时显示 App 运行状态
二、系统架构
┌─────────────┐ 0xAF 升级请求 ┌──────────────────┐ │ Python 上位机 │ ───────────────────▶ │ App 应用程序 │ │ (GUI) │ │ (0x08004000) │ │ │ │ 收指令→写标志→复位 │ └──────┬───────┘ └─────────┬────────┘ │ IAP 协议帧 (115200) │ 软复位 │ ▼ │ ┌─────────────────────────┐ └────────────────────▶│ Boot 引导程序 │ │ (0x08000000) │ │ 查 Config 标志决定 │ │ 进升级 or 跳 App │ └────────────┬────────────┘ │ 升级完成跳转 ▼ 新 App 运行Flash 内存布局(STM32F103C8, 64KB)
0x08000000 ┌──────────────────┐ │ Boot (16KB) │ 引导程序 0x08004000 ├──────────────────┤ │ App (47KB) │ 应用程序(可被升级) 0x0800F800 ├──────────────────┤ │ Config (1KB) │ 升级标志 + 固件信息(防变砖) 0x08010000 └──────────────────┘三、通信协议
帧格式(发送)
┌──────┬──────┬──────────┬──────┬──────────────┬────────┬────────┐ │ 0xAA │ 命令 │ 数据长度 │ 序号 │ 数据... │ CRC16低│ CRC16高│ └──────┴──────┴──────────┴──────┴──────────────┴────────┴────────┘- 帧头:
0xAA - CRC16:MODBUS 算法,初值
0xFFFF,多项式0xA001,对"命令~数据"计算,小端存放 - 波特率:115200,8N1
命令表
| 命令 | 命令码 | 数据 | 说明 |
|---|---|---|---|
| 握手 READY | 0x0A | 无 | 进入升级模式 |
| 擦除 ERASE | 0x0B | 无 | 擦除整个 App 区 |
| 写数据 WRITE | 0x0C | 数据(≤128B) | seq 从 0 递增,地址 = APP_BASE + seq×128 |
| 校验 VERIFY | 0x0D | 总长度(4B LE) + CRC32(4B LE) | 对写入固件整包校验 |
| 跳转 JUMP | 0x0E | 无 | 校验通过后跳转 App |
回复帧格式
┌──────┬──────────┬──────┬────────┬────────┐ │ 0xBB │ 状态码 │ 序号 │ CRC16低│ CRC16高│ └──────┴──────────┴──────┴────────┴────────┘状态码
| 状态码 | 含义 |
|---|---|
0xA0 | 成功 |
0xB0 | 重复包(序号相同) |
0x01 | CRC 校验错误 |
0x03 | 序号不连续(丢包) |
0x04 | 状态错误(当前状态不允许此操作) |
升级时序
上位机 Boot │ 0x0A 握手 │ ├─────────────────────▶│ 状态→READY │ BB A0 成功 │ │◀─────────────────────┤ │ 0x0B 擦除 │ ├─────────────────────▶│ 擦除 App 区,状态→ERASED │ BB A0 成功 │ │◀─────────────────────┤ │ 0x0C 数据帧(seq=0) │ ├─────────────────────▶│ 写 Flash 偏移 0 │ BB A0 成功 │ │◀─────────────────────┤ │ 0x0C 数据帧(seq=1) │ ├─────────────────────▶│ 写 Flash 偏移 128 │ BB A0 成功 │ │◀─────────────────────┤ │ ... │ │ 0x0D 校验 │ ├─────────────────────▶│ 硬件 CRC32 比对,状态→VERIFIED │ BB A0 成功 │ │◀─────────────────────┤ │ 0x0E 跳转 │ ├─────────────────────▶│ 清标志 → 跳转 App │ ▼ │ App 开始运行四、可靠性设计
4.1 Config 标志页(防变砖)
Flash 最后一页(0x0800F800)保存升级标志:
typedefstruct{u32 magic;// CONFIG_MAGIC,判断配置有效性u32 upgrade_flag;// UPGRADE_FLAG,是否请求升级u32 reserve[3];// 预留}Config_t;- 只有升级成功、跳转前才清标志
- 升级中断电/失败 → 标志还在 → 下次上电 Boot停在升级模式等你重发,不会变砖
- 已实测:升级中断电 2 次,重新上电均可恢复重来
4.2 关键设计点
- 中断 + 环形缓冲接收:STM32 USART 无 FIFO,慢轮询会丢字节。中断每收一字节入环,主循环取走处理
- 跳转前善后:关全局中断、清 NVIC、停 SysTick、复位时钟,再设 MSP 跳转
FLASH_ProgramWord强制写魔数:避免config_read首次读到擦除态0xFFFFFFFF时把 magic 清 0,导致标志永远不生效- CRC16 帧校验 + CRC32 整包校验双保险
4.3 通道抽象(transport 接口)
协议层不直接操作串口,而是通过统一的transport接口收发包。好处:以后接 WiFi/4G 远程升级时,只需新增一个 transport 实现,协议层零改动。
typedefstruct{void(*init)(void);/* 初始化通道 */void(*send)(uint8_t*buf,uint16_tlen);/* 发送一帧 */uint8_t(*recv)(uint8_t*buf,uint32_ttimeout);/* 收一字节, 超时返回0 */}transport_t;voidtransport_set(transport_t*t);/* 注册当前通道 */transport_t*transport_get(void);/* 获取当前通道 */recv必须带超时参数:让上层能感知"通道暂时无数据"。当前实现transport_uart.c(USART1),未来加transport_wifi.c即可切换。
五、疑难问题排查(踩坑记录)
| 现象 | 原因 | 解决 |
|---|---|---|
| 握手只收到 1~2 字节 | USART 无 FIFO + 慢轮询 + 阻塞 printf | 改用中断+环形缓冲区 |
写数据后多回0xB0重复包 | 三个if未用else if,seq自增后误判重复 | 改成else if链 |
擦除后写数据被拒(0x04) | 擦除后未设STATE_ERASED | 擦除成功设状态 |
| 空片/断电损坏后无法恢复 | App 无效时iap_load_app返回后死循环 | 兜底进升级模式 |
| config 标志写不进去 | config_read首次读到擦除态把 magic 清 0 | config_write强制写魔数 |
| 断电后上位机报"拒绝访问" | 串口句柄失效未重连 | 上位机加断线自动重连 |
| PC13 LED 不闪 | 用了 CRL 而非 CRH、ODR 位号写错 | PC13 在 CRH,位 13 |
六、移植到其他芯片(如 RCT6)
| 项 | C8T6 | RCT6 |
|---|---|---|
| Flash | 64KB | 256KB |
| 页大小 | 1KB | 2KB |
| RAM | 20KB | 48KB |
| Flash 下载算法 | Med-density | High-density |
只需改动 3 处,协议/上位机/App 逻辑零改动:
flash.h:FLASH_PAGE_SIZE1024 →2048flash.h:CONFIG_ADDR0x800F800 →0x803F800(256KB 最后一页)- Keil:APP 工程
IROM1尺寸放大,Flash 下载算法选 High-density
APP_BASE和VTOR仍是0x08004000,不用改——当初用"偏移量 + 宏"设计就是为了移植时收网。
总结
这套 IAP 方案麻雀虽小五脏俱全:Boot 跳转、Flash 擦写、自定义协议、状态机、上位机、可靠性设计、通道抽象都有。个人认为最有价值的是那几个坑——它们都是真实踩过、花时间排查出来的,能帮你少走很多弯路。
完整代码已开源,两个平台内容同步,欢迎 Star:
- GitCode(国内访问快):👉 https://gitcode.com/2402_85030385/IAP
- GitHub(国际访问):👉 https://github.com/wenroudezhizi/IAP
如果文章对你有帮助,欢迎点赞、评论、收藏!有问题也欢迎在评论区交流。