Skip to content

agent: add hi3520dv200 + HISFC350 SPI flash driver#96

Merged
widgetii merged 2 commits into
masterfrom
agent-hi3520dv200-hisfc350
May 12, 2026
Merged

agent: add hi3520dv200 + HISFC350 SPI flash driver#96
widgetii merged 2 commits into
masterfrom
agent-hi3520dv200-hisfc350

Conversation

@widgetii
Copy link
Copy Markdown
Member

@widgetii widgetii commented May 12, 2026

Summary

  • Adds support for hi3520dv200 (V1-era HiSilicon DVR/NVR SoC, Cortex-A9, Macronix MX25L25635E 32 MiB NOR on CS1) to the bare-metal flash agent.
  • Introduces a HISFC350 SPI flash driver (`spi_flash_hisfc350.c`) selectable via `SPI_DRIVER=hisfc350` in the per-SoC Makefile stanza. Default stays FMC100 for every other supported SoC.
  • Fixes a long-standing selfupdate cache-coherency bug that affected ARMv7-A in general (the trampoline copy never cleaned D-cache, so the BX target read stale memory; survived on Cortex-A7 by coincidence, deterministically broken on Cortex-A9).
  • Bumps INFO response to v3 with a `flash_mem` field so the host stops hardcoding `0x14000000` (hi3520dv200 has FLASH_MEM at `0x58000000`).

What's new

File Change
`agent/spi_flash_hisfc350.c` (new) NOR-only HISFC350 driver. Probes both CS lines. EAR-based bank-switching for >16 MiB chips.
`agent/main.c` INFO v3 (+`flash_mem`); cache-aware selfupdate + trampoline; widened `addr_readable`/`io_region` for V1 peripheral block.
`agent/uart.c` Preserve bootrom IBRD/FBRD (works on chips where bootrom UART runs off a slow reference); divisor-scaling `set_baud`; bounded FIFO drain; IBRD safety check.
`agent/Makefile` `SPI_DRIVER` selector; hi3520dv200 SoC stanza.
`agent/README.md` Update SoC table; document HISFC350 vs FMC100.
`src/defib/agent/client.py` Parse `flash_mem` from INFO v3+; expose `client.flash_mem`; add hi3520dv200 to `chip_to_agent`.
`src/defib/cli/app.py` Use `client.flash_mem` instead of hardcoded `0x14000000` in `agent install`, `agent read`, flash-doctor.

Why HISFC350 instead of EN4B for 32 MiB chips

EN4B (true 4-byte mode, 0xB7) was tried first on the MX25L25635E. The HISFC350's `GLOBAL_CONFIG.ADDR_MODE_4B` bit latched cleanly, but reads at offsets ≥16 MiB returned a fixed repeating pattern instead of real data — the chip never actually entered 4-byte mode, for reasons we haven't figured out. Switching to WREAR (0xC5) + EAR-banking (3-byte addressing with a 25th-bit selector) makes both halves accessible. The vendor U-Boot driver `hisfc350_spi_mx25l25635e.c` follows the same approach.

Why selfupdate needed cache fixes

The old trampoline did a byte-copy then `BX r3`. On ARMv7-A with write-back D-cache:

  1. Byte writes go to D-cache, not memory.
  2. I-fetch at the destination misses or hits stale lines.
  3. Result: the BX jumps into garbage (or, when binaries happen to be byte-identical, into the right code "by accident").

This worked on the existing Cortex-A7 boards because we rarely deployed a binary with diverging early-startup bytes. On Cortex-A9 (hi3520dv200) it broke deterministically. Fix:

  • The trampoline now walks the destination range invalidating I-cache and cleaning D-cache per line (DCCMVAU + ICIMVAU + DSB + ISB + BX).
  • `handle_selfupdate` also cleans the trampoline location itself before calling the function pointer — without this, the trampoline bytes themselves are in D-cache and the I-fetch reads stale memory.

