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
39 changes: 30 additions & 9 deletions agent/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,32 @@ else ifeq ($(SOC),hi3518ev200)
CRG_BASE = 0x12010000
SYSCTRL_REBOOT = 0x12020004
else ifeq ($(SOC),hi3516cv610)
UART_BASE = 0x11040000
UART_CLOCK = 24000000
LOAD_ADDR = 0x41000000
FLASH_MEM = 0x14000000
FMC_BASE = 0x10000000
RAM_BASE = 0x40000000
WDT_BASE = 0x12030000
CRG_BASE = 0x12010000
SYSCTRL_REBOOT = 0x12020004
# CV6xx family (cv608, cv610, dv500, dv500). The CV6xx fastboot
# bootrom treats the final-stage payload as U-Boot and parses its
# 1024-byte header: the code_offset field at byte 8 is 0x400, so
# the bootrom jumps to load_addr + 0x400. The agent must therefore
# be linked at 0x41000000 + 0x400 = 0x41000400 to match where its
# first instruction actually lands after the wrap.
# CRG/peripheral block moved to 0x11xxxxxx; FMC clock control sits
# at CRG+0x3F40 with the enable on bit 4 (not CRG+0x144/bit1 like
# V3/V4 chips). UART0 confirms the 0x11xxxxxx range from the
# vendor user guide and from the running kernel's earlycon arg.
UART_BASE = 0x11040000
UART_CLOCK = 24000000
LOAD_ADDR = 0x41000400
FLASH_MEM = 0x0F000000
FMC_BASE = 0x10000000
RAM_BASE = 0x40000000
WDT_BASE = 0x11030000
CRG_BASE = 0x11010000
SYSCTRL_REBOOT = 0x11020004
FMC_CRG_OFFSET = 0x3F40
FMC_CRG_CLK_BIT = 4
# CV6xx moved the SPI pinctrl block from 0x100C0000 to 0x10260000
# and changed register offsets + pad-drive values. The values come
# from the dimerr u-boot-hi3516cv6xx fork (drivers/mtd/fmc_hi3516cv610.c).
SPI_PIN_BASE = 0x10260000
SPI_PINS_CV6XX = 1
else ifeq ($(SOC),hi3519v101)
# V3A generation (3519v101 family: hi3519v101, hi3516av200) — Cortex-A7
# with V3-era peripheral addresses (UART 0x12100000, WDT 0x12080000)
Expand Down Expand Up @@ -183,6 +200,10 @@ CFLAGS = -mcpu=$(CPU_TYPE) -marm -O2 -ffreestanding -nostdlib \
$(if $(SYSCTRL_BASE),-DSYSCTRL_BASE=$(SYSCTRL_BASE)) \
$(if $(BOOTROM_BASE),-DBOOTROM_BASE=$(BOOTROM_BASE) -DBOOTROM_SIZE=$(BOOTROM_SIZE)) \
$(if $(UART_CKSEL_REG),-DUART_CKSEL_REG=$(UART_CKSEL_REG) -DUART_CKSEL_BIT=$(UART_CKSEL_BIT)) \
$(if $(FMC_CRG_OFFSET),-DFMC_CRG_OFFSET=$(FMC_CRG_OFFSET)) \
$(if $(FMC_CRG_CLK_BIT),-DFMC_CRG_CLK_BIT=$(FMC_CRG_CLK_BIT)) \
$(if $(SPI_PIN_BASE),-DSPI_PIN_BASE=$(SPI_PIN_BASE)) \
$(if $(SPI_PINS_CV6XX),-DSPI_PINS_CV6XX=1) \
-mno-unaligned-access -Wall -Wextra

LDFLAGS = -nostdlib -T link.ld -Ttext=$(LOAD_ADDR) --defsym=LOAD_ADDR=$(LOAD_ADDR)
Expand Down
46 changes: 38 additions & 8 deletions agent/spi_flash.c
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,20 @@
#define NAND_PAGE_SIZE 2048
#define NAND_BLOCK_SIZE (64 * NAND_PAGE_SIZE) /* 128 KiB */

/* CRG register for FMC clock — CRG_BASE is per-SoC (set via -DCRG_BASE=...) */
#define REG_FMC_CRG (*(volatile uint32_t *)(CRG_BASE + 0x0144))
#define FMC_CLK_ENABLE (1 << 1)
#define FMC_SOFT_RESET (1 << 0)
/* CRG register for FMC clock — CRG_BASE + per-family offset and bit
* positions. The V3/V4 default (offset 0x0144, clock-enable on bit 1,
* soft-reset on bit 0) is overridden via Makefile for chips where the
* CRG layout differs — notably CV6xx (offset 0x3F40, clock-enable bit
* 4) which moved every peripheral CRG entry. */
#ifndef FMC_CRG_OFFSET
#define FMC_CRG_OFFSET 0x0144
#endif
#ifndef FMC_CRG_CLK_BIT
#define FMC_CRG_CLK_BIT 1
#endif
#define REG_FMC_CRG (*(volatile uint32_t *)(CRG_BASE + FMC_CRG_OFFSET))
#define FMC_CLK_ENABLE (1u << FMC_CRG_CLK_BIT)
#define FMC_SOFT_RESET (1u << 0)

