嵌入式 IAP 在线升级-整体方案
flowchart LR %%{init: { "flowchart": { "curve": "basis" } } }%% full_update(全量差分包) diff_update(增量差分包) app(大程序) iap(小程序) dnload(数据暂存区) backup(数据暂存区) runapp(程序运行区) decode(数据解码区) full_update-->app full_update-->iap diff_update-->app diff_update-->iap app-->dnload iap-->dnload dnload--跳转至小程序-->backup backup-.㈣<br>回滚旧程序.->runapp backup--㈠<br>解码升级包-->decode decode--㈢<br>写入新程序-->runapp runapp--㈡<br>备份旧程序-->backup
当前方案
- 支持差分升级(hdiffpatchlite)
- 支持命令交互
- 支持 Y-Modem
- 支持异常回滚
- 支持快速启动
- 空间占用情况(40KB + 1个扇区)
- 目前程序大小为 31.5KB,建议预留 40KB 的空间。
- 另外还需要一个扇区作为引导程序与应用程序的共享空间,唤作「升级控制区」。
- 如果开启快速启动,升级控制区必须放在内部 FLASH 中。
- 如果关闭快速启动,升级控制区可以放在内部 FLASH 中,也可以放在外部 FLASH 中。
程序分工
众所周知,人不能踩着自己的脚上天,应用程序升级亦是如此,所以需要借助 bootloader 来完成自身的替换操作。
分工条目 | 引导程序 | 应用程序 |
---|---|---|
程序解码 | ✔ | |
备份回滚 | ✔ | |
程序替换 | ✔ | |
命令交互 | ✔ | ✔ |
数据存取 | ✔ | ✔ |
数据传输 | 基础的通信协议栈 | 完备的通信协议栈 |
应用程序(大程序)集成完备的通信协议栈,只负责接收、存储和校验升级包。
引导程序(小程序)集成基础的通信协议栈,没有应用程序或应用程序异常时也能进行升级。
存储空间
#1
内部flash存储空间划分 | 内部flash存储空间细分 |
---|---|
引导程序区 | 中断向量表 |
引导程序 | |
升级控制区 | 升级状态 |
应用程序区 | 重定向的中断向量表 |
运行中的旧程序 | |
外部flash存储空间划分 | 外部flash存储空间细分 |
数据暂存区 | 接收到的升级包 备份来的旧程序 |
数据解码区 | 解码后的新程序 |
#2
内部flash存储空间划分 | 内部flash存储空间细分 |
---|---|
引导程序区 | 中断向量表 |
引导程序 | |
升级控制区 | 升级状态 |
应用程序区 | 重定向的中断向量表 |
运行中的旧程序 | |
数据暂存区 | 接收到的升级包 备份来的旧程序 |
数据解码区 | 解码后的新程序 |
#3
内部flash存储空间划分 | 内部flash存储空间细分 |
---|---|
引导程序区 | 中断向量表 |
引导程序 | |
升级控制区 | 升级状态 |
应用程序区 (A) |
重定向的中断向量表 |
上一版的旧程序 | |
应用程序区 (B) |
重定向的中断向量表 |
运行中的旧程序 | |
外部flash存储空间划分 | 外部flash存储空间细分 |
数据暂存区 | 接收到的升级包 |
增量升级
全量升级由于要传输新版程序的完整镜像,因此升级时间通常较长,升级失败的概率也更大。那么能不能只传送差异数据呢?答案是可以。这种技术被称作增量升级/差量升级/差分升级。我个人更喜欢增量升级这个叫法,因为增量一定是差分,但是差分不一定是增量。
增量升级确实降低了传输过程中的数据量,但也带来了版本管理复杂的问题,所以说不能因为有了增量升级,全量升级就不用了。
全量升级 | 完整全量升级 | (✘) |
压缩全量升级 | (✘) | |
差分全量升级 | (✔) | |
增量升级 | 差分增量升级 | (✔) |
控制分区
为了方便实现应用程序与引导程序之间的相互协作,需要单独拿出一个扇区的空间作为升级控制区。
- 如果开启快速启动,升级控制区必须放在内部 FLASH 中。
- 如果关闭快速启动,升级控制区可以放在内部 FLASH 中,也可以放在外部 FLASH 中。
升级控制区的数据结构如下:
typedef enum update_step_t |
升级包头
为了保证在线升级能够顺利进行,除了升级数据以外,我们还要向设备发送一些附加信息,这些附加信息通常被添加至升级文件的头部。
包头长度 | 04B | 从「包头长度」开始计算 |
包头校验 | 04B | 从「数据长度」开始计算 |
数据长度 | 04B | 从「剩余数据」开始计算 |
数据校验 | 04B | 从「剩余数据」开始计算 |
产品代码 | 08B | 避免给错误的设备升级 |
产品代码 | 08B | 避免给错误的设备升级 |
旧程序 LEN 值 | 04B | 0xFFFFFFFF:全量镜像包 0x00000000:全量差分包 0xXXXXXXXX:增量差分包 |
新程序 LEN 值 | 04B | |
旧程序 CRC 值 | 04B | |
新程序 CRC 值 | 04B | |
旧程序 MD5 值 | 16B | |
新程序 MD5 值 | 16B | |
...... | ||
可以按需增加 |
typedef struct update_pack_t |
变长包头
升级文件



