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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ the handshake, reverting to 115200 after ~30 s idle. 13 commands: `INFO 0x01`,
optional features through a capability bitmask (`client.py`).

Backends: `spi_flash.c` (fmc100), `spi_flash_hisfc350.c` (V1-era parts),
`emmc_himci.c`. Eleven SoCs are supported; each has its own `ifeq` stanza in
`emmc_himci.c`. Twelve SoCs are supported; each has its own `ifeq` stanza in
`agent/Makefile` carrying `LOAD_ADDR` (and `SPI_DRIVER` where it differs).
`link.ld` itself is generic — it just places `. = LOAD_ADDR`.

Expand Down
18 changes: 17 additions & 1 deletion agent/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ else ifeq ($(SOC),gk7605v100)
WDT_BASE = 0x12030000
CRG_BASE = 0x12010000
SYSCTRL_REBOOT = 0x12020004
else ifeq ($(SOC),gk7205v500)
# V500 bootrom (gk7205v500/v510/v530): the agent rides in the boot-code
# area of a V500 boot image (see wrap_v500_payload). After a UART
# download the bootrom runs that code in place, at the 0x41000000 load
# address + 8 KiB key area + 20 KiB aux area; the image's own entry
# field (0x40707000) is not used on this path. Peripherals match
# gk7205v300.
UART_BASE = 0x12040000
UART_CLOCK = 24000000
LOAD_ADDR = 0x41007000
FLASH_MEM = 0x14000000
FMC_BASE = 0x10000000
RAM_BASE = 0x40000000
WDT_BASE = 0x12030000
CRG_BASE = 0x12010000
SYSCTRL_REBOOT = 0x12020004
else ifeq ($(SOC),hi3516cv300)
# V3 generation: ARM926EJ-S (ARMv5TEJ), not Cortex-A7.
# UART is PL011 (same as ev300 family) at a different base.
Expand Down Expand Up @@ -188,7 +204,7 @@ else ifeq ($(SOC),hi3520dv200)
SYSCTRL_REBOOT = 0x20050004
SPI_DRIVER = hisfc350
else
$(error Unknown SOC: $(SOC). Supported: hi3516ev300 hi3516ev200 gk7205v200 gk7205v300 gk7605v100 hi3516cv300 hi3516cv500 hi3518ev200 hi3516cv610 hi3519v101 hi3520dv200)
$(error Unknown SOC: $(SOC). Supported: hi3516ev300 hi3516ev200 gk7205v200 gk7205v300 gk7605v100 gk7205v500 hi3516cv300 hi3516cv500 hi3518ev200 hi3516cv610 hi3519v101 hi3520dv200)
endif

