AGNOS A/B 升级:sparse 解包与 64 字节尾标记

作者:

在

结论:AGNOS 的 A/B 系统更新是一条「拉取 staging → 标非活动槽不可启动 → 流式解压并解 sparse → 写分区与哈希校验 → abctl 切槽 → 重启后标记成功」的链。校验分两层:system 等非全量校验分区只比较尾部 64 字节 marker,其余分区重算 SHA-256。因此 marker 是性能优化,不是加密签名或全量介质自检。本文为固定源码快照的静态分析,不是可在车辆上照做的刷机手册。

平台辅助函数:设备路径、GPIO 与功放

Paths 按 PC/AGNOS 选择日志、持久化、下载缓存和共享内存路径;LOG_ROOT、COMMA_CACHE、OPENPILOT_PREFIX 可影响部分路径。GPIO 统一列出 panda、GNSS、蜂窝、相机、蜂鸣器和 IMU 中断 GPIO 编号;注释明确某些 _RST_N 名称与实际高有效复位相反,不能按名字推电平。功放侧 Amplifier._set_configs() 对 MAX98089 执行 I2C 读改写,初始化时先全局关闭、配置 DAC/I2S/扬声器、再开启;Amplifier.set_configs() 最多 15 次重试并递增等待,仅捕获 OSError,全部失败返回 False。

省电策略 HardwareComma.set_power_save() 在驻车时关闭功放、大核下线并调整 CPU governor/IRQ,恢复时重新初始化功放;它紧耦合 SoC sysfs 与 IRQ 名称。台架功率工具 read_power()/sample_power() 从固定 I2C hwmon 节点读取瓦特,按 123 Hz 定时采样,这是测量工具,不是 hardwared 中的 PowerMonitoring 驻车电量估计器。

组件 源码入口 行为与移植边界
路径选择 Paths 按 PC/AGNOS 选择日志、持久化、下载缓存与共享内存路径。
引脚表 GPIO 列出各外设 GPIO 编号;_RST_N 名称与实际复位极性可能相反。
功放配置 Amplifier._set_configs() MAX98089 I2C 读改写;失败返回 False,调用方未检查布尔值。
省电策略 HardwareComma.set_power_save() 驻车关功放、大核下线并调整 governor/IRQ。
台架功率工具 read_power()/sample_power() 按 123 Hz 读取 I2C hwmon 瓦特,非驻车电量估计器。

A/B 更新调用链

updated.handle_agnos_update() 只在 AGNOS 版本变化时触发,清除 staging 的一致标记后刷目标 slot;updated 由 manager 作为驻车进程运行。launch_chffrplus.sh 则在启动时先验证/切换已下载的目标系统,或循环调用设备更新器。清单在 common/hardware/comma/agnos.json,system/hardware/comma/agnos.json 是指向它的符号链接,故两处路径并非两份独立配置。

选择槽时 get_target_slot_number() 读取 abctl --boot_slot,选择另一槽;get_partition_path() 按清单 has_ab 拼分区标签。下载解压由 StreamingDecompressor.read() 流式处理 HTTP/XZ,同时计算解压后字节的 SHA-256;unsparsify() 解析 Android sparse 的 raw/fill/don’t-care 三类 chunk。分区写入 flash_partition() 先快检;非全量分区先清尾部 hash 标记,再由 extract_compressed_image() 写设备、核对 raw/hash/大小,成功后回写标记。重试与切槽上,flash_agnos_update() 先 --set_unbootable,仅下载类异常最多重试 10 次;swap() 清 marker 后调用 abctl --set_active。

阶段 核心函数与契约
选择槽与路径 get_target_slot_number() 读 abctl --boot_slot 选另一槽;get_partition_path() 按 has_ab 拼标签。
下载解压 StreamingDecompressor.read() 流式 HTTP/XZ 解压并计算解压后 SHA-256。
稀疏镜像 unsparsify() 解析 Android sparse 的 raw/fill/don’t-care chunk。
分区写入 flash_partition() 快检、清尾标记、写设备并核对 raw/hash/大小、回写标记。
校验策略 verify_partition() 全量校验分区重算 SHA-256;其他分区只比较末尾 64 字节 marker。
重试与切槽 flash_agnos_update() 先 --set_unbootable,下载类异常最多重试 10 次;swap() 清 marker 后 abctl --set_active。

清单中的 hash 指解压后的稀疏输入流,hash_raw 指写入的原始分区数据;对于非稀疏分区两者相同。system 是 sparse 且采用尾部 hash 标记,其余本快照清单分区采用全量哈希。

静态发现的故障边界

unsparsify() 遇到 don’t-care chunk 只 yield b"",extract_compressed_image() 对空字节不推进输出文件位置;用 1 个 4096 字节 don’t-care 块的合成 sparse 输入验证过该函数只输出 0 字节。若发布镜像实际包含此类 chunk,写入布局/大小不会保留空洞,并可能因大小或哈希校验失败——但当前未下载真实镜像,不能断言发布镜像触发了该路径。

swap() 在 abctl 返回不含成功标记的输出时无限循环,源码中没有退避或截止时间;若命令直接抛异常则退出。隔离探针还显示:前两次刷写抛 RequestException、第三次成功时,同一分区被调用三次、前两次各睡 10 秒;刷写抛非下载类 ValueError 时第一次异常直接传播,不进入十次重试;abctl --set_unbootable 假返回码为 1 时仍继续调用刷写,因为源码调用 subprocess.run() 后未检查 returncode。

verify_partition() 对非全量校验分区只信任尾部 marker,无法检测 marker 写成之后发生的实体损坏;隔离探针验证了「载荷已变而尾部仍放预期哈希字符串」时非全量 verify_partition() 返回真、强制全量检查返回假。test_manifest() 只检查 URL 的 HEAD 可达性、内容类型和非稀疏分区的两个 hash 字段相等,不覆盖 sparse 解包、分区写入、切槽或电源中断。

常见问题

AGNOS 更新时断电会怎样?

源码在写入前先 abctl --set_unbootable 标记目标槽不可启动,写入后由 swap() 用 abctl --set_active 切槽,新系统启动后再以 abctl --set_success 标记成功。本文只做静态分析,未做断电注入实板验证;插件化更新服务应把「镜像校验通过」「目标槽可启动」「切槽完成」作为分离状态回执。

为什么有些分区只比较 64 字节尾部标记?

因为 verify_partition() 对 full_check 分区重算指定大小的 SHA-256,其他分区只比较末尾 64 字节 marker,并不每次重读分区实体。这个 marker 是性能优化,无法检测写标记之后的实体损坏。

sparse 镜像里的 don’t-care 块会被保留吗?

源码里 unsparsify() 遇到 don’t-care chunk 只 yield b"",而 extract_compressed_image() 对空字节不推进输出位置;隔离探针用合成输入验证了它只输出 0 字节,所以空洞不会按原布局保留。

A/B 切槽失败会重试吗?

源码中 swap() 在 abctl 输出不含成功标记时无限循环,没有退避或截止时间;隔离探针连续尝试三次、中间无 sleep()。新平台应对切槽失败设置有界重试、告警和恢复策略。

延伸阅读

参考资料

  • 项目 02 —— openpilot-research/AGNOS_PLATFORM_UPDATE_DEEP_DIVE.md(平台辅助组件、A/B 安装与校验策略、sparse/XZ 边界)。
  • 项目 02 —— 同目录探针记录:四项更新控制流、五项 sparse/校验、四项 XZ 流隔离探针结论。

说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。

更新记录

首次发布 2026-10-11 最近核对:2026-10-11

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注