好问题 👍
你已经知道了“整包升级”,现在想搞 差分升级。我来给你系统讲解下:


一、什么是差分升级?

👉 定义
差分升级(Delta Update / Binary Patch)就是:
不传完整固件,而是只传“旧版本和新版本之间的差异”,目标机利用差分包 + 旧版本数据,重构出新版本。

举个形象的例子:

  • 整包升级:直接把 整本书的新版本寄给你。
  • 差分升级:只寄给你一份 “勘误表”,你在旧版书上改一改,就得到了新版。

二、为什么要差分升级?

  1. 节省带宽
    比如 rootfs 有 1GB,但新版本只改了几十 MB 文件。差分包可能只有 20~50 MB。
  2. 节省升级时间
    包小 → 下载快 → 升级过程短。
  3. 适合远程 OTA / 弱网环境

缺点:

  • 生成与应用差分需要额外计算,升级流程比整包复杂。
  • 必须保证“基准版本”一致:设备必须正好是差分包要求的版本,否则 patch 失败(可能直接变砖)。

三、差分升级的实现方式

差分升级分为两大类:

1. 文件级差分

  • 针对文件系统内容,比较 文件 A(old) vs A(new),生成差分(如 rsync / bsdiff / xdelta)。
  • 应用时逐文件 patch。
  • 特点:更灵活,适合只更新应用层(例如 /usr/bin/xxx 程序)。

工具:

  • rsync 协议(增量同步)
  • bsdiff/bspatch(对单文件做二进制差分)
  • xdelta(对任意文件做差分,效率比 bsdiff 高)

适用:应用升级 / data 分区升级


2. 镜像级差分

  • 针对整块分区镜像(如 rootfs.img、boot.img),计算二进制差分。

  • 生成差分包 → 设备上应用 patch → 得到完整新镜像 → 写到 rootfs_b。

  • 常用工具:

    • imgdiff(Android OTA 用的工具,对 squashfs/ext4 镜像做差分)
    • xdelta3(通用二进制差分)

适用:系统级升级(rootfs/boot 分区)


四、在 RK3588S + Buildroot 的场景怎么做?

方案 1:整包升级(最简单)

  • 制作新 rootfs.img,通过 USB/OTA 整包替换。
  • 缺点:包大。

方案 2:镜像差分升级(推荐)

  1. PC 上用 xdelta3 对旧 rootfs.img 和新 rootfs.img 生成差分包:

    xdelta3 -e -s rootfs_old.img rootfs_new.img rootfs.patch
    
  2. OTA/USB 传输 rootfs.patch(远小于整包)。

  3. 设备端执行:

    xdelta3 -d -s rootfs_old.img rootfs.patch rootfs_new.img
    
  4. rootfs_new.img 写入 rootfs_b 分区,更新 U-Boot 环境变量,重启。

方案 3:文件级差分(适合应用层)

  • 如果只是应用更新,可以用 bsdiff 生成 app_old → app_new 的 patch,OTA 下发 patch,设备端用 bspatch 更新。
  • 优点:更细粒度,速度快。
  • 缺点:只能解决应用层,无法覆盖整个 rootfs 升级。

五、差分升级的注意事项

  1. 基准版本一致

    • 必须知道设备当前的 rootfs 版本号(比如 hash/版本号),否则 patch 会失败。
    • 通常要在升级包里写明“本 patch 适用于 v1.2 → v1.3”。
  2. 校验

    • 升级前后都要算 hash(SHA256/MD5)校验,保证新 rootfs.img 正确无误。
  3. 回滚机制

    • 差分升级一旦失败,设备要能从旧的 rootfs 分区启动,避免变砖。
  4. 性能问题

    • 生成差分包 → 在 PC 上做,问题不大。
    • 应用差分包 → 设备端 CPU/IO 开销要考虑。RK3588S 性能较强,完全可以。

六、落地建议(你的 Buildroot + USB 升级场景)

  • 如果你们是研发/量产阶段
    → 推荐整包 USB 线刷(省心,包大无所谓)。

  • 如果你们是最终产品 OTA/远程升级
    → 推荐 镜像差分升级(用 xdelta3 或 imgdiff),节省流量。

  • 如果你们的需求只是应用程序频繁更新
    → 推荐 应用层文件级差分,直接更新 /data 下的二进制或资源。


