target: generalize the registry to FPGAs + CPUs, add program dispatch

Restructure in anticipation of programming ARM CPUs (ARM7/9 via
EmbeddedICE, e.g. over an Olimex ARM-USB-OCD); FPGA path unchanged.

- modules/fpga -> modules/target; fpga_target -> jtag_target with a
  `kind` (fpga|cpu) and grouped fpga/cpu sub-structs; data/targets.yaml
  (env BS_TARGETS); API target_*; commands target_list/target_info
  (kind-aware). Add arm7/arm9 families, arm_flash prog, embeddedice
  debug, and cpu fields (ram_base/size, flash_base/size).
- new program/: `program <dev> <file>` dispatches by the target's prog
  (svf wired; proxy_spi points at the flash workflow; arm_flash -> arm_debug).
- new arm_debug/: EmbeddedICE halt/resume/mem + arm_flash backend
  declared, not implemented yet.
- bscan_* take const jtag_target* and read the fpga sub-struct.
- data/probes.yaml: arm-usb-ocd profile slot; data/targets.yaml: an ARM7
  example entry. Docs + an ARM-debug design note in CLAUDE.md.

Builds; FPGA path re-validated on the IGLOO2 (target_list shows the CPU
example; jtag_open/autoinit/program 0 <svf> all work).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:33:58 +02:00
parent d1bdce91dc
commit 9ad776268e
20 changed files with 973 additions and 582 deletions

110
CLAUDE.md
View File