# Per-SoC CPU. V3 chips (cv300) are ARM926EJ-S (ARMv5TEJ);
Expand Down
6 changes: 6 additions & 0 deletions agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,19 @@ Requires `arm-none-eabi-gcc` (Arch: `pacman -S arm-none-eabi-gcc arm-none-eabi-n
| gk7205v200 | 0x12040000 | 0x14000000 | 0x40000000 | 0x41000000 |
| gk7205v300 | 0x12040000 | 0x14000000 | 0x40000000 | 0x41000000 |
| gk7605v100 | 0x12040000 | 0x14000000 | 0x40000000 | 0x41000000 |
| gk7205v500 | 0x12040000 | 0x14000000 | 0x40000000 | 0x41007000 |
| hi3516cv300 | 0x12100000 | 0x14000000 | 0x80000000 | 0x81000000 |
| hi3516cv500 | 0x12100000 | 0x14000000 | 0x80000000 | 0x81000000 |
| hi3518ev200 | 0x12100000 | 0x14000000 | 0x80000000 | 0x81000000 |
| hi3516cv610 | 0x11040000 | 0x14000000 | 0x40000000 | 0x41000000 |
| hi3519v101 | 0x12100000 | 0x14000000 | 0x80000000 | 0x81000000 |
| hi3520dv200 | 0x20080000 | 0x58000000 | 0x80000000 | 0x81000000 |

gk7205v500 also serves gk7205v510/v530. The V500 bootrom has no SPL stage:
`defib agent upload` puts the agent in the boot-code slot of an OpenIPC
u-boot-xmedia image (downloaded, or `-f` for your own), whose DDR-init code
runs first; the bootrom then executes the slot in place at `0x41007000`.

Addresses from [qemu-hisilicon](https://github.com/OpenIPC/LoTool) hardware definitions.

### Where defib looks for a built binary
Expand Down
36 changes: 25 additions & 11 deletions agent/spi_flash.c
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
#define NAND_STATUS_E_FAIL (1 << 2) /* Erase Fail */
#define NAND_STATUS_P_FAIL (1 << 3) /* Program Fail */

/* NAND geometry — currently only MX35LF1GE4AB (1Gbit) is recognized.
/* NAND geometry shared by every nand_ids[] part (1 Gbit).
* On-chip ECC is enabled by default; reads return ECC-corrected data. */
#define NAND_PAGE_SIZE 2048
#define NAND_BLOCK_SIZE (64 * NAND_PAGE_SIZE) /* 128 KiB */
Expand Down Expand Up @@ -421,16 +421,30 @@ static void nand_write_enable(void) {
fmc_wait_ready();
}

/* Identify SPI NAND chip from JEDEC ID. Returns 1 if recognized, 0 otherwise.
* Currently only MX35LF1GE4AB (Macronix, c2 12, 1Gbit / 128MB). The agent's
* flash_read_id reads bytes [0..2] of an 8-byte fetch; some SPI NAND chips
* return the manufacturer ID with a leading dummy byte, so we accept the ID
* shifted by one position too. */
/* SPI NAND chips the agent drives, by manufacturer + first device ID byte.
* All are 1 Gbit, 2 KiB pages, 64 pages per 128 KiB block — the geometry
* flash_init hardcodes. Only two ID bytes are matched because a chip that
* answers 0x9F with a leading dummy byte pushes its third byte out of the
* three-byte window flash_read_id captures (W25N01GV reads back 00 EF AA).
* None of these pairs collides with a SPI NOR ID.
*
* An unrecognised NAND falls through to the NOR path, which is not just
* wrong but can hang: flash_global_unlock() polls a NOR status register a
* NAND does not implement (GD5F1GM7 never clears "WIP"). */
static const uint8_t nand_ids[][2] = {
{ 0xC2, 0x12 }, /* Macronix MX35LF1GE4AB */
{ 0xEF, 0xAA }, /* Winbond W25N01GV (EF AA 21) */
{ 0xC8, 0x91 }, /* GigaDevice GD5F1GM7UE, 3.3 V */
{ 0xC8, 0x81 }, /* GigaDevice GD5F1GM7RE, 1.8 V */
};

/* Returns 1 if id[] is a known SPI NAND, read either directly or shifted
* by one dummy byte (id[0] = dummy). */
static int nand_identify(const uint8_t id[3]) {
/* Direct: id[0]=0xC2 id[1]=0x12 */
if (id[0] == 0xC2 && id[1] == 0x12) return 1;
/* Shifted by 1 (dummy byte at id[0]): id[1]=0xC2 id[2]=0x12 */
if (id[1] == 0xC2 && id[2] == 0x12) return 1;
for (unsigned i = 0; i < sizeof(nand_ids) / sizeof(nand_ids[0]); i++) {
if (id[0] == nand_ids[i][0] && id[1] == nand_ids[i][1]) return 1;
if (id[1] == nand_ids[i][0] && id[2] == nand_ids[i][1]) return 1;
}
return 0;
}

Expand Down Expand Up @@ -459,7 +473,7 @@ int flash_init(flash_info_t *info) {
* memory-mapped boot mode and uses different protection (BP bits
* via SET_FEATURE 0xA0 instead of write-status-register). */
info->flash_type = FLASH_TYPE_NAND;
info->size = 128u * 1024u * 1024u; /* MX35LF1GE4AB = 128 MiB */
info->size = 128u * 1024u * 1024u; /* every nand_ids[] part is 1 Gbit */
info->sector_size = NAND_BLOCK_SIZE; /* 128 KiB erase block */
info->page_size = NAND_PAGE_SIZE; /* 2 KiB read/program page */
current_flash_type = FLASH_TYPE_NAND;
Expand Down
3 changes: 3 additions & 0 deletions src/defib/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ def bad_block(self) -> list[SectorResult]:
"hi3516cv608": "hi3516cv610", # cv6xx-family, same memory map
"hi3518ev200": "hi3518ev200",
"hi3520dv200": "hi3520dv200", # V1-era, HISFC350 SPI controller
"gk7205v500": "gk7205v500",
"gk7205v510": "gk7205v500", # V500 family, same memory map
"gk7205v530": "gk7205v500",
}


Expand Down
172 changes: 169 additions & 3 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

from collections.abc import Callable, Coroutine
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, NoReturn

import typer

Expand Down Expand Up @@ -1080,7 +1080,7 @@ def agent_upload(
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
port: str = typer.Option("/dev/ttyUSB0", "-p", "--port", help="Serial device (/dev/ttyUSB0), tcp://host:port, rfc2217://host:port, or socket:///path"),
output: str = typer.Option("human", "--output", help="Output mode: human, json"),
file: str | None = typer.Option(None, "-f", "--file", help="CV6xx composite boot file (GSL+DDR+U-Boot); required for CV6xx, ignored for other protocols"),
file: str | None = typer.Option(None, "-f", "--file", help="CV6xx: composite boot file (GSL+DDR+U-Boot), required. V500: U-Boot image whose header and DDR-init code carry the agent (default: OpenIPC u-boot-xmedia download). Ignored for other protocols"),
power_cycle: bool = typer.Option(False, "--power-cycle", help="Auto power-cycle via configured controller (DEFIB_POWER_TYPE)"),
poe_port_override: str = typer.Option("", "--poe-port", help="Explicit MikroTik ether port (e.g. ether3) — overrides comment-based auto-discovery. Requires --power-cycle."),
) -> None:
Expand All @@ -1107,6 +1107,7 @@ async def _agent_upload_async(
from defib.profiles.loader import load_profile
from defib.protocol.hisilicon_cv6xx import HiSiliconCV6xx
from defib.protocol.hisilicon_standard import HiSiliconStandard
from defib.protocol.hisilicon_v500 import HiSiliconV500
from defib.protocol.registry import find_protocol
from defib.recovery.events import ProgressEvent, Stage
from defib.transport.serial_platform import (
Expand Down Expand Up @@ -1149,7 +1150,24 @@ async def _agent_upload_async(
)
return

# HiSiliconStandard / V500 path — needs SoC profile for the SPL+agent
# V500 has no SPL stage and no profile: the agent replaces the boot code
# of a V500 boot image, and the bootrom runs it after the image's own
# DDR-init (aux) code.
if protocol_cls is HiSiliconV500:
await _agent_upload_v500(
chip=chip,
port=port,
output=output,
console=console,
agent_path=agent_path,
agent_data=agent_data,
donor_path=composite_path,
power_cycle=power_cycle,
poe_port_override=poe_port_override,
)
return

# HiSiliconStandard path — needs SoC profile for the SPL+agent
# two-stage upload.
profile = load_profile(chip)
cached_fw = get_cached_path(chip)
Expand Down Expand Up @@ -1394,6 +1412,154 @@ async def _power_cycle_into_handshake(
return hs


# How long agent upload waits for a human to power-cycle a V500 board.
MANUAL_HANDSHAKE_TIMEOUT = 60.0


async def _agent_upload_v500(
*,
chip: str,
port: str,
output: str,
console: Any,
agent_path: Any,
agent_data: bytes,
donor_path: str | None,
power_cycle: bool,
poe_port_override: str = "",
) -> None:
"""Upload the agent to a V500-family SoC inside a donor boot image."""
import asyncio
import json as json_mod
from pathlib import Path

from defib.agent.client import FlashAgentClient
from defib.firmware import download_v500_donor
from defib.power.base import PowerControllerError
from defib.protocol.hisilicon_v500 import (
V500_AGENT_LOAD_ADDR, V500_CHIP_IDS, HiSiliconV500, v500_member,
wrap_v500_payload,
)
from defib.recovery.events import ProgressEvent, Stage
from defib.transport.serial_platform import (
create_transport, normalize_port_name,
)

def fail(message: str) -> NoReturn:
if output == "json":
print(json_mod.dumps({"event": "error", "message": message}))
else:
console.print(f"[red]{message}[/red]")
raise typer.Exit(1)

try:
donor_file = Path(donor_path) if donor_path else download_v500_donor(chip)
wrapped = wrap_v500_payload(
donor_file.read_bytes(), agent_data, V500_AGENT_LOAD_ADDR,
)
except (OSError, ValueError, ConnectionError) as e:
fail(f"Cannot build the V500 boot image: {e}")

power = None
power_port = ""
if power_cycle:
from defib.power.factory import power_controller_from_env
try:
power = power_controller_from_env()
power_port = await _resolve_power_port(power, port, poe_port_override)
except Exception as e:
if power is not None:
await power.close()
fail(f"Power controller error: {e}")

if output == "human":
console.print(f"Agent: [cyan]{agent_path.name}[/cyan] ({len(agent_data)} bytes)")
console.print(f"Boot image: [cyan]{donor_file.name}[/cyan] header + DDR init, "
f"{len(wrapped)} bytes with the agent")
if power is None:
console.print("\n[yellow]Power-cycle the camera now![/yellow]\n")

def on_progress(e: ProgressEvent) -> None:
if e.message:
if output == "human":
console.print(f" {e.message}")
elif output == "json":
print(json_mod.dumps({"event": "progress", "message": e.message}), flush=True)

try:
transport = await create_transport(normalize_port_name(port))
except Exception as e:
if power is not None:
await power.close()
fail(f"Cannot open {port}: {e}")
if power is not None:
_attach_power_transport(power, transport)
try:
protocol = HiSiliconV500()
if power is not None:
def on_power_log(message: str) -> None:
on_progress(ProgressEvent(
stage=Stage.POWER_CYCLE, bytes_sent=0, bytes_total=1, message=message,
))

try:
hs = await _power_cycle_into_handshake(
power, power_port, transport,
lambda: protocol.handshake(transport, on_progress),
on_power_log, proactive=True,
)
except PowerControllerError as e:
fail(f"Power cycle failed: {e}")
finally:
await power.close()
else:
# Without a power controller nobody retries for us; give the
# human a generous window, then report instead of hanging.
try:
hs = await asyncio.wait_for(
protocol.handshake(transport, on_progress),
timeout=MANUAL_HANDSHAKE_TIMEOUT,
)
except asyncio.TimeoutError:
fail(f"No bootrom response within {MANUAL_HANDSHAKE_TIMEOUT:.0f}s")
if not hs.success:
fail("Handshake failed")

# The donor's DDR init is per family member: a gk7205v500 image does
# not bring up a V510's DDR, and then the agent just never answers.
detected = V500_CHIP_IDS.get(hs.chip_id or 0)
if detected and not donor_path and detected != v500_member(chip):
fail(
f"The board answered as a {detected.upper()} (chip ID "
f"0x{hs.chip_id:08x}), not {chip}: its boot image carries the "
f"wrong DDR init. Re-run with -c gk7205{detected}."
)

result = await protocol.send_firmware(transport, wrapped, on_progress)
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
if not result.success:
fail(f"Upload failed: {result.error}")

if output == "human":
console.print("[green]Agent uploaded![/green] Waiting for READY...")

client = FlashAgentClient(transport, chip)
if not await client.connect(timeout=10.0):
fail(
"Agent not responding. The bootrom accepted the image, so the "
"donor's DDR init or the boot entry is the suspect: try "
"-f with the board's own U-Boot as the donor."
)
info = await client.get_info()
if output == "human":
console.print("[green bold]Agent ready![/green bold]")
console.print(f" RAM: 0x{info.get('ram_base', 0):08x}")
console.print(f" Flash: {int(info.get('flash_size', 0)) // 1024}KB")
elif output == "json":
print(json_mod.dumps({"event": "ready", **info}))
finally:
await transport.close()


async def _agent_upload_cv6xx(
*,
chip: str,
Expand Down
33 changes: 32 additions & 1 deletion src/defib/firmware.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,15 @@ def download_firmware(
# Download
name = asset_name(chip)
assert name is not None # firmware_url() is None otherwise
dest = get_cache_dir() / name
return _download(url, get_cache_dir() / name, on_progress)


def _download(
url: str,
dest: Path,
on_progress: Callable[[int, int], None] | None = None,
) -> Path:
"""Fetch ``url`` into ``dest``, removing any partial file on failure."""
logger.info("Downloading firmware from %s", url)

try:
Expand Down Expand Up @@ -287,3 +295,26 @@ def download_firmware(
if isinstance(e, (ValueError, ConnectionError)):
raise
raise ConnectionError(f"Failed to download firmware: {e}") from e


# V500-family (gk7205v500/v510/v530) U-Boot is built in OpenIPC/u-boot-xmedia,
# not OpenIPC/firmware. defib only needs its boot-image header and DDR-init
# (aux) code to carry the flash agent, and those do not depend on the flash
# type, so the smaller NOR build is the donor.
XMEDIA_UBOOT_BASE_URL = (
"https://github.com/OpenIPC/u-boot-xmedia/releases/download/latest"
)


def download_v500_donor(chip: str) -> Path:
"""Fetch (or reuse the cached) V500 U-Boot used as the agent's boot image.

Raises:
ConnectionError: If the download fails.
"""
name = f"u-boot-{_strip_variant(chip).lower()}-nor.bin"
dest = get_cache_dir() / name
if dest.exists():
logger.info("Using cached V500 donor: %s", dest)
return dest
return _download(f"{XMEDIA_UBOOT_BASE_URL}/{name}", dest)
Loading
Loading