要不要我帮你出一份 基于 Buildroot + RK3588S 的差分升级 Demo(包含 PC 端生成 patch + 设备端应用 patch + USB 传输 + A/B 分区切换的完整流程脚本)?




太好了 — 我把 “Buildroot + RK3588S,走 USB 通道的差分升级(镜像级)”可复现实战 Demo 做成一套你能直接跑的说明和脚本。下面包含:

  • PC 端:如何 生成差分包(xdelta3) + 签名 + 打包成传输用的 .tar(或 .patch)。(thewiki.moe, Cool Fish In Glacier)
  • 设备端:如何 接收(USB Mass Storage / FunctionFS)、验证、合成新镜像、写入非活动槽(A/B)、更新 U-Boot 环境变量并重启(含失败回滚保护)。(opensource.rock-chips.com)
  • 必要的安全/校验步骤(hash 与签名)。
  • 注意事项与调试小技巧。
    如果你想要我把某部分改成“文件级差分(bsdiff)”或改用 imgdiff 的 Android 风格差分,也可以马上给你替换版本。(Android Git Repositories, XDA论坛)

1)设计前提(约定)

  • 设备分区(示例,实际请用你设备的分区表):

    • boot_a /dev/mmcblk0p5(当前)
    • boot_b /dev/mmcblk0p6(备用)
    • rootfs_a /dev/mmcblk0p7(当前)
    • rootfs_b /dev/mmcblk0p8(备用)
  • 设备当前运行 slot 由 U-Boot 环境变量 boot_slot 标识(ab)。设备上有 fw_setenv/fw_printenv(mtd-utils)可设置 U-Boot env。

  • 差分采用 xdelta3(镜像级差分),因通用且速度/体积较好。也可以换 imgdiff。(thewiki.moe, Cool Fish In Glacier)


2)PC 端:生成差分包(示例脚本)

文件:gen_rootfs_patch.sh(在 PC 上运行,假设已有 rootfs_old.imgrootfs_new.img

#!/bin/bash
set -e

OLD_IMG="$1"
NEW_IMG="$2"
OUT_DIR="$3"   # e.g. ./out
PATCH_NAME="rootfs_${OLD_VER}_to_${NEW_VER}.xdelta"

if [ -z "$OLD_IMG" ] || [ -z "$NEW_IMG" ] || [ -z "$OUT_DIR" ]; then
  echo "Usage: $0 <rootfs_old.img> <rootfs_new.img> <out_dir>"
  exit 1
fi

mkdir -p "$OUT_DIR"
# compute version ids (sha256 short)
OLD_VER=$(sha256sum "$OLD_IMG" | awk '{print $1}' | cut -c1-12)
NEW_VER=$(sha256sum "$NEW_IMG" | awk '{print $1}' | cut -c1-12)

PATCH_FILE="$OUT_DIR/rootfs_${OLD_VER}_to_${NEW_VER}.xdelta"
META_FILE="$OUT_DIR/metadata.txt"
BUNDLE="$OUT_DIR/rootfs_patch_${OLD_VER}_to_${NEW_VER}.tar"

echo "Generating xdelta3 patch..."
xdelta3 -e -s "$OLD_IMG" "$NEW_IMG" "$PATCH_FILE"

echo "Writing metadata..."
cat > "$META_FILE" <<EOF
old_hash=$(sha256sum "$OLD_IMG" | awk '{print $1}')
new_hash=$(sha256sum "$NEW_IMG" | awk '{print $1}')
old_ver=$OLD_VER
new_ver=$NEW_VER
tool=xdelta3
EOF

# Sign the patch (optional but recommended)
echo "Signing patch with openssl (use your private key private.pem)"
openssl dgst -sha256 -sign private.pem -out "$PATCH_FILE.sig" "$PATCH_FILE"

echo "Bundling..."
tar -cvf "$BUNDLE" -C "$OUT_DIR" "$(basename "$PATCH_FILE")" "$(basename "$PATCH_FILE").sig" "$(basename "$META_FILE")"

echo "Done: $BUNDLE"

说明:

  • xdelta3 -e -s old new patch 生成 patch。应用用 xdelta3 -d -s old patch new。(thewiki.moe, Reddit)
  • 我用 SHA256 做版本识别(在 metadata.txt 内),设备端会比对 old_hash 与本地实际镜像一致才应用。

3)PC → 设备 的 USB 传输方式(两种选项)

