Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 12 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,18 @@ Backends: `spi_flash.c` (fmc100), `spi_flash_hisfc350.c` (V1-era parts),
U-Boot device discovery.
- **Firmware** (`src/defib/firmware.py`) — downloads OpenIPC releases from
GitHub, caches under `XDG_CACHE_HOME`. Most chips use one
`u-boot-<chip>-universal.bin`; the u-boot-xmedia SoCs in
`PER_FLASH_TYPE_UBOOT` (hi3516ev200/ev300, hi3518ev300, hi3516dv200,
gk7205v500/v510/v530) publish `u-boot-<chip>-nor.bin` and `-nand.bin`
instead, so callers pass `flash_type` (NOR when unknown). On NAND those SoCs
install the UBI-only layout (`NAND_UBI_LAYOUT` in `install/layout.py`: 768k
boot, 256k env, rest ubi), whose mtdparts/bootcmd/bootargs come from the
U-Boot default env; `NAND_LAYOUT` is the legacy split layout for other chips.
`u-boot-<chip>-universal.bin`; the SoCs in `PER_FLASH_TYPE_UBOOT` (the
u-boot-xmedia ones: hi3516ev200/ev300, hi3518ev300, hi3516dv200,
gk7205v500/v510/v530; and the OpenIPC/u-boot gk7202v300, gk7205v200/v300,
gk7605v100) publish `u-boot-<chip>-nor.bin` and `-nand.bin` instead, so
callers pass `flash_type` (NOR when unknown). On NAND those SoCs install the
UBI-only layout (`NAND_UBI_LAYOUT` in `install/layout.py`: 768k boot, 256k
env, rest ubi), whose mtdparts/bootcmd/bootargs come from the U-Boot default
env; `NAND_LAYOUT` is the legacy split layout for other chips. The
`NOR_SPLIT_SOCS` (gk7205v200 family) also install the split NOR package:
one `firmware.bin.<board>` at 0x50000, the rest of the chip erased, which
the kernel divides at boot; their NOR U-Boot scans the layout, so the env
stage loads its defaults (`env default -a`) on either NOR layout.
- **Capture** (`src/defib/capture/`) — record/replay UART sessions in `.dcap`.
- Loose modules worth knowing: `flashdump.py` (dump flash through a U-Boot
console), `ubi.py` (extract UBIFS volumes from raw UBI), `uboot_env.py`,
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,33 @@ gk7205v510/v530 NAND packages are published under board `gk7205v500`.
`install --nand` on any other chip still uses the older split layout (raw
kernel partition, `mtdparts` and `bootcmd` set by the installer).