/* SPI timing: TCSH=6 [15:12], TCSS=6 [11:8], TSHSL=0xF [7:0] */
#define SPI_TIMING_VAL ((6 << 12) | (6 << 8) | 0xF) /* 0x660F */
Expand All @@ -142,20 +152,40 @@ static void fmc_wait_ready(void);
static void spi_wait_wip(void);

/* Mode switching: normal mode for register commands, boot mode for reads */
/* I/O pad configuration base for SPI flash pins */
#define IO_BASE 0x100C0000
#define io_reg(off) (*(volatile uint32_t *)(IO_BASE + (off)))
/* I/O pad configuration base for SPI flash pins. Per-family override
* via -DSPI_PIN_BASE=...; default targets V3/V4 chips. */
#ifndef SPI_PIN_BASE
#define SPI_PIN_BASE 0x100C0000
#endif
#define io_reg(off) (*(volatile uint32_t *)(SPI_PIN_BASE + (off)))

static void fmc_enter_normal(void) {
/* Full FMC init for register-mode operations (matching U-Boot) */

/* Configure SPI flash I/O pads (SPL may have left them in boot-mode config) */
/* Configure SPI flash I/O pads (SPL may have left them in boot-mode
* config). CV6xx moved the pinctrl block from 0x100C0000 to
* 0x10260000 and uses different register offsets + values; the
* dimerr/u-boot-hi3516cv610 driver fmc_hi3516cv610.c is the source
* of truth for the CV6xx values. */
#ifdef SPI_PINS_CV6XX
/* CV6xx layout (per drivers/mtd/fmc_hi3516cv610.c, 3.3V single-CS):
* 0x0C MOSI_IO0, 0x10 CLK, 0x14 HOLD_IO3, 0x18 CS0,
* 0x1C MISO_IO1, 0x20 WP_IO2. */
io_reg(0x0C) = 0x1261; /* sfc_mosi_io0 */
io_reg(0x10) = 0x1291; /* sfc_clk */
io_reg(0x14) = 0x1161; /* sfc_hold_io3 */
io_reg(0x18) = 0x1131; /* sfc_cs0 */
io_reg(0x1C) = 0x1261; /* sfc_miso_io1 */
io_reg(0x20) = 0x12E1; /* sfc_wp_io2 */
#else
/* V3/V4 layout (hi3516ev300 / cv500 / gk7205 / ...) */
io_reg(0x14) = 0x401; /* sfc_clk */
io_reg(0x18) = 0x461; /* sfc_hold_io0 */
io_reg(0x1C) = 0x461; /* sfc_miso_io1 */
io_reg(0x20) = 0x461; /* sfc_wp_io2 */
io_reg(0x24) = 0x461; /* sfc_mosi_io3 */
io_reg(0x28) = 0x461; /* sfc_csn */
#endif

fmc_reg(FMC_CFG) = 0x1821; /* OP_MODE_NORMAL | bootrom defaults */
fmc_reg(FMC_SPI_TIMING_CFG) = SPI_TIMING_VAL;
Expand Down
1 change: 1 addition & 0 deletions src/defib/agent/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def get_agent_binary(chip: str) -> Path | None:
"hi3519v101": "hi3519v101",
"hi3516av200": "hi3519v101", # 3519v101 family, same memory map
"hi3516cv610": "hi3516cv610",
"hi3516cv608": "hi3516cv610", # cv6xx-family, same memory map
"hi3518ev200": "hi3518ev200",
"hi3520dv200": "hi3520dv200", # V1-era, HISFC350 SPI controller
}
Expand Down
179 changes: 176 additions & 3 deletions src/defib/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from typing import Any

import typer

app = typer.Typer(
Expand Down Expand Up @@ -999,21 +1001,29 @@ 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"),
power_cycle: bool = typer.Option(False, "--power-cycle", help="Auto power-cycle via configured controller (DEFIB_POWER_TYPE)"),
) -> None:
"""Upload flash agent to device via boot protocol (requires power-cycle)."""
import asyncio
asyncio.run(_agent_upload_async(chip, port, output))
asyncio.run(_agent_upload_async(chip, port, output, file, power_cycle))


async def _agent_upload_async(chip: str, port: str, output: str) -> None:
async def _agent_upload_async(
chip: str, port: str, output: str,
composite_path: str | None = None,
power_cycle: bool = False,
) -> None:
import json as json_mod

from rich.console import Console