@@ -18,8 +18,9 @@ configuration flash via a BSCAN proxy (started with the KU15P), and
other families (Lattice, Microsemi, …) by playing a vendor-exported SVF.
The Viveris library itself lives unchanged in `src/modules/`. Everything
new is in `src/bs/` (the REPL) and the project modules (`fpga/`, `bscan/`,
`spi_flash/`, `svf/`, `probes/`) sitting alongside the Viveris ones.
new is in `src/bs/` (the REPL) and the project modules (`target/`,
`bscan/`, `spi_flash/`, `svf/`, `probes/`, `program/`, `arm_debug/`)
sitting alongside the Viveris ones.
## Architecture
@@ -38,14 +39,16 @@ src/ — code + libs —
├── os_interface/ Portable fs/network wrappers
└── natsort/ Natural pin-name sorting
— new (this project) —
├── fpga/ Registry loader (parses data/fpga_registry.yaml, libyaml)
├── target/ Target registry: FPGAs + CPUs (parses data/targets.yaml)
├── bscan/ JTAG TAP primitives (set_ir/shift_ir/shift_dr/tap_reset/
│ idle_cycles) + BSCAN proxy (bitstream load, SPI-over-USER1)
├── spi_flash/ SPI NOR chip DB + read/erase/program/verify over a callback
├── svf/ SVF player (svf_play): SIR/SDR/RUNTEST/STATE, masked compare
── probes/ Probe-config profiles loader (parses data/probes.yaml, libyaml)
── probes/ Probe-config profiles loader (parses data/probes.yaml, libyaml)
├── program/ `program` dispatch: routes a target to its backend by `prog`
└── arm_debug/ ARM (EmbeddedICE) debug + flash backend (not implemented yet)
data/ — runtime resources, looked up CWD-relative —
├── fpga_registry.yaml FPGA registry (IDCODE BSDL, IR opcodes, proxy, caveats, prog, max_tck)
├── targets.yaml Target registry (FPGAs + CPUs: IDCODE, BSDL/proxy, debug, flash, prog)
├── probes.yaml Probe-config profiles (defaults + per-probe overrides)
├── bsdl_files/ BSDL files for target FPGAs
├── bscan_proxies/ BSCAN proxy bitstreams (MIT, from quartiq)
@@ -64,12 +67,13 @@ Adding a feature usually means adding a new script command in
| Phase | Module | Status | Summary |
|-------|--------|--------|---------|
| 1 | `bs/` cleanup, REPL polish, README | **done** (commit `7cb3627`) | Fix format-strings, delete dead code, tab-completion, banner |
| 2 | `fpga/` | **done** (commit `545fe09`) | Per-target descriptor (IDCODE, BSDL, IR codes, proxy path, caveats). Now a **runtime YAML** registry (`data/fpga_registry.yaml`, libyaml), later gaining `prog` method + `max_tck_khz`. |
| 2 | `fpga/` | **done** (commit `545fe09`) | Per-target descriptor (IDCODE, BSDL, IR codes, proxy path, caveats). Now a **runtime YAML** registry (`data/targets.yaml`, libyaml), later gaining `prog` method + `max_tck_khz`. |
| 2.5 | `bscan/` | **done** (commit `dec0d14`) | Load BSCAN proxy bitstream via `CFG_IN`, expose fast `bscan_spi_xfer()` via `USER1`. Required for realistic flashing speeds. |
| 3 | `spi_flash/` | **done** (commit `c4afe87`) | Chip DB (JEDEC ID → page/sector/cmd set) + generic `read/erase/program/verify` over an `xfer` callback. detect+read validated on KCU105; erase/program implemented but not yet hardware-tested. |
| 4 | script commands | **done** (commit `d6f843e`) | `flash_detect`, `flash_read` (+file), `flash_erase`, `flash_write`, `flash_verify`. Full set validated on KCU105 (save/erase/write-random/verify/restore round-trip). ~100 KB/s write once the proxy is loaded. |
| 5 | `probes/` + JTAG-link | **done** | `data/probes.yaml` probe-config profiles (`jtag_open <idx> <profile>`, `jtag_profiles`, `jtag_close`); driver-neutral `JTAG_TCK_FREQ_KHZ`/`JTAG_RTCK`; device `max_tck_khz` clock cap resolved at `jtag_autoinit`; `prog` method tag. See the config-strategy design note. Validated on the IGLOO2 (FlashPro). |
| 6 | `svf/` | **done** (subset, commit `c77d86e`) | SVF player + `svf_play`: SIR/SDR with masked TDO compare, RUNTEST, STATE — single-device. Validated on the IGLOO2 IDCODE; a real Libero SVF and a generic `program` dispatch off the `prog` tag are still TODO. |
| 6 | `svf/` | **done** (subset, commit `c77d86e`) | SVF player + `svf_play`: SIR/SDR with masked TDO compare, RUNTEST, STATE — single-device. Validated on the IGLOO2 IDCODE. |
| 7 | `target/` + `program/` + `arm_debug/` | **structure done; ARM impl TODO** | Generalized `fpga/` into a kind-aware `target/` registry (FPGA \| CPU). `program <dev> <file>` dispatches by `prog` (svf wired; proxy_spi points at the flash workflow). `arm_debug/` (EmbeddedICE) + `arm_flash` backend are declared but not implemented; `arm-usb-ocd` probe profile added. FPGA path re-validated on the IGLOO2. See the ARM-debug design note. |
Move forward phase by phase: validate one with the user before starting
the next. Don't break the validated path
@@ -95,25 +99,25 @@ internally — the `STARTUPE3` problem disappears). Realistic throughput
50200 KB/s, so ~1040 min for 128 MB. This is what Vivado, OpenOCD's
`jtagspi` driver, and `xc3sprog` all do.
### Per-FPGA descriptor
### Per-target descriptor
`fpga_target` struct (Phase 2) holds the per-target facts that can't be
derived from the BSDL alone:
- `idcode` + `idcode_mask` — match the device on the chain
- `bsdl_filename` — BSDL to auto-load
- `cfg_in_ir_code`, `user1_ir_code`, `jprogram_ir_code` — Xilinx-specific
private IR opcodes (read from BSDL when available)
- `proxy_bitstream_path` — path to the BSCAN proxy `.bit` for this part
- `caveats` — flags for known hardware gotchas (e.g. CCLK via STARTUPE3)
The `jtag_target` struct (`target/target.h`) holds the per-target facts
that can't be derived from the IDCODE/BSDL alone. A `kind` (fpga|cpu)
selects a sub-struct; the rest is shared:
- common — `idcode`+`idcode_mask` (chain match), `family`, `ir_length`,
`prog` (programming backend), `max_tck_khz`
- `fpga` sub-struct — `bsdl_filename`, the Xilinx config IR opcodes
(`cfg_in`/`user1`/`jprogram`/…), `proxy_bitstream`, `caveats`
- `cpu` sub-struct — `debug` transport (EmbeddedICE), work-RAM
(`ram_base`/`size`) and on-chip flash region (`flash_base`/`size`)
Registry is a **runtime YAML file** (`data/fpga_registry.yaml` at the repo
root), parsed via libyaml — no longer a compile-time array. It is looked
up CWD-relative (like `data/bsdl_files/`), overridable with
`$BS_FPGA_REGISTRY`, and loaded lazily on first access. Adding a part =
one YAML entry + its `.bsd` in `data/bsdl_files/` + (optionally) its proxy
`.bit` in `data/bscan_proxies/` — no rebuild. The `family` field accepts
`xilinx_*`, `microsemi_igloo2/smartfusion2`, `lattice_machxo2/3`
(enum in `fpga.h`).
Registry is a **runtime YAML file** (`data/targets.yaml`), parsed via
libyaml — looked up CWD-relative (like `data/bsdl_files/`), overridable
with `$BS_TARGETS`, loaded lazily. Adding a target = one flat YAML entry
(+ a `.bsd`/proxy for FPGAs) — no rebuild. `family` accepts `xilinx_*`,
`microsemi_*`, `lattice_*`, `arm7`/`arm9`; `prog` is
`proxy_spi`/`svf`/`arm_flash`/`none` (inferred when omitted). Enums in
`target.h`.
### Digilent SMT2 modules need libdjtg, not raw MPSSE
@@ -171,7 +175,7 @@ driver-neutral terms that get resolved per session.**
|-------|-------------|----------------|---------|
| **Probe (sonde)** | driver + interface, pin map, buffer-enable, TRST/SRST pins, level-shift, *max TCK the adapter supports* | `data/probes.yaml` (done) | at `jtag_open` |
| **JTAG link** | TCK freq, RTCK, reset behaviour, chain layout — **driver-neutral names**, resolved to effective values | *missing today* | open, then refined after detect |
| **Device** | IDCODE/BSDL/IR/proxy/caveats, programming method, *max TCK the part/board tolerates* | `data/fpga_registry.yaml` (done) | after IDCODE match |
| **Device** | IDCODE/BSDL/IR/proxy/caveats, programming method, *max TCK the part/board tolerates* | `data/targets.yaml` (done) | after IDCODE match |
### The smell that motivated this
@@ -195,7 +199,7 @@ fact bounded by both the probe and the board/device.
method) is known — using the `jtag_close`→reopen seam if a re-init is
needed. This also dissolves the chicken-and-egg: a conservative link
default gets you to detection, then the device refines it.
- `data/probes.yaml` gains an optional `max_tck_khz`; `data/fpga_registry.yaml`
- `data/probes.yaml` gains an optional `max_tck_khz`; `data/targets.yaml`
gains optional `max_tck_khz` + a `prog` method tag (`proxy_spi`/`svf`).
### Phasing
@@ -213,7 +217,7 @@ fact bounded by both the probe and the board/device.
still TODO.
- **C (done)** — `prog` method tag (`proxy_spi`/`svf`/`none`) on each
registry entry, inferred when omitted (proxy → `proxy_spi`; Microsemi/
Lattice → `svf`); shown by `fpga_info`/`fpga_list` and available via
Lattice → `svf`); shown by `target_info`/`target_list` and available via
`fpga_prog_method_name()` for backend dispatch. RTCK generalised as a
neutral `JTAG_RTCK` (mirrored to `PROBE_FTDI_JTAG_ENABLE_RTCK`,
FTDI-only). Reset abstraction deferred — it's a bundle of probe-
@@ -221,7 +225,7 @@ fact bounded by both the probe and the board/device.
actual `program` dispatch command lands with the SVF player.
What exists already: the **probe layer** (`data/probes.yaml`) and the
**device layer** (`data/fpga_registry.yaml`). The new work is the **JTAG-link
**device layer** (`data/targets.yaml`). The new work is the **JTAG-link
layer** in the middle.
## Programming backends: beyond Xilinx external flash (design note)
@@ -298,6 +302,52 @@ a registry entry.
one-time NVCM) with minimal JTAG — out of scope for this JTAG-centric
tool.
## Programming CPUs over JTAG: ARM7/9 via EmbeddedICE (design note)
Structure in place (`target/` kind=cpu, `program/` dispatch, `arm_debug/`
+ `arm_flash` declared); the debug/flash code is the next real work.
### Why CPUs are a different shape
A CPU isn't configured like an FPGA — there's no bitstream or BSDL pin
map to drive. You **halt the core through its debug unit, write a small
flash loader into on-chip RAM, run it to program the internal flash, and
verify** — the OpenOCD approach. So the target carries different facts
(captured in the `cpu` sub-struct): the debug transport, a work-RAM
region for the loader, and the flash region.
### The debug seam (`arm_debug/`)
ARM7/ARM9 expose **EmbeddedICE** on a JTAG TAP: a scan chain (DSCR, DCC,
breakpoint regs) reached via the standard IR/DR shifts. The seam is four
primitives — `halt`, `resume`, `mem_read`, `mem_write` — built on the
existing `bscan_*` IR/DR functions; everything else (the RAM loader, the
per-MCU flash algorithm) sits on top. Cortex-M is the same idea behind a
*different* transport (ADIv5/DAP, and SWD which isn't JTAG); it would be
a second `debug` value behind the same seam, not a rewrite.
### Backend dispatch (`program/`)
`program <dev> <file>` looks the device up in the registry and routes by
its `prog` tag: `svf`→SVF player, `proxy_spi`→the Xilinx flash workflow,
`arm_flash`→`arm_debug`'s RAM-loader flash. Adding a backend = one `prog`
value + one case. This is the seam that makes new target classes drop in
without touching the REPL.
### Probe
ARM-USB-OCD is an Olimex **FT2232** (OpenOCD-class) adapter — the
existing FTDI driver drives it; it just needs a `probes.yaml` profile
(`arm-usb-ocd`) with the Olimex control-pin map (TRST/SRST, buffer
enable). The profile slot exists; the exact pin numbers still need
filling from the Olimex schematic / OpenOCD's interface config.
### What's left (the implementation)
EmbeddedICE scan-chain access + halt/resume + memory R/W, then a per-MCU
RAM flash loader (LPC2xxx, AT91SAM7, …) and the `arm_flash` backend. The
registry, dispatch, probe-profile and config layers are ready for it.
## Embedded port (design note)
Not yet implemented — captured for a possible standalone programmer.
@@ -365,9 +415,9 @@ mkdir build && cd build && cmake .. && make
```
Build needs **libyaml** (pkg-config `yaml-0.1`; Arch `libyaml`, Debian
`libyaml-dev`) — the FPGA registry is parsed from `data/fpga_registry.yaml`
`libyaml-dev`) — the FPGA registry is parsed from `data/targets.yaml`
at runtime. Run `bs` from the repo root so it finds that file (and
`data/bsdl_files/`, `data/bscan_proxies/`), or point `$BS_FPGA_REGISTRY` at it.
`data/bsdl_files/`, `data/bscan_proxies/`), or point `$BS_TARGETS` at it.
The Digilent SMT2 backend is built by default on UNIX (disable with
`-DBS_ENABLE_DIGILENT=OFF`). To actually use such a probe, install the