gk7202v300, gk7205v200, gk7205v300 and gk7605v100 take their U-Boot from
OpenIPC/u-boot (`openipc-hisilicon`), also published per flash type. That NOR
U-Boot works out the layout at every boot, so `install` loads its default
environment (`env default -a`, keeping the camera's `ethaddr`) instead of
writing `mtdparts` and `bootcmd`. Besides the fixed-slot package, their firmware
ships a split NOR package, `openipc.<board>-nor-<variant>-split.tgz`, and
`install` recognises it by its contents:

| Offset | Size | Contents |
|--------|------|----------|
| `0x000000` | 256K | boot: `u-boot-<chip>-nor.bin` |
| `0x040000` | 64K | env |
| `0x050000` | rest of chip | firmware: `firmware.bin.<board>` (a hashed FIT, then the squashfs at the next 64 KiB); the kernel divides it into `kernel`, `rootfs` and `rootfs_data` at boot |

```bash
defib install -c gk7205v300 \
--firmware openipc.gk7205v300-nor-ultimate-split.tgz \
-p /dev/ttyUSB0 --power-cycle
```

The `rootfs` stage writes `firmware.bin` at `0x50000` and erases the rest of the
chip, so the first boot starts with an empty overlay; the `kernel` and
`rootfs-data` stages have nothing of their own to do. Only that U-Boot boots the
image: a plan without the `uboot` stage warns that the one already on the flash
must be it. See the [OpenIPC wiki](https://github.com/OpenIPC/wiki/blob/master/en/nor-split-layout.md)
for the layout itself.

Targets that must bootstrap through a stock U-Boot use an explicit U-Boot
variant. For example, HiWatch DS-I203 uses:

Expand Down Expand Up @@ -135,7 +162,8 @@ cleanup behavior.
The release U-Boot owns boot-critical hardware initialization such as DDR
cold-init and RAM probing limits. Defib owns the layout it actually flashes: it
detects NOR capacity, selects the standard OpenIPC 8/16/32 MiB layout, and
persists the matching `mtdparts` before the first Linux boot. Device runtime
persists the matching `mtdparts` before the first Linux boot (except on the
gk7205v200 family above, whose U-Boot sets `mtdparts` itself). Device runtime
policy such as `osmem`, Linux PHY parameters and sensor selection belongs to the
firmware/device profile. An explicit local U-Boot can still be supplied with
`--uboot`.
Expand Down
29 changes: 23 additions & 6 deletions src/defib/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
# Chips that have pre-built u-boot-{chip}-universal.bin binaries on OpenIPC.
# CV6xx chips are deliberately absent — see CV6XX_BOOT_VARIANTS below.
AVAILABLE_FIRMWARE: set[str] = {
"gk7202v300", "gk7205v200", "gk7205v300", "gk7605v100",
"hi3516av100", "hi3516av200", "hi3516av300",
"hi3516cv100", "hi3516cv200", "hi3516cv300", "hi3516cv500",
"hi3516dv100", "hi3516dv300",
Expand All @@ -42,16 +41,28 @@
"t40a", "t40n", "t40xp",
}

# SoCs whose OpenIPC U-Boot is built from OpenIPC/u-boot-xmedia and published
# once per flash type as u-boot-{chip}-nor.bin / u-boot-{chip}-nand.bin. The
# SoCs whose OpenIPC U-Boot is published once per flash type as
# u-boot-{chip}-nor.bin / u-boot-{chip}-nand.bin: the u-boot-xmedia SoCs, and
# the gk7205v200 family built from OpenIPC/u-boot (openipc-hisilicon). The
# NAND build owns the UBI-only layout (768k boot, 256k env, rest ubi) in its
# default environment, so a NOR image must never be installed on NAND or vice
# versa. The old u-boot-{chip}-universal.bin of the HiSilicon members was the
# retired u-boot-hi3516ev200 build with the split NAND layout; it is never
# downloaded for these SoCs again.
# versa. The old u-boot-{chip}-universal.bin of these SoCs came from retired
# per-SoC repositories (u-boot-hi3516ev200, u-boot-gk7205v200); it is never
# downloaded for them again.
PER_FLASH_TYPE_UBOOT: frozenset[str] = frozenset({
"hi3516ev200", "hi3516ev300", "hi3518ev300", "hi3516dv200",
"gk7205v500", "gk7205v510", "gk7205v530",
"gk7202v300", "gk7205v200", "gk7205v300", "gk7605v100",
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
})

# SoCs whose OpenIPC firmware also ships the split NOR layout,
# openipc.<board>-nor-<variant>-split.tgz: one firmware.bin (a hashed FIT, then
# the squashfs at the next 64 KiB) written at 0x50000, which the kernel divides
# into kernel, rootfs and rootfs_data at boot. Their OpenIPC/u-boot NOR build
# boots it, and also scans the fixed slots, so it owns mtdparts and bootcmd on
# either layout.
NOR_SPLIT_SOCS: frozenset[str] = frozenset({
"gk7202v300", "gk7205v200", "gk7205v300", "gk7605v100",
})

FLASH_TYPES: tuple[str, ...] = ("nor", "nand")
Expand Down Expand Up @@ -145,6 +156,12 @@ def uses_per_flash_type_uboot(chip: str) -> bool:
return CHIP_TO_FIRMWARE.get(base, base) in PER_FLASH_TYPE_UBOOT


def supports_nor_split(chip: str) -> bool:
"""True for SoCs whose firmware and NOR U-Boot use the split NOR layout."""
base = _strip_variant(chip).lower()
return CHIP_TO_FIRMWARE.get(base, base) in NOR_SPLIT_SOCS


def asset_name(chip: str, flash_type: str | None = None) -> str | None:
"""Published release filename for a chip, or None if there isn't one.

Expand Down
74 changes: 74 additions & 0 deletions src/defib/install/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,51 @@ class FirmwareBundle:
For a UBI-only NAND package the kernel lives inside the rootfs image
(``/boot/fitImage`` in the UBIFS volume): ``kernel_name`` is empty and
``kernel`` is ``b""``.

For a split NOR package (``openipc.<board>-nor-<variant>-split.tgz``) the
one image is ``firmware.bin.<board>``: it is carried as the rootfs, the
kernel is empty, and ``split`` is set.
"""

kernel_name: str
kernel: bytes
rootfs_name: str
rootfs: bytes
split: bool = False


ERASE_BLOCK = 0x10000
FDT_MAGIC = b"\xd0\x0d\xfe\xed"
SQUASHFS_MAGIC = b"hsqs"


def split_image_end(image: bytes) -> int:
"""Where rootfs_data starts, relative to the image, for a split firmware.bin.

The FIT's ``totalsize`` (header bytes 4..7, big-endian) rounded up to a
64 KiB block is where the squashfs starts; its ``bytes_used`` (superblock
offset 40, little-endian) rounded up the same way is where it ends. The
kernel's mtdsplit, U-Boot and sysupgrade all draw the same lines.
Raises ValueError for anything else.
"""
if image[:4] != FDT_MAGIC:
raise ValueError("firmware.bin does not start with a FIT image")
fit = int.from_bytes(image[4:8], "big")
squash = (fit + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK
if not fit or squash + 48 > len(image) or image[squash:squash + 4] != SQUASHFS_MAGIC:
raise ValueError("firmware.bin has no squashfs at the 64 KiB block after its FIT")
used = int.from_bytes(image[squash + 40:squash + 48], "little")
end = squash + (used + ERASE_BLOCK - 1) // ERASE_BLOCK * ERASE_BLOCK
if not used or len(image) < squash + used:
raise ValueError(
"firmware.bin ends inside its squashfs: the image is truncated"
)
if end < len(image):
raise ValueError(
"firmware.bin runs past the end of its squashfs: not an image as the "
"OpenIPC build packs it"
)
return end


def _is_ubi_member(name: str) -> bool:
Expand All @@ -45,6 +84,8 @@ def load_firmware_bundle(
rootfs_name = ""
rootfs: bytes | None = None
ubi_members: list[str] = []
split_name = ""
split_image: bytes | None = None
expected_md5: dict[str, str] = {}

with tarfile.open(path, "r:gz") as archive:
Expand All @@ -58,6 +99,9 @@ def load_firmware_bundle(
line = stream.read().decode().strip()
if line:
expected_md5[name.removesuffix(".md5sum")] = line.split()[0]
elif name.rsplit("/", 1)[-1].startswith("firmware.bin"):
split_name = name
split_image = stream.read()
elif ubi_only:
if _is_ubi_member(name):
ubi_members.append(name)
Expand All @@ -70,7 +114,37 @@ def load_firmware_bundle(
rootfs_name = name
rootfs = stream.read()

if split_image is not None and not ubi_only:
if kernel or rootfs:
raise ValueError(
"tarball holds both firmware.bin and a separate kernel/rootfs; "
"expected one OpenIPC package or the other"
)
expected = expected_md5.get(split_name)
if expected is not None and hashlib.md5(split_image).hexdigest() != expected:
raise ValueError(f"MD5 mismatch for {split_name}")
split_image_end(split_image)
board = split_name.rsplit("/", 1)[-1].removeprefix("firmware.bin").lstrip(".")
if boards is not None and board not in boards:
raise ValueError(
f"{split_name} is built for {board or 'an unnamed board'}, not "
+ " or ".join(sorted(boards))
+ "; refusing to write another board's firmware"
)
return FirmwareBundle(
kernel_name="",
kernel=b"",
rootfs_name=split_name,
rootfs=split_image,
split=True,
)

if ubi_only:
if not rootfs and split_image is not None:
raise ValueError(
f"{split_name} is a split NOR package (openipc.<board>-nor-<variant>"
"-split.tgz); it cannot be installed on NAND"
)
if not rootfs:
raise ValueError(
"tarball has no rootfs.ubi.<board> image; this NAND layout needs "
Expand Down
54 changes: 50 additions & 4 deletions src/defib/install/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,28 @@


def ubi_nand_boards(chip: str) -> set[str]:
"""The ``rootfs.ubi.<board>`` suffixes a NAND package for ``chip`` may carry."""
"""The ``rootfs.ubi.<board>`` suffixes a NAND package for ``chip`` may carry.

An aliased chip (gk7201v200, gk7205v210, ...) installs the U-Boot of the SoC
it is published under, and that SoC's package with it.
"""
from defib.firmware import CHIP_TO_FIRMWARE

base = chip.partition(":")[0]
return {base, _UBI_NAND_BOARD.get(base, base)}
published = CHIP_TO_FIRMWARE.get(base.lower(), base)
return {base, published, _UBI_NAND_BOARD.get(base, base), _UBI_NAND_BOARD.get(published, published)}

def split_boards(chip: str) -> set[str]:
"""The ``firmware.bin.<board>`` suffixes a split NOR package for ``chip`` may carry.

Aliased chips (gk7205v210, gk7201v200, ...) install the image of the SoC
they are published under.
"""
from defib.firmware import CHIP_TO_FIRMWARE

base = chip.partition(":")[0].lower()
return {base, CHIP_TO_FIRMWARE.get(base, base)}


_MTDPART_RE = re.compile(
r"^(?P<size>-|(?:0x[0-9a-f]+|\d+)[kmg]?)"
Expand Down Expand Up @@ -207,9 +226,14 @@ def uboot_sf_lock_unsupported(response: str) -> bool:
# subcommand. That is compatibility information only when the table itself
# contains no sf lock entry. If sf lock is listed, a Usage response means our
# invocation was rejected and must remain a hard failure.
usage_seen = any(line.startswith("usage:") for line in lines)
#
# Only the table counts. A console that echoes the command puts "sf lock 0"
# above "Usage:", and that echo is not an entry: OpenIPC/u-boot's sf has no
# lock subcommand at all and answers exactly this way.
usage_at = next((i for i, line in enumerate(lines) if line.startswith("usage:")), None)
usage_seen = usage_at is not None
sf_usage: list[str] = []
for line in lines:
for line in lines[usage_at or 0:] if usage_seen else []:
candidate = line.removeprefix("usage:").strip() if line.startswith("usage:") else line
if re.match(r"^sf(?:\s|$)", candidate):
sf_usage.append(candidate)
Expand Down Expand Up @@ -371,6 +395,28 @@ def detect_nor_size_mb(response: str) -> int | None:
return None


NOR_SIZE_CANDIDATES_MB = (32, 16, 8)


def nor_size_probe_read(ram_addr: int, size_mb: int) -> str:
"""A 16-byte ``sf read`` that only a chip of at least ``size_mb`` accepts.

OpenIPC/u-boot (openipc-hisilicon) probes the flash at boot and its
``sf probe`` prints nothing at the prompt, so the size it found is not on
offer; but ``sf read`` refuses an offset past the chip ("Offset exceeds
device limit"). The largest candidate whose last 16 bytes read is the size.
"""
return f"sf read 0x{ram_addr:x} 0x{size_mb * 1024 * 1024 - 16:x} 0x10"


def sf_read_succeeded(response: str) -> bool:
"""True when ``sf read`` reported the read done and nothing refused it."""
text = response.lower()
if "exceeds" in text or "past flash size" in text or "error" in text:
return False
return "read: ok" in text


def select_nor_size_mb(
override: int,
detected: int | None,
Expand Down
Loading
Loading