diff --git a/README.md b/README.md index f052e64..c499bdc 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,34 @@ Tested on real hardware with CRS112-8P-4S: | IVGHP203Y-AF | hi3516cv300 | `/dev/uart-IVGHP203Y-AF` | | IVG85HG50PYA-S | hi3516ev300 | `/dev/uart-IVG85HG50PYA-S` | +## Flash Agent (High-Speed Flash Dump) + +Defib includes a bare-metal flash agent that runs directly on the SoC, +replacing U-Boot in the boot chain. It communicates over a COBS binary +protocol at 921600 baud — ~5x faster than U-Boot's `md.b` hex dump. + +```bash +# 1. Upload the agent (power-cycle the camera when prompted) +defib agent upload -c hi3516ev300 -p /dev/ttyUSB0 + +# 2. Dump the entire flash (address and size auto-detected) +defib agent read -p /dev/ttyUSB0 -o flash_dump.bin + +# Query device info (flash size, RAM base, JEDEC ID) +defib agent info -p /dev/ttyUSB0 + +# Write data back to flash +defib agent write -p /dev/ttyUSB0 -i flash_dump.bin + +# Scan flash health (bad sectors, stuck bits) +defib agent scan -p /dev/ttyUSB0 +``` + +Address defaults to flash base (`0x14000000`) and size is auto-detected +from the device. Override with `-a` and `-s` if needed. Use `--no-verify` +to skip the CRC32 check, or `--output-mode json` for automation. See +[agent/README.md](agent/README.md) for protocol details and supported chips. + ## Testing with QEMU Defib can be tested end-to-end against the diff --git a/agent/main.c b/agent/main.c index b440bff..ba5323c 100644 --- a/agent/main.c +++ b/agent/main.c @@ -333,6 +333,8 @@ static void handle_flash_write(const uint8_t *data, uint32_t len) { uint32_t chunk = pkt_len - 2; for (uint32_t i = 0; i < chunk && received < size; i++) staging[received++] = pkt[2 + i]; + /* Backpressure ACK */ + proto_send_ack(ACK_OK); } else if (cmd == 0) { uint8_t err[5]; err[0] = ACK_FLASH_ERROR; diff --git a/src/defib/agent/client.py b/src/defib/agent/client.py index 6e39653..e048437 100644 --- a/src/defib/agent/client.py +++ b/src/defib/agent/client.py @@ -383,41 +383,32 @@ async def write_flash( ) -> bool: """Write data to flash (assumes sectors already erased). - Splits into WRITE_MAX_TRANSFER blocks to avoid FIFO overflow. - Each block: host sends CMD_WRITE with flash address, agent - receives to RAM, verifies CRC, programs page-by-page, verifies. + Uses _write_block with per-packet backpressure for reliable + transfer. Agent receives to RAM staging, verifies CRC, programs + flash page-by-page, verifies readback. """ total = len(data) offset = 0 + max_retries = 3 while offset < total: block_size = min(WRITE_MAX_TRANSFER, total - offset) block = data[offset:offset + block_size] - block_crc = zlib.crc32(block) & 0xFFFFFFFF - payload = struct.pack(" int: payload = struct.pack(" None: result = await protocol.send_firmware( transport, agent_data, on_progress, spl_override=spl_data, + payload_label="Agent", ) if not result.success: if output == "json": @@ -934,9 +935,9 @@ async def _agent_info_async(port: str, output: str) -> None: @agent_app.command("read") def agent_read( port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"), - addr: str = typer.Option(..., "-a", "--addr", help="Start address (hex)"), - size: str = typer.Option(..., "-s", "--size", help="Size in bytes (or 1KB, 16MB, etc)"), - output_file: str = typer.Option(..., "-o", "--output", help="Output binary file"), + addr: str = typer.Option(None, "-a", "--addr", help="Start address (hex, default: flash base 0x14000000)"), + size: str = typer.Option(None, "-s", "--size", help="Size in bytes (or 1KB, 16MB, etc; default: auto-detect)"), + output_file: str = typer.Option("flash_dump.bin", "-o", "--output", help="Output binary file"), verify: bool = typer.Option(True, "--verify/--no-verify", help="CRC32 verify after read"), output: str = typer.Option("human", "--output-mode", help="Output mode: human, json"), ) -> None: @@ -946,7 +947,7 @@ def agent_read( async def _agent_read_async( - port: str, addr_str: str, size_str: str, output_file: str, verify: bool, output: str, + port: str, addr_str: str | None, size_str: str | None, output_file: str, verify: bool, output: str, ) -> None: import json as json_mod import time @@ -958,8 +959,6 @@ async def _agent_read_async( from defib.transport.serial import SerialTransport console = Console() - address = int(addr_str, 0) - size = _parse_size(size_str) transport = await SerialTransport.create(port) client = FlashAgentClient(transport) @@ -968,12 +967,23 @@ async def _agent_read_async( await transport.close() raise typer.Exit(1) + # Default to full flash dump when address/size not specified + if addr_str is None or size_str is None: + info = await client.get_info() + if not info: + console.print("[red]Failed to get device info[/red]") + await transport.close() + raise typer.Exit(1) + + address = int(addr_str, 0) if addr_str is not None else 0x14000000 + size = _parse_size(size_str) if size_str is not None else int(info["flash_size"]) + if output == "human": console.print(f"Reading 0x{address:08x} + {size} bytes...") t0 = time.time() data = await client.read_memory(address, size, on_progress=lambda d, t: ( - console.print(f"\r {d}/{t} ({d*100//t}%)", end="") if output == "human" else None + print(f"\r {d}/{t} ({d*100//t}%)", end="", flush=True) if output == "human" else None )) elapsed = time.time() - t0 @@ -1005,7 +1015,7 @@ async def _agent_read_async( @agent_app.command("write") def agent_write( port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial port"), - addr: str = typer.Option(..., "-a", "--addr", help="Start address (hex)"), + addr: str = typer.Option("0x14000000", "-a", "--addr", help="Start address (hex, default: flash base)"), input_file: str = typer.Option(..., "-i", "--input", help="Input binary file"), verify: bool = typer.Option(True, "--verify/--no-verify", help="CRC32 verify after write"), output: str = typer.Option("human", "--output", help="Output mode: human, json"), @@ -1042,7 +1052,7 @@ async def _agent_write_async( t0 = time.time() ok = await client.write_memory(address, data, on_progress=lambda d, t: ( - console.print(f"\r {d}/{t} ({d*100//t}%)", end="") if output == "human" else None + print(f"\r {d}/{t} ({d*100//t}%)", end="", flush=True) if output == "human" else None )) elapsed = time.time() - t0 diff --git a/src/defib/protocol/hisilicon_standard.py b/src/defib/protocol/hisilicon_standard.py index ff60af9..6582bb5 100644 --- a/src/defib/protocol/hisilicon_standard.py +++ b/src/defib/protocol/hisilicon_standard.py @@ -262,13 +262,14 @@ async def _send_uboot( firmware: bytes, profile: SoCProfile, on_progress: Callable[[ProgressEvent], None] | None = None, + label: str = "U-Boot", ) -> bool: - """Send U-Boot image to DDR.""" + """Send U-Boot (or agent) image to DDR.""" total = len(firmware) _emit(on_progress, ProgressEvent( stage=Stage.UBOOT, bytes_sent=0, bytes_total=total, - message="Sending U-Boot", + message=f"Sending {label}", )) if not await self._send_head(transport, total, profile.uboot_address): @@ -289,7 +290,7 @@ async def _send_uboot( _emit(on_progress, ProgressEvent( stage=Stage.UBOOT, bytes_sent=total, bytes_total=total, - message="U-Boot complete", + message=f"{label} complete", )) return True @@ -299,6 +300,7 @@ async def send_firmware( firmware: bytes, on_progress: Callable[[ProgressEvent], None] | None = None, spl_override: bytes | None = None, + payload_label: str = "U-Boot", ) -> RecoveryResult: if self._profile is None: return RecoveryResult(success=False, error="No profile loaded") @@ -321,7 +323,8 @@ async def send_firmware( ) stages.append(Stage.SPL) - if not await self._send_uboot(transport, firmware, profile, on_progress): + if not await self._send_uboot(transport, firmware, profile, on_progress, + label=payload_label): return RecoveryResult( success=False, stages_completed=stages, error="Failed to send U-Boot",