from defib.agent.client import FlashAgentClient, get_agent_binary
from defib.firmware import get_cached_path
from defib.profiles.loader import load_profile
from defib.protocol.hisilicon_cv6xx import HiSiliconCV6xx
from defib.protocol.hisilicon_standard import HiSiliconStandard
from defib.protocol.registry import find_protocol
from defib.recovery.events import ProgressEvent
from defib.transport.serial_platform import (
create_transport, normalize_port_name,
Expand All @@ -1033,7 +1043,27 @@ async def _agent_upload_async(chip: str, port: str, output: str) -> None:

agent_data = agent_path.read_bytes()

# Get real SPL from cached U-Boot
# Dispatch on protocol family. CV6xx fastboot expects a single
# composite (GSL + DDR + U-Boot-shaped) blob — the agent ships in
# place of the U-Boot section, wrapped in a 1024-byte header so the
# bootrom jumps to LOAD_ADDR + 0x400 where the agent's first
# instruction is linked.
protocol_cls = find_protocol(chip)
if protocol_cls is HiSiliconCV6xx:
await _agent_upload_cv6xx(
chip=chip,
port=port,
output=output,
console=console,
agent_path=agent_path,
agent_data=agent_data,
composite_path=composite_path,
power_cycle=power_cycle,
)
return

# HiSiliconStandard / V500 path — needs SoC profile for the SPL+agent
# two-stage upload.
profile = load_profile(chip)
cached_fw = get_cached_path(chip)
if not cached_fw:
Expand Down Expand Up @@ -1127,6 +1157,149 @@ def on_progress(e: ProgressEvent) -> None:
await transport.close()


async def _agent_upload_cv6xx(
*,
chip: str,
port: str,
output: str,
console: Any,
agent_path: Any,
agent_data: bytes,
composite_path: str | None,
power_cycle: bool,
) -> None:
"""CV6xx fastboot agent upload — single-composite, no separate SPL."""
import asyncio
import json as json_mod

from defib.agent.client import FlashAgentClient
from defib.power.factory import power_controller_from_env
from defib.power.vectis import VectisController
from defib.protocol.hisilicon_cv6xx import (
HiSiliconCV6xx, parse_cv6xx_boot, wrap_cv6xx_payload,
)
from defib.recovery.events import ProgressEvent
from defib.transport.rfc2217 import Rfc2217Transport
from defib.transport.serial_platform import (
create_transport, normalize_port_name,
)

if not composite_path:
msg = (
f"CV6xx chip '{chip}' needs a composite boot file (GSL+DDR+U-Boot) "
"to derive the bootrom-loadable header — pass it via -f/--file."
)
if output == "json":
print(json_mod.dumps({"event": "error", "message": msg}))
else:
console.print(f"[red]{msg}[/red]")
raise typer.Exit(2)

composite = open(composite_path, "rb").read()
try:
parts = parse_cv6xx_boot(composite)
except Exception as e:
msg = f"Failed to parse composite '{composite_path}': {e}"
if output == "json":
print(json_mod.dumps({"event": "error", "message": msg}))
else:
console.print(f"[red]{msg}[/red]")
raise typer.Exit(2)

wrapped = wrap_cv6xx_payload(parts.uboot_data, agent_data)

if output == "human":
console.print(f"Agent: [cyan]{agent_path.name}[/cyan] ({len(agent_data)} bytes)")
console.print(f"Composite: [cyan]{composite_path}[/cyan] ({len(composite)} bytes)")
console.print(f"Wrapped payload: {len(wrapped)} bytes (1024 B header + agent)")

power = power_controller_from_env() if power_cycle else None

transport = await create_transport(normalize_port_name(port))
if power is not None and isinstance(power, VectisController) and isinstance(transport, Rfc2217Transport):
power.attach_transport(transport)

protocol = HiSiliconCV6xx()

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)

# Spawn handshake before reset — same ordering as RecoverySession's
# proactive-blast path (see #109).
if output == "human" and power is not None:
console.print(f"Power-cycling via {power.name()}...")
elif output == "human":
console.print("\n[yellow]Power-cycle the camera now![/yellow]\n")

await transport.flush_input()
hs_task = asyncio.create_task(protocol.handshake(transport, on_progress))
if power is not None:
await asyncio.sleep(0.05)
try:
await power.power_cycle("")
except Exception as e:
hs_task.cancel()
try:
await hs_task
except BaseException:
pass
if output == "json":
print(json_mod.dumps({"event": "error", "message": f"Power cycle failed: {e}"}))
else:
console.print(f"[red]Power cycle failed:[/red] {e}")
await transport.close()
raise typer.Exit(1)

hs = await hs_task
if not hs.success:
if output == "json":
print(json_mod.dumps({"event": "error", "message": "Handshake failed"}))
else:
console.print("[red]Handshake failed[/red]")
await transport.close()
raise typer.Exit(1)

result = await protocol.send_firmware(
transport, composite, on_progress, uboot_override=wrapped,
)
if not result.success:
if output == "json":
print(json_mod.dumps({"event": "error", "message": result.error or "Upload failed"}))
else:
console.print(f"[red]Upload failed:[/red] {result.error}")
await transport.close()
raise typer.Exit(1)

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

client = FlashAgentClient(transport, chip)
ready = await client.connect(timeout=10.0)
if ready:
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}))
else:
if output == "json":
print(json_mod.dumps({"event": "error", "message": "Agent not responding"}))
else:
console.print("[red]Agent not responding[/red]")
await transport.close()
raise typer.Exit(1)

await transport.close()
if power is not None:
await power.close()


@agent_app.command("flash")
def agent_flash(
chip: str = typer.Option(..., "-c", "--chip", help="Chip model name"),
Expand Down
Loading
Loading