STM32 IAP方案实战:从寄存器到上位机,实现双重CRC校验,可实现多方式通信升级

5 阅读6分钟

从零实现 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

命令表

命令命令码数据说明
握手 READY0x0A进入升级模式
擦除 ERASE0x0B擦除整个 App 区
写数据 WRITE0x0C数据(≤128B)seq 从 0 递增,地址 = APP_BASE + seq×128
校验 VERIFY0x0D总长度(4B LE) + CRC32(4B LE)对写入固件整包校验
跳转 JUMP0x0E校验通过后跳转 App

回复帧格式

┌──────┬──────────┬──────┬────────┬────────┐
│ 0xBB │ 状态码   │ 序号 │ CRC16低│ CRC16高│
└──────┴──────────┴──────┴────────┴────────┘

状态码

状态码含义
0xA0成功
0xB0重复包(序号相同)
0x01CRC 校验错误
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)保存升级标志:

typedef struct {
    u32 magic;         // CONFIG_MAGIC,判断配置有效性
    u32 upgrade_flag;  // UPGRADE_FLAG,是否请求升级
    u32 reserve[3];    // 预留
} Config_t;
  • 只有升级成功、跳转前才清标志
  • 升级中断电/失败 → 标志还在 → 下次上电 Boot 停在升级模式等你重发,不会变砖
  • 已实测:升级中断电 2 次,重新上电均可恢复重来

4.2 关键设计点

  1. 中断 + 环形缓冲接收:STM32 USART 无 FIFO,慢轮询会丢字节。中断每收一字节入环,主循环取走处理
  2. 跳转前善后:关全局中断、清 NVIC、停 SysTick、复位时钟,再设 MSP 跳转
  3. FLASH_ProgramWord 强制写魔数:避免 config_read 首次读到擦除态 0xFFFFFFFF 时把 magic 清 0,导致标志永远不生效
  4. CRC16 帧校验 + CRC32 整包校验双保险

4.3 通道抽象(transport 接口)

协议层不直接操作串口,而是通过统一的 transport 接口收发包。好处:以后接 WiFi/4G 远程升级时,只需新增一个 transport 实现,协议层零改动

typedef struct {
    void    (*init)(void);                    /* 初始化通道 */
    void    (*send)(uint8_t *buf, uint16_t len);  /* 发送一帧 */
    uint8_t (*recv)(uint8_t *buf, uint32_t timeout); /* 收一字节, 超时返回0 */
} transport_t;

void transport_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 ifseq 自增后误判重复改成 else if
擦除后写数据被拒(0x04擦除后未设 STATE_ERASED擦除成功设状态
空片/断电损坏后无法恢复App 无效时 iap_load_app 返回后死循环兜底进升级模式
config 标志写不进去config_read 首次读到擦除态把 magic 清 0config_write 强制写魔数
断电后上位机报"拒绝访问"串口句柄失效未重连上位机加断线自动重连
PC13 LED 不闪用了 CRL 而非 CRH、ODR 位号写错PC13 在 CRH,位 13

六、移植到其他芯片(如 RCT6)

C8T6RCT6
Flash64KB256KB
页大小1KB2KB
RAM20KB48KB
Flash 下载算法Med-densityHigh-density

只需改动 3 处,协议/上位机/App 逻辑零改动:

  1. flash.hFLASH_PAGE_SIZE 1024 → 2048
  2. flash.hCONFIG_ADDR 0x800F800 → 0x803F800(256KB 最后一页)
  3. Keil:APP 工程 IROM1 尺寸放大,Flash 下载算法选 High-density

APP_BASEVTOR 仍是 0x08004000,不用改——当初用"偏移量 + 宏"设计就是为了移植时收网。


总结

这套 IAP 方案麻雀虽小五脏俱全:Boot 跳转、Flash 擦写、自定义协议、状态机、上位机、可靠性设计、通道抽象都有。个人认为最有价值的是那几个坑——它们都是真实踩过、花时间排查出来的,能帮你少走很多弯路。

完整代码已开源,两个平台内容同步,欢迎 Star:

如果文章对你有帮助,欢迎点赞、评论、收藏!有问题也欢迎在评论区交流。