A)USB Mass Storage(虚拟 U 盘):把 .tar 放到设备暴露的 U 盘里(用户拖拽),设备端守护进程检测到文件并启动升级。优点简单。

B)USB FunctionFS + 自定义协议:PC 端写一个小工具(libusb 或 pyusb)把文件上传,设备端通过 FunctionFS 收包(更自动/可加速)。

你之前有兴趣做 FunctionFS 文件传输,这一步可以直接沿用。两者设备端接收后的后续处理相同。


4)设备端:接收后自动化脚本(核心部分)

文件:apply_rootfs_patch.sh(设备 /usr/local/bin,需可执行)

#!/bin/bash
set -e

WORK_DIR="/tmp/upgrade"
BUNDLE="$1"   # /mnt/usb/rootfs_patch_abc.tar
OLD_IMG="/tmp/rootfs_old.img"
NEW_IMG="/tmp/rootfs_new.img"
PATCH="/tmp/rootfs.patch"
META="/tmp/metadata.txt"
SIG="/tmp/rootfs.patch.sig"

mkdir -p "$WORK_DIR"
cd "$WORK_DIR"

# 1. extract bundle
tar -xvf "$BUNDLE"

# 2. read metadata
source metadata.txt   # defines old_hash/new_hash

# 3. locate current active slot and extract its image to compare
CURRENT_SLOT=$(fw_printenv -n boot_slot)
if [ "$CURRENT_SLOT" = "a" ]; then
  CURRENT_ROOT="/dev/mmcblk0p7"   # rootfs_a
  INACTIVE_ROOT="/dev/mmcblk0p8"  # rootfs_b
else
  CURRENT_ROOT="/dev/mmcblk0p8"
  INACTIVE_ROOT="/dev/mmcblk0p7"
fi

# Optional: dump current partition to file for patch base (may need space)
echo "Dumping current rootfs for patch base (this may take time)..."
dd if="$CURRENT_ROOT" of="$OLD_IMG" bs=4M conv=sync status=progress

# 4. verify old hash matches
CALC_OLD_HASH=$(sha256sum "$OLD_IMG" | awk '{print $1}')
if [ "$CALC_OLD_HASH" != "$old_hash" ]; then
  echo "Base image mismatch: expected $old_hash got $CALC_OLD_HASH"
  exit 2
fi

# 5. verify signature (optional)
openssl dgst -sha256 -verify public.pem -signature "$SIG" "$PATCH" || { echo "signature invalid"; exit 3; }

# 6. apply xdelta3 patch to produce new image
xdelta3 -d -s "$OLD_IMG" "$PATCH" "$NEW_IMG"

# 7. verify new hash
CALC_NEW_HASH=$(sha256sum "$NEW_IMG" | awk '{print $1}')
if [ "$CALC_NEW_HASH" != "$new_hash" ]; then
  echo "New image hash mismatch"
  exit 4
fi

# 8. write new image to inactive partition
echo "Writing new image to inactive partition $INACTIVE_ROOT"
# Ideally use block-level tools; dd with sync is used here
dd if="$NEW_IMG" of="$INACTIVE_ROOT" bs=4M conv=fsync status=progress

# 9. set U-Boot env to boot inactive slot next
if [ "$CURRENT_SLOT" = "a" ]; then
  fw_setenv boot_slot b
else
  fw_setenv boot_slot a
fi
fw_setenv upgrade_available 1
fw_setenv bootcount 0

# 10. reboot
echo "Rebooting to new slot..."
reboot