Verified locally

  • `make -C agent test HOST_CC=gcc` — 5406 / 5406 unit tests pass.
  • `uv run pytest tests/ -x -q --ignore=tests/fuzz` — 490 pass.
  • `uv run ruff check src/ tests/` — clean.
  • `uv run mypy src/defib/ --ignore-missing-imports` — clean.
  • All 10 supported SoCs build: hi3516ev300, hi3516ev200, gk7205v200, gk7205v300, hi3516cv300, hi3516cv500, hi3518ev200, hi3516cv610, hi3519v101, hi3520dv200.

Verified on real hardware (hi3520dv200 on /dev/ttyUSB3, manual power-cycle)

  • Agent uploads via `defib agent upload`, sends READY, reports `JEDEC c22019 / 32768 KB / sector 64 KB / RAM 0x80000000 / FLASH_MEM 0x58000000 / agent v3`.
  • Reads from the lower 16 MiB return real vendor U-Boot bytes; reads from the upper 16 MiB return DIFFERENT real data (bank-switching works end to end).
  • Selfupdate verified end-to-end (no power-cycle needed for subsequent agent updates).

Known limitations / follow-ups

Tracked separately in [#97] (issue opened in this PR sequence). Briefly:

  • High baud (`set_baud` >115200) is rejected by the IBRD safety check on hi3520dv200 because the bootrom hands UART running off a ~2 MHz reference. Vendor U-Boot `board.c` clears bit 13 of `CRG+0xE4` to switch UART onto APB; we tried that but the resulting UART rate didn't match any guess at the actual APB clock. Need to identify the real clock empirically (probably needs a power-cycle relay so we can iterate without manual intervention).
  • Stock-firmware dump and write/erase tests deferred until high-baud works (32 MiB at 115200 ≈ 54 min round-trip).

Test plan

  • On a power-cycle relay-equipped rig: run `defib agent upload -c hi3520dv200 -p $PORT`, confirm JEDEC + 32 MiB.
  • Dump full 32 MiB to a file, verify CRC, store as stock-firmware backup.
  • Erase a known-empty sector (e.g. `0xFE0000`), verify it reads as 0xFF.
  • Write a small known pattern, read back, verify CRC matches.
  • Re-flash the original sector contents, verify chip is back to stock.
  • Test scan over the full 32 MiB.

🤖 Generated with Claude Code

V1-era HiSilicon SoCs (hi3520dv200, also covers hi3518ev100 / hi3535
boards by extension) ship a HISFC350 SPI flash controller with a
register layout that's incompatible with the FMC100 driver shipped
since the agent's introduction. This adds a parallel `spi_flash_hisfc350.c`
implementing the same `spi_flash.h` API, gated by `SPI_DRIVER=hisfc350` in
the per-SoC Makefile stanza so the FMC100 path stays the default for
every other supported SoC.

What's new:

- `agent/spi_flash_hisfc350.c` — NOR-only HISFC350 driver. Probes both
  CS lines (vendor hi3520dv200 reference board puts the chip on CS1).
  For 32 MiB chips, uses the Macronix Extended Address Register
  (WREAR 0xC5) to bank-switch the high 16 MiB into the AHB read window
  — true 4-byte mode (EN4B) was tried but the chip didn't actually
  enter it, even though the controller's GLOBAL_CONFIG.ADDR_MODE_4B
  latched. EAR-banking matches what the vendor U-Boot driver does.

- `agent/main.c`:
  * `addr_readable` whitelisted the V1-era peripheral block (FMC at
    0x10010000, CRG at 0x20030000, UART at 0x20080000) per-SoC via the
    Makefile-supplied bases. `io_region` widened to match so 32-bit
    register reads work via CMD_READ.
  * INFO response bumped to v3, adds a `flash_mem` field at bytes
    24..27. Lets the host stop hardcoding 0x14000000 — hi3520dv200
    has FLASH_MEM at 0x58000000.
  * `handle_selfupdate` now does explicit D-cache clean + I-cache
    invalidate around the trampoline copy. Without this, byte writes
    to 0x80000200 sat in D-cache and the function-pointer call jumped
    to whatever stale memory happened to hold (typically zeros) —
    the agent crashed silently.
  * Trampoline (ARMv7-A only) extended with per-cache-line DCCMVAU +
    ICIMVAU + DSB + ISB before the BX so the freshly-copied bytes at
    LOAD_ADDR are visible to the I-fetch unit. Without these the
    selfupdate path was deterministically broken on Cortex-A9 and
    survived on Cortex-A7 only by coincidence (identical bytes).

- `agent/uart.c`:
  * `uart_init` preserves bootrom IBRD/FBRD instead of recomputing
    from a possibly-wrong UART_CLOCK constant. hi3520dv200 ships UART
    on a slow ~2 MHz reference clock (IBRD=1, FBRD=5) so the bootrom's
    divisors are the only known-good way to keep 115200 working.
  * `uart_set_baud` scales from the captured bootrom divisor so future
    set_baud calls stay clock-agnostic, and bounds the FIFO-drain wait
    so a wrong baud can't permanently hang the agent.
  * IBRD safety check rejects any baud that would compute IBRD<1 (which
    would disable the PL011 baud generator and brick the link until
    power-cycle). Today this caps hi3520dv200 at the bootrom's 115200
    — see roadmap issue for the high-baud follow-up.

- `agent/Makefile`: adds `SPI_DRIVER` selector (default `fmc100`) and
  the hi3520dv200 SoC stanza (Cortex-A9, FLASH_MEM at 0x58000000,
  HISFC350).

- `src/defib/agent/client.py`: parses `flash_mem` from INFO v3+ and
  exposes it as `client.flash_mem` so callers don't hardcode
  0x14000000. Adds hi3520dv200 to `chip_to_agent`. Falls back to
  0x14000000 for older agents that don't report it.

- `src/defib/cli/app.py`: `agent install`, `agent read` and the
  flash-doctor recovery path now use `client.flash_mem` instead of
  the hardcoded constant.

Verified locally:
- 5406 / 5406 agent C unit tests pass.
- 490 Python tests pass; ruff + mypy clean.
- All 10 supported SoCs build cleanly.
- Real hardware (hi3520dv200 board on /dev/ttyUSB3): agent uploads,
  reports correct JEDEC c22019 / 32 MiB / sector 64 KiB / RAM 0x80000000,
  and reads return distinct real data from both the lower and upper
  16 MiB halves (confirming bank-switching works). Selfupdate also
  verified end-to-end on real hardware.

Not verified yet (needs power-cycle relay): high-baud (>115200), full
chip dump, write/erase, scan. Tracking issue captures the full TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vendor U-Boot's hisfc350 driver (and our own previous warm boots) leave
the controller configured for DMA-mode QUAD I/O reads with BUS_CONFIG1
READ_EN=0. After a warm jump (e.g. agent uploaded via U-Boot loady +
go, or a prior agent run that touched the chip), the AHB read window at
FLASH_MEM returns bus garbage instead of flash content — every CRC32
and read collapses to a per-size constant.

Reset to a known state at the top of flash_init:

1. Clear GLOBAL_CONFIG.ADDR_MODE_4B (controller back to 3-byte).
2. Issue EX4B (0xE9) on both CS lines to leave 4-byte chip mode (vendor
   U-Boot's hisfc350 enters 4-byte mode on >16 MiB chips via EN4B).
3. Reprogram BUS_CONFIG1 for AHB STD READ (0x03, READ_EN=1, no dummy,
   IF_TYPE=STD). The mem-mapped read window at FLASH_MEM now serves
   flash bytes again.

Verified end-to-end on hi3520dv200 hardware: agent CRC32 over 8 ranges
including the 16 MiB bank-switch boundary all match vendor U-Boot's
reference, including the full-chip CRC = 0xD5300DFC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@widgetii widgetii merged commit b07ce50 into master May 12, 2026
13 checks passed
@widgetii widgetii deleted the agent-hi3520dv200-hisfc350 branch May 12, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant