@[toc]
在这里插入图片描述

一、引言:为何需要差分升级?

在物联网(IoT)设备的生命周期中,固件空中升级(FOTA)是不可或缺的一环。然而,传统的全量包升级方式意味着每次都需要传输完整的固件,这不仅消耗了大量的网络带宽,延长了升级时间,还对设备的Flash存储空间提出了更高的要求。

**差分升级(Differential Update)**通过只传输新旧固件之间的“差异”部分(即补丁包),极大地减小了升级包的体积,完美地解决了上述痛点。本文将详细介绍如何在RT-Thread操作系统中,利用QBoot引导加载程序和轻量级差分库HPatchLite,构建一套完整、高效且资源可控的差分升级方案。

核心组件架构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
graph TD
subgraph "设备端 (RT-Thread)"
A["QBoot (Bootloader)"]
B["FAL (Flash抽象层)"]
C["hpatchlite-wrapper (差分合并引擎)"]
end

subgraph "云端/PC端"
D["hdiffi (差分工具)"]
E["package_tool.py (打包工具)"]
F["新旧固件.bin"]
G["下载服务器"]
end

F -- "输入" --> D
D -- "生成 patch.bin" --> E
E -- "打包成 patch.rbl" --> G
G -- "下载升级包" --> A

A -- "调用" --> C
C -- "读写Flash" --> B

style C fill:#f9f,stroke:#333,stroke-width:2px

图1:差分升级方案的整体架构


二、工作流程:从制作补丁到设备升级

整个差分升级流程分为两大阶段:在PC端制作差分包,以及在设备端执行升级。

阶段一:在PC端制作QBoot差分升级包 (.rbl)

这一阶段的目标是生成一个QBoot可以识别和解析的、包含差分信息的升级包。

步骤 1: 生成原始差分补丁 (patch.bin)

  1. 下载工具: 从 HPatchLite官方GitHub仓库 下载预编译的二进制工具包 (HPatchLite_vX.X.X_bin_xxxx.zip) 并解压。

  2. 准备文件: 准备好 old.bin (设备当前固件) 和 new.bin (新版固件)。

  3. 执行命令: 使用 hdiffi.exe 工具生成原始差分数据。

    1
    2
    # hdiffi.exe <旧固件> <新固件> <输出的原始补丁>
    .\hdiffi.exe old.bin new.bin patch.bin

    (可选)为了进一步减小补丁体积,可以启用tuz压缩算法。这对于带宽受限的设备至关重要。

    1
    2
    # -c-tuz 参数启用了tuz压缩
    .\hdiffi.exe -c-tuz old.bin new.bin patch_compressed.bin

步骤 2: 将原始补丁封装为QBoot格式 (.rbl)

QBoot需要一个带有标准头部信息的升级包。我们使用配套的Python脚本 package_tool.py 来完成封装。

1
2
# python package_tool.py <原始补丁> <新固件>
python .\package_tool.py patch.bin new.bin

关键点解析

  • 第一个参数 (patch.bin) 是上一步生成的原始补丁,它将成为.rbl文件的数据主体。
  • 第二个参数 (new.bin) 不会被打包.rbl文件。它仅用于计算RBL头部所需的元数据,如新固件的总大小 (raw_size) 和CRC校验值 (raw_crc),供QBoot在升级后进行校验。

执行成功后,生成的 patch.rbl 就是我们可以下发给设备的最终升级包。


三、核心机制:原地升级(In-Place Update)与缓冲策略

差分升级最棘手的问题在于“原地升级”:我们需要从app分区读取旧数据,同时又要向同一个app分区写入新数据。由于Flash的物理特性(通常不能在读取一个扇区的同时擦写它),我们必须借助一个**缓冲区(Swap/Buffer)**来完成这个“左右手互搏”的操作。

QBoot与hpatchlite-wrapper巧妙地提供了两种缓冲策略,开发者可以根据设备的RAM和Flash资源进行权衡选择。

1
2
3
4
5
6
7
8
9
10
11
graph TD
subgraph "原地升级挑战"
App["app分区"]
App -- "需要读取旧数据" --> Algo["差分合并算法"];
Algo -- "需要写入新数据" --> App;
subgraph "矛盾点"
style Conflict fill:#ff9999
Conflict["无法在同一Flash扇区<br/>同时进行读和写/擦除操作"];
end
App <--> Conflict;
end

图2:原地升级的内在矛盾

策略一:使用Flash作为缓冲区 (Flash Swap)

此策略在RAM资源极其宝贵时使用。它会利用一个独立的Flash分区作为临时周转空间。

  • Kconfig配置: QBOOT_HPATCH_USE_FLASH_SWAP
  • 工作流程:
    1. 当需要修改app分区的某个扇区S时,首先将S的原始内容拷贝swap分区。
    2. 差分算法从swap分区读取旧数据,结合补丁数据,生成新的数据。
    3. 擦除app分区的扇区S
    4. 将新生成的数据从RAM写回app分区的扇区S
  • 优点: 极大地节省了宝贵的RAM。
  • 缺点: 速度相对较慢(涉及多次Flash读写),对Flash的磨损也更大。
策略二:使用RAM作为缓冲区 (RAM Buffer)

