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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions agent/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 19 additions & 27 deletions src/defib/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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("<III", addr + offset, block_size, block_crc)
await send_packet(self._transport, CMD_WRITE, payload)

cmd, resp = await recv_response(self._transport, timeout=5.0)
if cmd != RSP_ACK or resp[0] != ACK_OK:
logger.error("Flash write rejected at offset %d", offset)
return False
ok = False
for attempt in range(max_retries):
ok = await self._write_block(addr + offset, block)
if ok:
break
logger.warning(
"Flash write retry %d at offset %d/%d",
attempt + 1, offset, total,
)
import asyncio
await asyncio.sleep(0.1)

# Send data in small chunks
blk_off = 0
seq = 0
while blk_off < block_size:
chunk = min(WRITE_CHUNK_SIZE, block_size - blk_off)
pkt = struct.pack("<H", seq) + block[blk_off:blk_off + chunk]
await send_packet(self._transport, RSP_DATA, pkt)
blk_off += chunk
seq += 1

# Wait for program + verify (page programming is slow)
timeout = max(60.0, block_size / 500)
cmd, resp = await recv_response(self._transport, timeout=timeout)
if cmd != RSP_ACK or resp[0] != ACK_OK:
logger.error("Flash write failed at offset %d", offset)
if not ok:
logger.error("Flash write failed at offset %d/%d", offset, total)
return False

offset += block_size
Expand All @@ -432,7 +423,8 @@ async def crc32(self, addr: int, size: int) -> int:

payload = struct.pack("<II", addr, size)
await send_packet(self._transport, CMD_CRC32, payload)
cmd, data = await recv_response(self._transport, timeout=10.0)
timeout = max(10.0, 5.0 + size / (1024 * 1024))
cmd, data = await recv_response(self._transport, timeout=timeout)
if cmd != RSP_CRC32 or len(data) < 4:
raise RuntimeError("CRC32 response invalid")
return int(struct.unpack("<I", data[:4])[0])
Expand Down
28 changes: 19 additions & 9 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,7 @@ def on_progress(e: ProgressEvent) -> 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":
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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

Expand Down
11 changes: 7 additions & 4 deletions src/defib/protocol/hisilicon_standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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

Expand All @@ -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")
Expand All @@ -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",
Expand Down
Loading