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
37 changes: 29 additions & 8 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1644,7 +1644,7 @@ def install(
host_ip: str = typer.Option("192.168.1.10", "--host-ip", help="IP to assign to host NIC for TFTP"),
device_ip: str = typer.Option("192.168.1.20", "--device-ip", help="IP for camera in U-Boot"),
tftp_port: int = typer.Option(69, "--tftp-port", help="TFTP server port"),
nor_size: int = typer.Option(8, "--nor-size", help="NOR flash size in MB (8 or 16)"),
nor_size: int = typer.Option(8, "--nor-size", help="NOR flash size in MB (8, 16, or 32)"),
nand: bool = typer.Option(False, "--nand", help="Use NAND flash instead of NOR"),
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
debug: bool = typer.Option(False, "-d", "--debug", help="Enable debug logging"),
Expand Down Expand Up @@ -1678,6 +1678,16 @@ def install(
"rootfs": (0x350000, 0xA00000), # 10240KB
}

# 32MB NOR flash layout — OpenIPC U-Boot has no setnor32m env var, so we
# send mtdparts directly. Pattern continues 8m/16m: same boot/env/kernel
# offsets, larger rootfs to use the extra space.
_NOR32M_LAYOUT = {
"boot": (0x000000, 0x40000), # 256KB
"env": (0x040000, 0x10000), # 64KB
"kernel": (0x050000, 0x300000), # 3MB
"rootfs": (0x350000, 0x1800000), # 24MB
}

# NAND flash layout: 1M(boot),1M(env),8M(kernel),-(ubi)
_NAND_LAYOUT = {
"boot": (0x000000, 0x100000), # 1MB
Expand Down Expand Up @@ -1753,7 +1763,12 @@ async def _install_async(
flash_cmd = "nand"
flash_label = "NAND"
else:
layout = _NOR16M_LAYOUT if nor_size >= 16 else _NOR8M_LAYOUT
if nor_size >= 32:
layout = _NOR32M_LAYOUT
elif nor_size >= 16:
layout = _NOR16M_LAYOUT
else:
layout = _NOR8M_LAYOUT
flash_cmd = "sf"
flash_label = f"NOR {nor_size}MB"

Expand Down Expand Up @@ -2198,13 +2213,19 @@ async def tftp_and_flash(
bootargs = _nand_bootargs(rootfs_is_ubi=is_ubi_image(rootfs_data))
await _cmd(f"setenv bootargs {bootargs}", timeout=3.0)
else:
nor_cmd = "setnor8m" if nor_size < 16 else "setnor16m"
if output == "human":
console.print(f"\n [bold]Setting boot environment[/bold] (run {nor_cmd})")
# setnor8m does: set mtdparts, set bootcmd, saveenv, reset
# We do it manually to avoid the auto-reset
mtdparts_var = f"mtdpartsnor{nor_size}m"
await _cmd(f"run {mtdparts_var}", timeout=3.0)
console.print("\n [bold]Setting boot environment[/bold]")
# OpenIPC U-Boot defines mtdpartsnor{8,16}m env vars but
# not 32m — for 32MB, send the raw mtdparts string.
if nor_size >= 32:
mtdparts = (
"hi_sfc:256k(boot),64k(env),3072k(kernel),"
"24576k(rootfs),-(rootfs_data)"
)
await _cmd(f"setenv mtdparts {mtdparts}", timeout=3.0)
else:
mtdparts_var = f"mtdpartsnor{nor_size}m"
await _cmd(f"run {mtdparts_var}", timeout=3.0)
await _cmd("setenv bootcmd ${bootcmdnor}", timeout=3.0)
resp = await _cmd("saveenv", timeout=10.0)
if output == "human":
Expand Down
89 changes: 68 additions & 21 deletions src/defib/protocol/hisilicon_standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,8 @@ async def _send_frame_with_retry(
for attempt in range(retries):
await transport.flush_input()
await transport.flush_output()
await transport.write(frame_data)
try:
await transport.write(frame_data)
ack = await transport.read(1, timeout=timeout)
if ack == ACK_BYTE:
logger.debug("TX %s ACKed (attempt %d/%d)", label, attempt + 1, retries)
Expand Down Expand Up @@ -394,28 +394,38 @@ def _detect_spl_size(firmware: bytes, profile_max: int) -> int:
"""Detect actual SPL code size from firmware binary.

HiSilicon mini-boot layout: vector table + .reg + executable code +
LZMA-compressed U-Boot payload. The SPL only needs the code region.
If the code is larger than the profile default (e.g. when SVB is
enabled), use the actual size so the bootrom receives all of it.
compressed U-Boot payload (LZMA or gzip, depending on the build).
The SPL only needs the code region — bytes past the compressed
payload boundary land in SRAM that the bootrom uses for its own
stack/state, so writing them corrupts the bootrom.

When a compressed-payload boundary is found we trust it absolutely
and use it instead of profile_max — even if it's smaller. profile_max
comes from HiTool's reference SPL which fills the full window; an
OpenIPC build that's more compact must NOT be padded to that size.
"""
# Search for LZMA header (0x5D + 4-byte LE dictionary size) after
# the .reg region. Common dict sizes: 64K, 128K, 256K, 512K, 1M, 8M.
VALID_DICT_SIZES = {
1 << n for n in range(16, 25) # 64K .. 16M
}
# LZMA: 0x5D + 4-byte LE dictionary size (64K..16M)
VALID_LZMA_DICT = {1 << n for n in range(16, 25)}
# gzip: 1f 8b 08 (deflate method)
for i in range(0x4000, min(len(firmware), 0x10000)):
if firmware[i] == 0x5D:
b = firmware[i]
detected: int | None = None
if b == 0x5D:
ds = int.from_bytes(firmware[i + 1 : i + 5], "little")
if ds in VALID_DICT_SIZES:
# Round up to 1 KB boundary
detected = (i + 0x3FF) & ~0x3FF
if detected > profile_max:
logger.info(
"SPL code extends to 0x%X (%d bytes), "
"profile default was 0x%X (%d bytes)",
detected, detected, profile_max, profile_max,
)
return max(detected, profile_max)
if ds in VALID_LZMA_DICT:
detected = i & ~0x3FF
label = "LZMA"
elif b == 0x1F and firmware[i + 1] == 0x8B and firmware[i + 2] == 0x08:
detected = i & ~0x3FF
label = "gzip"
if detected is not None:
if detected != profile_max:
logger.info(
"SPL boundary detected (%s) at 0x%X (%d bytes); "
"profile default was 0x%X (%d bytes)",
label, detected, detected, profile_max, profile_max,
)
return detected
return profile_max

async def _send_spl(
Expand Down Expand Up @@ -457,7 +467,11 @@ async def _send_spl(
))

if not await self._send_tail(transport, len(chunks) + 1):
return False
# av200/av300 SPL detaches the bootrom protocol handler as
# soon as all declared bytes arrive — no TAIL ACK is sent.
if profile.prestep_data is None:
return False
logger.debug("SPL TAIL not ACKed (non-fatal for av200/av300, all data sent)")

# DDR training delay: HiTool sleeps 300ms after SPL transfer.
# Always apply — the SPL runs DDR training which needs time.
Expand All @@ -471,6 +485,38 @@ async def _send_spl(
))
return True

@staticmethod
def _zero_long_ff_runs(firmware: bytes, threshold: int = 12) -> bytes:
"""Zero out long runs of 0xFF bytes.

The hi3516cv500-family bootrom (av300, dv300, cv500) hangs mid-DATA
frame when the payload contains >=12 consecutive 0xFF bytes — almost
certainly a quirk in the bootrom's UART receive path. These runs
only appear as inert padding between SPL code and the compressed
U-Boot payload, so zeroing them is safe.
"""
if firmware.count(b"\xff" * threshold) == 0:
return firmware
out = bytearray(firmware)
run_start = -1
for i, b in enumerate(out):
if b == 0xFF:
if run_start < 0:
run_start = i
else:
if run_start >= 0 and i - run_start >= threshold:
logger.debug(
"_zero_long_ff_runs: zeroed %d 0xFF bytes at offset 0x%X",
i - run_start, run_start,
)
for j in range(run_start, i):
out[j] = 0
run_start = -1
if run_start >= 0 and len(out) - run_start >= threshold:
for j in range(run_start, len(out)):
out[j] = 0
return bytes(out)

async def _send_uboot(
self,
transport: Transport,
Expand All @@ -480,6 +526,7 @@ async def _send_uboot(
label: str = "U-Boot",
) -> bool:
"""Send U-Boot (or agent) image to DDR."""
firmware = self._zero_long_ff_runs(firmware)
total = len(firmware)
logger.debug(
"=== %s === address=0x%08X total=%d chunks=%d",
Expand Down
22 changes: 19 additions & 3 deletions src/defib/transport/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,25 @@ async def read(self, size: int, timeout: float | None = None) -> bytes:
self._port.timeout = old_timeout

async def write(self, data: bytes) -> None:
await asyncio.get_event_loop().run_in_executor(
None, self._port.write, data
)
# Pyserial honours write_timeout inside Serial.write(), so the worker
# thread actually returns instead of blocking in pselect6 forever.
# asyncio.wait_for can't help here — cancelling a run_in_executor
# future leaves the underlying thread still blocked.
# 5s ceiling: a 1KB write at 115200 baud is ~89ms; >5s means the
# kernel TX buffer isn't draining (USB-serial hung, flow control,
# cable unplugged).
old_wt = self._port.write_timeout
self._port.write_timeout = 5.0
try:
await asyncio.get_event_loop().run_in_executor(
None, self._port.write, data
)
except serial.SerialTimeoutException as exc:
raise TransportTimeout(
f"Write timeout (5.0s, {len(data)} bytes)"
) from exc
finally:
self._port.write_timeout = old_wt

async def flush_input(self) -> None:
self._port.reset_input_buffer()
Expand Down
Loading
Loading