From f04667c9247c393023b4a05036cfd75b79fa325f Mon Sep 17 00:00:00 2001 From: bowei Date: Wed, 16 Sep 2026 02:46:29 +0800 Subject: [PATCH] Add shared PetaLinux eMMC and HJX9009 TF production skill --- petalinux-emmc/SKILL.md | 36 ++++ petalinux-emmc/references/board-intake.md | 7 + .../references/hjx9009-profile.json | 14 ++ petalinux-emmc/references/hjx9009-tf.md | 19 ++ petalinux-emmc/scripts/emmc_cache.py | 185 ++++++++++++++++++ 5 files changed, 261 insertions(+) create mode 100644 petalinux-emmc/SKILL.md create mode 100644 petalinux-emmc/references/board-intake.md create mode 100644 petalinux-emmc/references/hjx9009-profile.json create mode 100644 petalinux-emmc/references/hjx9009-tf.md create mode 100644 petalinux-emmc/scripts/emmc_cache.py diff --git a/petalinux-emmc/SKILL.md b/petalinux-emmc/SKILL.md new file mode 100644 index 0000000..9a1b7b8 --- /dev/null +++ b/petalinux-emmc/SKILL.md @@ -0,0 +1,36 @@ +--- +name: petalinux-emmc +description: Build, reuse, deploy, recover, and hardware-validate a PetaLinux production system whose kernel and ext4 rootfs reside on eMMC or removable SD/TF while Zynq boots FSBL and U-Boot from QSPI. Use for PR26-style two-stage production deployment; HJX9009 uses TF instead of nonexistent eMMC. +--- + +# PetaLinux eMMC/TF 正式系统 + +目标是从已经可恢复的 QSPI 过渡 Linux 部署正式 FIT 内核与持久 ext4 根文件系统,完成真实断电启动和读写验收。名称中的 `emmc` 指 PR26 的第二阶段;**介质和控制器由板卡证据决定**,不能从 `/dev/mmcblk0` 推断是板载 eMMC。PL bit→bin 和应用程序发布属于另外的 Petalinux-pl/ps 任务,不得冒称完成。 + +## 板卡和输入 + +若用户尚未指定板卡,先让用户选择:`1. HJX9009 2. emv9.3 3. ipots 4. others`。HJX9009 必须阅读 [references/hjx9009-tf.md](references/hjx9009-tf.md);其 J1 TF 接 PS SD0、没有 eMMC。其余板卡目前没有通过本 skill 验证的正式系统 profile,阅读 [references/board-intake.md](references/board-intake.md),取得原理图/HDF 或等效硬件配置并查实存储、启动、恢复和电源细节后才可烧写;不能套用 HJX9009 的 MIO、分区或桥接。 + +阅读项目 AGENTS/CLAUDE adapter、全局/项目 bmem 和 PR26/Result 原文。将用户交付的当前 Vivado `.hdf`(2018.1)或相应版本 `.xsa`、bit、PS7 初始化、板卡修订、PetaLinux/Vivado/SDK 版本、可恢复镜像与串口/JTAG路径绑定。旧参考工程只择项审阅,不整树覆盖;历史同名 HDF、旧 IP 和密码不是当前输入。现场网络地址、TFTP 绑定和介质容量每次实测,不复制笔记。 + +## QSPI 前置门禁 + +首次安装前**实际证明**目标板已经从 QSPI 冷启动进入可恢复的过渡 Linux:记录 BootROM/U-Boot/QSPI FIT 来源、initramfs、MTD 布局、网络/TFTP、分区和校验工具、已知良好 QSPI 镜像及恢复链。若板上已是正式 TF/eMMC 系统,先证明 QSPI 救援 FIT 仍在且可通过受控回退路径启动,不能要求默认 `bootcmd` 重回 QSPI-only,也不能为了检查而无条件重写现有生产 bootenv。不能以 delivery 文件存在或上次成功代替当前门禁。若过渡/救援系统尚未部署或与当前 HDF/板卡不匹配,调用共享 `petalinux-qspi` skill 完成其构建/部署/验收,然后返回本流程;保留现有生产系统的可恢复配置,该调用不免除板卡输入和写前边界检查。若过渡系统可启动但目标介质尚未正常枚举,先修复/验证检测链,不能通过格式化解决未枚举问题。 + +## 构建和复用 + +采用独立正式工程。保持 `boot media = QSPI`、`BOOT.BIN = 同一硬件交付的 FSBL + U-Boot`,正式 `image.ub` 在经验证的 eMMC/TF BOOT 分区,根文件系统在第二分区 ext4;实际 FIT/启动路径必须与板卡检测条件相容。需要早期桥接 initramfs、软件 CD 旁路或根 UUID 时,把源文件、DT、U-Boot overlay、rootfs 配置和启动参数都算作构建输入,不得仅改环境后仍声称镜像相同。`BOOT.BIN` 不自动包含 PL bit 或 FIT。QSPI kernel 分区中的过渡 `image.ub.qspi` 保持可恢复。 + +先为当前输入生成 `build_inputs.json`,可用 sibling `petalinux-qspi/scripts/qspi_cache.py capture` 的通用指纹捕获接口;HJX9009 使用稳定的 [references/hjx9009-profile.json](references/hjx9009-profile.json) 作 profile 文件。指纹包含 HDF/bit、所有 config/recipe/DT/U-Boot/桥接源、工具版本、BOOT 组成与根文件系统方案;部署时变化的 IP/密码不进指纹。再用 [scripts/emmc_cache.py](scripts/emmc_cache.py) 检查当前指纹与交付的 `BOOT.BIN`、`image.ub`、`rootfs.tar.bz2`、`build_inputs.json`、`summary.json`、`MANIFEST.md` 和 `SHA256SUMS`。构建门禁 G0=输入、G1=HDF 导入、G2=配置、G3=构建打包;仅在检查返回 `reusable=true`、G0–G3 均 PASS、产物与当前硬件输入/启动方案一致时跳过重建。文件存在、mtime、旧 bit 相同或仅有一次启动日志均不足;旧交付缺可核验指纹/清单时补齐证据或重建。复用构建**不能**跳过当前介质和板级部署验收。 + +示例:`python /scripts/emmc_cache.py check --current --delivery `。`summary.json` 中 `build_gates.G0..G3.status`(或 `gates.G0..G3.status`)均须为 `PASS`。若目标板当前已有同哈希 FIT、同根 UUID/内容与同 BOOT,先执行 E4 验收;达到所有门禁可报告“当前部署已是最新”,不必重复分区或烧写。 + +## 顺序门禁 + +1. **E0 前置/只读:** QSPI 门禁;当前 HDF/板号;U-Boot/SDHCI/MTD;介质 CID、容量、分区、挂载;首尾与拟用地址范围的重复只读校验。TF/eMMC 容量可见不证明高地址可写;出现不稳定范围时缩小到经验证范围或换介质,绝不盲用标称容量。计算唯一分区计划、UUID、空间余量、恢复路径和本轮目标文件哈希。 +2. **E1 构建:** 从同一 HDF 生成/审阅 PS7、设备树和 FSBL;配置正式 kernel/SD 驱动、ext4 rootfs 归档与必要 initramfs;构建并解包审计。生成 FIT、rootfs 归档和仅含 FSBL+U-Boot 的 BOOT.BIN,记录 BIF、版本、SHA-256、启动命令与构建日志。不得直接运行旧 `emmc_auto_format.sh`/`auto_up_emmc.sh`:它们写死设备/IP、无严密失败门禁,可能清空分区和 QSPI bootenv。 +3. **E2 介质部署:** 用户的正式系统部署请求包含目标介质重建时,仍先核对 CID/容量/无其它介质、精确设备节点、挂载点与分区计划;写前停用自动挂载并可靠卸载。先把全部产物传输到安全暂存区、检查文件大小/哈希/可用空间,任何传输失败即停止,不允许落入格式化或 Flash 擦除。按已验证范围分区/格式化,回读分区表、fsck;写 FAT BOOT 的 `image.ub` 并读回哈希;将对应 `rootfs.tar.bz2` 解包到 ext4,检查 `/sbin/init`、权限、设备节点及关键文件,sync、卸载、fsck、只读回读。若最终归档与上卡内容有差异,明确记录,不得报告逐字节一致。 +4. **E3 QSPI 启动更新:** 仅在 E2 全通过、旧 QSPI 镜像与 JTAG 恢复可用时,更新必要的 FSBL/U-Boot 或持久 bootcmd。计算 boot/bootenv/kernel 的实际范围;不要清空 bootenv 或覆盖救援 FIT。QSPI 擦写后做**全长回读比较与 CRC/哈希**,环境回读确认;不能只看 `flashcp`/`sf write` 无报错。卡缺失或 FIT 失效时的 QSPI 救援分支必须单独冷启动实测。 +5. **E4 现场验收:** 至少两次完整断电冷启动,串口证明 `QSPI FSBL/U-Boot → 目标介质 FIT → 目标 ext4 /`;核对 FIT hash、CID、根 SOURCE/FSTYPE/UUID、实际网络、MTD、FPGA manager(仅存在不等于 PL 已部署)。做受控文件写入、sync、断电,再读回持久化;检查 ext4/mmc 错误和 fsck。再单独验证无卡/损坏 FIT 的 QSPI 救援路径。某项未完成记为 `PARTIAL`/`BLOCKED`,不称“全功能完成”。 + +所有 U-Boot/串口命令逐条核对退出状态、期望回显、实际 `$filesize` 和 CRC。不能让 TFTP 失败后旧 DDR 数据落入写入步骤。把自动状态/log 放在项目 `Notes/auto/`;manifest 记录门禁、证据路径、板卡/介质身份、输入与产物哈希、实际网络和遗留限制。按项目 bmem 协议导入用户 Notes、沉淀会话和可持久结论并同步;分别报告本地/远端/中心同步状态。 diff --git a/petalinux-emmc/references/board-intake.md b/petalinux-emmc/references/board-intake.md new file mode 100644 index 0000000..59bff71 --- /dev/null +++ b/petalinux-emmc/references/board-intake.md @@ -0,0 +1,7 @@ +# 未验证板卡的正式存储系统输入 + +适用于 emv9.3、ipots、others。先取得板卡版本、原理图或等效板级配置,当前 HDF/XSA、PetaLinux/Vivado 版本,以及 QSPI 过渡系统的门禁结果。emv9.3 历史原理图指出有 eMMC,但仍须查实这块目标板的实际芯片/焊接和读写;ipots/others 无已验证 profile。未验证字段要标 `UNKNOWN`,不能从 HJX9009 借值。 + +必须确认:BootROM 启动源绑带/拨码及重置采样;QSPI 拓扑/容量/擦除尺寸/救援分区;正式存储芯片或卡座、电压/复位/供电、SD 控制器实例、MIO/总线宽度、CD/WP/电压切换;FSBL 初始化;U-Boot 先于 Linux 的介质读取能力;根分区标识;串口/JTAG/恢复途径;现网 TFTP 地址。拿到这些证据后写本板 profile,明确是否需要 CD 旁路、FIT 桥接或不同 BOOT 组成,再运行主门禁。 + +若目标不是 Zynq-7000 或启动链含 PMUFW/ATF/bit 等额外组件,不能沿用 HJX9009 的 `FSBL+U-Boot` 打包规则。若介质无法无损识别,不允许用 `/dev/mmcblk0` 名称或历史容量猜测目标并执行格式化。 diff --git a/petalinux-emmc/references/hjx9009-profile.json b/petalinux-emmc/references/hjx9009-profile.json new file mode 100644 index 0000000..b2e14db --- /dev/null +++ b/petalinux-emmc/references/hjx9009-profile.json @@ -0,0 +1,14 @@ +{ + "schema_version": 1, + "board": "HJX9009", + "soc": "XC7Z100FF900", + "production_storage": "removable-microsd-j1", + "storage_controller": "ps7-sd0", + "storage_mio": {"clock": 40, "command": 41, "data": [42, 43, 44, 45], "detect": 13}, + "bootrom_media": "qspi-dual-cascade", + "boot_composition": ["fsbl", "u-boot"], + "production_kernel_location": "microsd-fat-boot", + "production_rootfs": "microsd-ext4", + "qspi_rescue_fit": true, + "profile_revision": "pr26-tf-20260915" +} diff --git a/petalinux-emmc/references/hjx9009-tf.md b/petalinux-emmc/references/hjx9009-tf.md new file mode 100644 index 0000000..0ea083f --- /dev/null +++ b/petalinux-emmc/references/hjx9009-tf.md @@ -0,0 +1,19 @@ +# HJX9009:用 J1 TF 替代 PR26 的 eMMC + +本 profile 依据项目 `Notes/PR26.md`、`Notes/Task-PR26-1.md`、`Notes/Result-PR26-1.md`、`Petalinux/delivery/pr26_tf_20260915/MANIFEST.md` 以及 `Notes/auto/pr26_qspi_20260915/tf_final_*` 实测记录。每次核对 PCB 版本与当前交付;下面的 CID、容量、IP、镜像字节数和 UUID 均为**历史证据,不是未来部署常量**。 + +## 硬件与启动 + +- XC7Z100FF900;BootROM 启动绑带 `MIO[5:4:3]=100` 选 QSPI。U5/U6 双级联 S25FL128S,历史 `sf probe` 为 `s25fl128s_64k`、32 MiB、128 KiB 擦除。先做现板探测再写任何偏移。 +- 板上无 eMMC。J1 microSD 经 U12 MAX13035E 接 SD0,MIO40=CLK、41=CMD、42..45=DAT0..3,MIO13=`SDIO_SDDET`;SD1 的 MIO46..53 已有以太网/UART/MDIO用途。DT 应启用 `sdhci0`、4 bit、≤50 MHz、`no-1-8-v`、`disable-wp`,禁用 `sdhci1`;不要声明 `non-removable`。 +- 历史 HDF/FSBL 把 `SD0_CD_SEL` 指向 EMIO56,与原理图不符。优先从新 HDF/FSBL修正,不能仅靠无效的 `cd-inverted` 属性。历史 U-Boot `sd0_cd_fix` 把 `0xF8000830` 写为 `0x000D0037`,但 MIO13 曾被复用为 QSPI1 IO3 输出,原生 Card Present 仍为 0。实际完成的正式系统先由 U-Boot 读取 TF FAT FIT,再在早期 Linux 桥接 initramfs 设置 SDHCI CDTEST、枚举 SD 卡、挂载 ext4 并切换 `/sbin/init`。**CDTEST 是这版硬件的受控兼容旁路,不证明卡检测硬件修复;若新版硬件/FSBL 可原生检测,优先删除旁路并做双冷启动验收。** 若需沿用旁路,桥接必须先验证 CID/容量/根分区 UUID,超时和身份不匹配时停止或转救援,不可静默挂错介质。 +- 历史正式 `bootcmd` 为:`run phyfix; if mmc dev 0 && fatload mmc 0:1 0x02000000 image.ub; then run sd0_cd_fix; bootm 0x02000000; else sf probe 0 0 0 && sf read 0x02000000 0x00520000 0x00ED8E98 && run sd0_cd_fix && bootm 0x02000000; fi`。此处 `0x00ED8E98` 是**旧救援 FIT 的实际字节数**,未来必须从当前镜像/布局重算并验证 DDR 安全范围;不得照抄。先证明 TF 分支,再证明无卡/坏 FIT 救援分支;2026-09-15 交付的后一分支未独立实测。 + +## 介质边界与验收的真实结果 + +- 历史卡 15 GiB,CID `05000c61736466672200000012018c00`。原 1 GiB FAT32 BOOT + 14 GiB ext4 在约 8 GiB 及以上访问出现重复读写/文件系统不一致;无法唯一归咎卡或板端。因此最后采用低地址 p1 LBA 2048..2099199(1 GiB FAT32),p2 LBA 2099200..14682111(6 GiB ext4),尾部故意空置。**不要把这个历史保守布局应用于新卡,也不要把旧卡扩回 14 GiB;新介质要独立做拟用范围的重复块测试、fsck 和回读。** +- 历史 `pr26_tf_20260915` 已将新 FSBL/U-Boot 写 QSPI boot,558976 字节写后全长 `cmp.b`/CRC 通过;TF p1 放 `image.ub`,p2 为 ext4 正式根。三次断电启动均显示 FAT FIT 读取、FIT hash 校验、桥接挂载 `/dev/mmcblk0p2`、sysvinit/Dropbear/联网;第三次证实跨断电持久化文件回读。日志含非致命 BusyBox mount/switch_root 与 RTC 警告,不可描述为“零警告”。 +- `rootfs.tar.bz2` 最终归档晚于 p2 写入;历史对照有 23 个构建元数据/包数据库/版本或认证记录差异,因此不能把最终归档宣称为板上逐字节版本。新交付应让上卡内容与冻结归档同源并核对关键文件。 +- 旧 QSPI kernel 救援 FIT 保留在 `0x520000`;2026-09-15 的最终 BOOT 更新后,**无卡实物冷启动回退尚未独立验证**。后续完整 E4 必须补上该门禁。 + +网络历史实测为板 `192.168.1.50/24`、Windows TFTP `192.168.1.126`;Notion 的 `192.168.1.140`/`.66` 是例子。每次由 live interface、TFTP 文件及 SHA-256 重新确认。PL FPGA manager 注册不等于最新 bit 已转换/加载;不要混入此 skill 的正式系统完成状态。 diff --git a/petalinux-emmc/scripts/emmc_cache.py b/petalinux-emmc/scripts/emmc_cache.py new file mode 100644 index 0000000..be2c3b2 --- /dev/null +++ b/petalinux-emmc/scripts/emmc_cache.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +"""Check whether a production PetaLinux build is reusable; never validates target media.""" + +import argparse +import hashlib +import json +import os +import posixpath +import shutil +import sys +import tarfile +import tempfile + +REQUIRED = ("BOOT.BIN", "image.ub", "rootfs.tar.bz2", "build_inputs.json", + "summary.json", "MANIFEST.md") + + +def digest(path): + h = hashlib.sha256() + with open(path, "rb") as stream: + for part in iter(lambda: stream.read(1024 * 1024), b""): + h.update(part) + return h.hexdigest().upper() + + +def fingerprint(obj): + data = dict(obj) + data.pop("fingerprint_sha256", None) + encoded = json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return hashlib.sha256(encoded.encode("utf-8")).hexdigest().upper() + + +def load_json(path): + with open(path, "r", encoding="utf-8") as stream: + return json.load(stream) + + +def checksum_entries(path): + entries = {} + with open(path, "r", encoding="utf-8") as stream: + for lineno, raw in enumerate(stream, 1): + line = raw.strip() + if not line: + continue + parts = line.split(None, 1) + if len(parts) != 2 or len(parts[0]) != 64 or any(c not in "0123456789abcdefABCDEF" for c in parts[0]): + raise ValueError("invalid SHA256SUMS line {}".format(lineno)) + name = parts[1].lstrip("* ").replace("\\", "/") + chunks = name.split("/") + if not name or os.path.isabs(name) or ".." in chunks or name == "SHA256SUMS" or name in entries: + raise ValueError("unsafe or duplicate SHA256SUMS entry: {}".format(name)) + entries[name] = parts[0].upper() + return entries + + +def check(current_path, delivery): + root = os.path.abspath(delivery) + errors = [] + for name in REQUIRED + ("SHA256SUMS",): + if not os.path.isfile(os.path.join(root, name)): + errors.append("missing artifact: " + name) + if errors: + return {"reusable": False, "delivery": root, "errors": errors} + + try: + current = load_json(current_path) + stored = load_json(os.path.join(root, "build_inputs.json")) + for label, obj in (("current", current), ("stored", stored)): + if obj.get("schema_version") != 1 or fingerprint(obj) != obj.get("fingerprint_sha256"): + errors.append(label + " fingerprint invalid") + if current.get("fingerprint_sha256") != stored.get("fingerprint_sha256"): + errors.append("build-defining inputs differ") + except Exception as exc: + errors.append("fingerprint check failed: " + str(exc)) + + try: + sums = checksum_entries(os.path.join(root, "SHA256SUMS")) + for name in REQUIRED: + if name not in sums: + errors.append("artifact absent from SHA256SUMS: " + name) + elif digest(os.path.join(root, name)) != sums[name]: + errors.append("checksum mismatch: " + name) + for name, expected in sums.items(): + path = os.path.realpath(os.path.join(root, name)) + if os.path.commonpath((os.path.realpath(root), path)) != os.path.realpath(root) or not os.path.isfile(path) or digest(path) != expected: + errors.append("invalid checksum entry: " + name) + except Exception as exc: + errors.append("checksum check failed: " + str(exc)) + + try: + summary = load_json(os.path.join(root, "summary.json")) + gates = summary.get("build_gates", summary.get("gates", {})) + for gate in ("G0", "G1", "G2", "G3"): + item = gates.get(gate, {}) + if item.get("status") != "PASS": + errors.append("build gate not PASS: " + gate) + except Exception as exc: + errors.append("build gate check failed: " + str(exc)) + + try: + with tarfile.open(os.path.join(root, "rootfs.tar.bz2"), "r:bz2") as archive: + init = None + for member in archive: + name = member.name + clean = posixpath.normpath(name) + if name.startswith("/") or clean == ".." or clean.startswith("../"): + errors.append("unsafe rootfs archive path: " + name) + break + if member.issym() or member.islnk(): + target = member.linkname + resolved_link = posixpath.normpath(posixpath.join(posixpath.dirname(clean), target)) + if not target.startswith("/") and (resolved_link == ".." or resolved_link.startswith("../")): + errors.append("unsafe rootfs archive link: " + name) + break + if clean.lstrip("./") == "sbin/init": + init = member + if init is None or not (init.isfile() or init.issym() or init.islnk()): + errors.append("rootfs archive lacks sbin/init") + except Exception as exc: + errors.append("rootfs archive check failed: " + str(exc)) + + return {"reusable": not errors, "delivery": root, "errors": errors} + + +def self_test(): + root = tempfile.mkdtemp(prefix="emmc-cache-test-") + expected_parent = os.path.realpath(tempfile.gettempdir()) + try: + profile = {"schema_version": 1, "files": {}, "values": {"version": "2018.1"}} + profile["fingerprint_sha256"] = fingerprint(profile) + current = os.path.join(root, "current.json") + delivery = os.path.join(root, "delivery") + os.mkdir(delivery) + with open(current, "w", encoding="utf-8") as stream: + json.dump(profile, stream) + with open(os.path.join(delivery, "build_inputs.json"), "w", encoding="utf-8") as stream: + json.dump(profile, stream) + for name in ("BOOT.BIN", "image.ub", "MANIFEST.md"): + with open(os.path.join(delivery, name), "wb") as stream: + stream.write(name.encode("ascii")) + with tarfile.open(os.path.join(delivery, "rootfs.tar.bz2"), "w:bz2") as archive: + import io + info = tarfile.TarInfo("./sbin/init") + info.size = 4 + archive.addfile(info, io.BytesIO(b"init")) + with open(os.path.join(delivery, "summary.json"), "w", encoding="utf-8") as stream: + json.dump({"build_gates": {gate: {"status": "PASS"} for gate in ("G0", "G1", "G2", "G3")}}, stream) + with open(os.path.join(delivery, "SHA256SUMS"), "w", encoding="utf-8") as stream: + for name in REQUIRED: + stream.write("{} {}\n".format(digest(os.path.join(delivery, name)), name)) + if not check(current, delivery)["reusable"]: + raise AssertionError("valid delivery rejected") + with open(os.path.join(delivery, "image.ub"), "ab") as stream: + stream.write(b"corrupt") + if check(current, delivery)["reusable"]: + raise AssertionError("corrupt delivery accepted") + print(json.dumps({"self_test": "PASS"})) + return 0 + finally: + resolved = os.path.realpath(root) + if os.path.dirname(resolved) == expected_parent and os.path.basename(resolved).startswith("emmc-cache-test-"): + shutil.rmtree(resolved) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + sub = parser.add_subparsers(dest="action", required=True) + p = sub.add_parser("check") + p.add_argument("--current", required=True) + p.add_argument("--delivery", required=True) + sub.add_parser("self-test") + args = parser.parse_args() + if args.action == "self-test": + return self_test() + result = check(args.current, args.delivery) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 if result["reusable"] else 2 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as exc: + print(json.dumps({"error": str(exc)}), file=sys.stderr) + sys.exit(2)