说明要点:

  • dd 把当前分区导出为 rootfs_old.img(作为 xdelta 的基线)。如果你的系统保存中了原始镜像(比如 /opt/images/rootfs_v1.img),就可以用它而不用再 dd。这样更快、也省空间。
  • 一定要做 old_hash 检查,避免把错的基线去 patch 导致产出垃圾镜像。
  • 写入 inactive partition 后用 fw_setenv 切槽并设置 upgrade_available 标志,设备下一次启动从新槽引导并做健康检测。U-Boot 的 bootcount/bootlimit 可配合实现失败回滚。(opensource.rock-chips.com)

5)U-Boot & 回滚策略(设备上)

U-Boot env 建议字段(示例):

boot_slot=a
upgrade_available=0
bootcount=0
bootlimit=3

引导流程(简述):

  1. U-Boot 读取 boot_slot,尝试从对应 slot 启动内核/根。
  2. 内核 / init 程序启动后负责做“健康自检”(比如关键服务能启动、应用返回 OK)并在成功时运行 fw_setenv upgrade_available 0; fw_setenv bootcount 0
  3. 如果内核没确认成功,下一次 U-Boot 会增加 bootcount;当 bootcount>=bootlimit 时,U-Boot 或启动脚本会把 boot_slot 切回另一槽,清 bootcount 并启动旧系统(回滚)。

该机制在很多嵌入式 OTA 系统常用(RAUC 也实现类似行为)。(RAUC)


6)安全性(必须做)

  • 签名:补丁包签名(示例用 openssl),设备端用公钥验证。
  • 双重 hash 校验:校验 old_hash(防误用)、校验 new_hash(防传输损坏)。
  • 写入前检查空间:确保设备有足够临时空间(xdelta 需要输出完整 new.img,可能临时占用与镜像相近空间)。
  • 保障救砖通道:始终保留 loader / maskrom 可刷写接口(用于无法回滚/救砖时)。(opensource.rock-chips.com, wiki.t-firefly.com)

7)性能与优化建议

  • 避免全量 dd 导出:如果设备上保存了“当前镜像的原始副本”,用它当基线会快很多。
  • 分块差分:若镜像很大,可把镜像分成固定大小 chunk(如 64MB),对每个 chunk 做 xdelta 差分并只更新变更的 chunk(复杂度高但流量更低)。Android 的 imgdiff 思路类似。(Cool Fish In Glacier, Android Git Repositories)
  • 并行写入:RK3588S 性能强,可在写入阶段尽量使用 bs=4Mconv=fsync 确保写入完整性。

8)调试流程(常见问题与排查)

  • old_hash 不匹配 → 检查你对基线的定位:是否设备已被改动?是否你拿到的基线版本不是设备当前版本?
  • xdelta 解包失败 → 确认 xdelta3 版本一致(不同版本产生的 patch 兼容性问题极少但可能)并确保没有传输损坏(用 sha256sum)。(thewiki.moe)
  • 写入后无法启动 → 先不要擦除旧槽,使用 loader 模式(rkdeveloptool)线刷来救砖;或使用 UART 查看内核日志定位问题。(opensource.rock-chips.com, Android Git Repositories)

9)如果你要我把 Demo 做成可直接运行的“整套工程”

我可以直接给你(一次性交付):

  1. gen_rootfs_patch.sh(PC 端生成与签名脚本)
  2. usb_send.sh(PC 端:把 bundle 复制到设备的 MassStorage 或通过 libusb 上传的示例)
  3. apply_rootfs_patch.sh(设备端自动化脚本,上面已给出核心)
  4. u-boot env 推荐模板与 fw_setenv 使用示例。
  5. 一个小 README,包含测试步骤(如何在开发板上做 end-to-end 测试)和常见失败恢复方法(如何用 rkdeveloptool 线刷回出厂镜像)。

你要不要我现在就把这些脚本整理成一个压缩包(把脚本全部写好并显示在这里),还是先只要 apply_rootfs_patch.sh 的可改进版本(比如支持 chunked patch / 进度显示)?另外告诉我你偏好哪种 USB 传输方式:Mass Storage(拖拽) 还是 FunctionFS(自动化 + 自写 PC 工具)

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