如何移植引导程序?
创建一个分支
当前模板工程用的是 vscode 和 eide 插件,请自行搜索 eide 插件的使用方式或参见:官方文档。
基于模板工程创建小程序分支是比较推荐的方式,当然你也可以使用 baseline 中的代码自行创建一个不使用 eide 的工程,比如:Keil、IAR。
修改板级配置(bsp_cfg.h)
配置 FLASH 的 SPIx 号
#define BSP_USING_SPI1
配置 RS485 的 URTx 号
#define BSP_USING_UART1
配置 FLASH 的通信引脚(SPI)
#define FLASH_SPI_SCK_PRT GPIO_PORT_X // SCLK
#define FLASH_SPI_SCK_PIN GPIO_PIN_XX
#define FLASH_SPI_SCK_FNC GPIO_FUNC_X
#define FLASH_SPI_TXD_PRT GPIO_PORT_X // MOSI
#define FLASH_SPI_TXD_PIN GPIO_PIN_XX
#define FLASH_SPI_TXD_FNC GPIO_FUNC_X
#define FLASH_SPI_RXD_PRT GPIO_PORT_X // MISO
#define FLASH_SPI_RXD_PIN GPIO_PIN_XX
#define FLASH_SPI_RXD_FNC GPIO_FUNC_X配置 FLASH 的片选引脚
#define FLASH_CSB_PIN GET_PIN(X, XX)
#define FLASH_CSB_LVL PIN_LOW配置 FLASH 的供电引脚(如果有的话)
#define FLASH_PWR_PIN
#define FLASH_PWR_LVL配置 RS485 的通信引脚(URT)
#define RS485_URT_TXD_PRT GPIO_PORT_X
#define RS485_URT_TXD_PIN GPIO_PIN_XX
#define RS485_URT_TXD_FNC GPIO_FUNC_X
#define RS485_URT_RXD_PRT GPIO_PORT_X
#define RS485_URT_RXD_PIN GPIO_PIN_XX
#define RS485_URT_RXD_FNC GPIO_FUNC_X配置 RS485 的收发引脚
#define RS485_RTS_PIN GET_PIN(X, XX)
#define RS485_RTS_LVL RT_SERIAL_HALF_DUPLEX_TX_HIGH // 高电平发送|低电平发送
#define RS485_RTS_DLY 50 // 切换为发送模式后要延时的时间(微秒)配置 LED 的控制引脚(如果有的话)
#define LED_PIN GET_PIN(X, XX)
配置 WDT 的喂狗引脚(如果有的话)
#define WDT_PIN GET_PIN(X, XX)
修改存储配置(fal_cfg.h)
配置快速启动
#define BOOTLOADER_USING_FAST_MODE
配置存储空间
#define __booter_zone_size ( 40 * 1024) // 引导区(小程序)
#define __update_zone_size ( 8 * 1024) // 控制区
#define __paramt_zone_size ( 16 * 1024) // 参数区
#define __runapp_zone_size (448 * 1024) // 运行区(大程序)
#define __backup_zone_size (448 * 1024) // 暂存区
#define __decode_zone_size (448 * 1024) // 解码区
#define __update_zone_addr (__booter_zone_size)
#define __runapp_zone_addr (__booter_zone_size + __update_zone_size + __paramt_zone_size)
/* partition table */
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WORD, "bootloader", "chipflash", 0, __booter_zone_size, 0}, /* 引导区 */ \
{FAL_PART_MAGIC_WORD, "update", "chipflash", __update_zone_addr, __update_zone_size, 0}, /* 控制区 */ \
{FAL_PART_MAGIC_WORD, "runapp", "chipflash", __runapp_zone_addr, __runapp_zone_size, 0}, /* 运行区 */ /* 原先的旧程序 */ \
\
{FAL_PART_MAGIC_WORD, "backup", "norflash0", 0, __backup_zone_size, 0}, /* 暂存区 */ /* 备份的旧程序 */ /* 下载的升级包 */ \
{FAL_PART_MAGIC_WORD, "decode", "norflash0", __backup_zone_size, __decode_zone_size, 0}, /* 解码区 */ /* 解码的新程序 */ \
}
如何制作升级文件?
制作现场用的全量升级包
在 build 目录下新建一个批处理脚本:
@echo off |
把 hdiffi.exe 和 update_header_v2.exe 拷贝到 ./build 目录下
修改以下变量:
- exepath = hdiffi.exe 和 update_header_v2.exe 所在的路径
- binpath = main.bin 所在的路径
- old = 「上次编译的程序」所在的路径
- new = 「这次编译的程序」所在的路径
- product_code = 实际项目的产品代码(这个产品代码是为了避免在升级时升错产品而设置的)
运行该批处理文件,生成以下文件:
- update.pkg.full.zip.bin 现场用的全量升级包
- update.pkg.diff.zip.bin 测试用的增量升级包
如果想要程序编译完之后自动调用该脚本,达到自动生成全量升级包的目的,请参照下图修改 EIDE 编译选项。
制作现场用的增量升级包
用批处理脚本制作
把旧版程序拷贝到脚本中%binpath%
对应的目录,并重命名为%ver%
对应的名称。
运行上面的脚本,生成以下文件:
- update.pkg.diff.zip.specified.version.bin 现场用的增量升级包
用图形化工具制作
- 选择旧版程序
- 选择新版程序
- 点击生成按钮
如何实现在线升级?
在引导程序中升级
- 使用 USB-485 数据线连接电脑和设备,然后使用串口调试工具打开对应的 COM 口。
- 配置串口和传输协议:115200, 8, 1, N, Y-Modem=1024。
- 敲击键盘上的 TAB 键,引导程序会打印出所有支持的命令。
- 执行 download 命令,引导程序进入 ymodem 接收模式,等待上位机下发升级文件。
- 在 windterm 中单击右键,选择:传输二进制 -> 发送 YModem。
- 选择制作好的升级文件并下发。
- 等待文件传输完毕。
- 收到升级文件后,引导程序开始执行升级流程。
- 待升级流程执行完毕,自动跳转至应用程序。
参考视频:
在应用程序中升级
{FAL, "bootloader", "chipflash", addr, size, 0}, /* 引导区 */ |
- 在应用程序中接收升级文件并存储到 “backup” 分区。
- 校验升级文件,如果是差分升级则还要校验旧程序。
- 如果校验失败,则将 “backup” 分区擦除,结束。
- 如果校验成功,则将 “update” 分区中的升级标识设置为
update_step_verify
。 - 重启进入引导程序,开始执行升级流程。
- 待升级流程执行完毕,自动跳转至应用程序。