此策略在RAM资源相对充足时使用,速度更快。

  • Kconfig配置: QBOOT_HPATCH_USE_RAM_BUFFER
  • 工作流程:
    1. 当需要修改app分区的某个扇区S时,直接将S的原始内容读入一个较大的RAM缓冲区。
    2. 差分算法直接在RAM中,从这个缓冲区读取旧数据,生成新数据。
    3. 擦除app分区的扇区S
    4. 将RAM缓冲区中的新数据写回app分区的扇区S
  • 优点: 速度快,对Flash的磨损小。
  • 缺点: 消耗的RAM较多。注意:此RAM缓冲区的大小必须大于等于app分区所在Flash的最小擦除单元(Sector Size)。

四、设备端配置实战

1. FAL分区规划

一个合理的FAL分区规划是成功的前提。

1
2
3
4
5
6
7
8
/* 示例 fal_cfg.h,Flash扇区大小为128KB */
#define FAL_PART_TABLE \
{ \
{FAL_PART_MAGIC_WORD, "app", "onchip_flash_128k", 0, 3*128*1024, 0}, \
{FAL_PART_MAGIC_WORD, "factory", "onchip_flash_128k", 3*128*1024, 2*128*1024, 0}, \
{FAL_PART_MAGIC_WORD, "swap", "onchip_flash_128k", 5*128*1024, 1*128*1024, 0}, \
{FAL_PART_MAGIC_WORD, "download", "onchip_flash_128k", 6*128*1024, 1*128*1024, 0}, \
}
  • app: 差分升级的目标分区。
  • download: 用于存放下载的 patch.rbl 升级包。
  • swap: 如果使用Flash Swap策略,此分区必须存在,且其大小至少要等于app分区的擦除粒度(本例中为128KB)。
2. Kconfig配置

在menuconfig中启用并配置HPatchLite功能。

1
2
3
4
5
6
7
RT-Thread online packages --->
tools packages --->
Qboot: A cross-platform MCU bootloader framework --->
[*] using hpatch-lite decompress
[ ] Use a FLASH partition as swap buffer ---> # 策略一
[*] Use a RAM buffer (Faster, consumes RAM) # 策略二
(4096) RAM buffer size for HPatchLite (must be >= sector size)
3. rtconfig.h 宏定义示例 (对应RAM Buffer策略)
1
2
3
4
5
6
7
8
9
#define PKG_USING_QBOOT
#define QBOOT_APP_PART_NAME "app"
#define QBOOT_DOWNLOAD_PART_NAME "download"
// ... 其他QBoot配置 ...

/* --- HPatchLite 关键配置 --- */
#define QBOOT_USING_HPATCHLITE
#define QBOOT_HPATCH_USE_RAM_BUFFER
#define QBOOT_HPATCH_RAM_BUFFER_SIZE 4096

五、深入底层:hpatchlite-wrapper 的工作原理

hpatchlite-wrapper 是连接QBoot上层逻辑与HPatchLite核心算法的桥梁。其核心API hpi_patch 通过回调函数机制,实现了高度的灵活性和平台解耦。

1
2
3
4
5
6
7
8
9
/**
* @brief 使用 HPatchLite 库执行差分合并操作
*/
hpi_patch_result_t hpi_patch(hpatchi_listener_t *listener, // 用户上下文
int patch_cache_size,
int decompress_cache_size,
hpi_TInputStream_read _do_read_diff, // 读补丁回调
read_old_t _do_read_old, // 读旧固件回调
write_new_t _do_write_new); // 写新固件回调

在QBoot的实现中,_do_read_old_do_write_new 函数内部就包含了对上述**缓冲策略(RAM或Flash)**的复杂处理逻辑,从而对hpi_patch核心算法屏蔽了底层细节。

特别注意:RAM占用
当使用压缩补丁时,hdiffi工具的输出日志会包含一行至关重要的信息:
requirements memory size: (must) 27065 + (custom cache) 32768

  • (must) 27065: 这是解压器(如tuz强制要求的、必须通过hpi_malloc成功分配的RAM大小。它由补丁数据本身决定。
  • (custom cache) 32768: 这是您在调用hpi_patch时传入的patch_cache_sizedecompress_cache_size

您必须确保设备的堆内存足以满足这个 (must) 的要求,否则差分升级会因内存分配失败而终止。

六、结论

通过将QBoot的引导框架、FAL的标准化存储接口与HPatchLite的高效差分算法相结合,RT-Thread开发者可以轻松地为项目集成一套健壮、高效且资源可控的差分升级(FOTA)方案。正确理解并选择适合项目资源的缓冲策略,以及在制作补丁时关注内存需求,是成功实施此方案的关键。这不仅能极大地提升用户体验,还能显著降低项目的长期运营成本。

七、相关链接

https://github.com/sulfurandcu/hpatchlite-wrapper
https://github.com/qiyongzhong0/rt-thread-qboot/blob/master/src/qboot_hpatchlite.c

八、后记

  • hpatchlite-wrapper这个软件包发布快一年了,论坛上一个文章和提问也没有.估计下载的也没几个.我也是知道有hpatchlite这个差分升级组件,才去软件包内搜索才知道居然有人已经实现了这个功能了.
  • 大家发布软件包都推上去连宣传一下也没有,都没人知道什么时候多了一个好用的软件包,是干嘛用的.
  • 或许RTT官方应该在每个软件包发布或者更新的时候,来论坛发一个帖子给这些软件包增加一点曝光度.