Capture the plan for the strongest fit of the tool: feed a board netlist + the chain devices' BSDL and auto-generate/run boundary-scan interconnect tests (opens/shorts/stuck-at) + chain integrity. Covers inputs (netlist, BSDL, board.yaml), a new bstest/ layer whose central primitive is a whole-chain boundary register (lifts the current single-device assumption), the test types, the hard parts (safety/ contention, control-cell mapping, multi-device bit order, vector gen), and a 5-phase plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
30 KiB
Project guide for Claude Code
This file is loaded automatically when working on bs_explorer in any
Claude Code session, on any machine where the repo is cloned. Keep
machine-specific or user-specific facts out of here — they belong in
~/.claude/ memory, not in the repo.
Project status, decisions, and roadmap below are durable: update them when reality changes, not for every transient task.
What this project is
bs_explorer is an application layer on top of Viveris's
jtag-boundary-scanner
library (LGPL). End goal: program FPGAs/CPLDs over JTAG from a CLI tool
on a host with an FTDI/Digilent/J-Link probe — Xilinx external SPI
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 (target/,
bscan/, spi_flash/, svf/, probes/, program/, arm_debug/)
sitting alongside the Viveris ones.
Architecture
src/
├── bs/ Application (readline REPL, no business logic)
└── modules/
— Viveris's library (LGPL, unchanged) —
├── jtag_core/ TAP state machine, IR/DR shifts
├── bsdl_parser/ .bsd loader
├── bus_over_jtag/ SPI/I²C/MDIO/parallel mem bit-bang over EXTEST
├── drivers/ FTDI, J-Link, Linux GPIO, LPT, Digilent (optional, dlopen)
├── script/ Script engine (the real UI)
├── config/ Built-in config.script
├── os_interface/ Portable fs/network wrappers
└── natsort/ Natural pin-name sorting
— new (this project) —
├── 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)
├── program/ `program` dispatch: routes a target to its backend by `prog`
└── arm_debug/ ARM7TDMI (EmbeddedICE) debug: halt/resume, Thumb->ARM, memory read (works); flash-write backend TODO
data/ — runtime resources, looked up CWD-relative —
├── 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)
└── scripts/ Example scripts
doc/ Tutorial and longer-form docs (doc/tutorial.md is the end-to-end walkthrough)
CMakeLists.txt Top-level build (binary lands at build/bs)
User interacts with the project through the script engine commands
(jtag_*, set, if, …), exposed via the REPL or piped script files.
Adding a feature usually means adding a new script command in
src/modules/script/script.c and the supporting C in a new module.
Roadmap
| 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/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. |
| 7 | target/ + program/ + arm_debug/ |
structure done; ARM read works, flash-write 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/ (ARM7TDMI EmbeddedICE) does halt/resume, Thumb->ARM, and system-speed memory read — cpu_read/cpu_halt/cpu_resume; validated by dumping an LPC2103's 32 KB flash to Intel HEX. Context save/restore + the arm_flash write backend are TODO. arm-usb-ocd probe profile added. See the ARM-debug design note. |
| 8 | FTDI driver → libftdi1 | done | Replaced the proprietary libftd2xx with open-source libftdi1 (libusb): any VID:PID + auto kernel-detach. Detected an NXP LPC2103 (ARM7TDMI-S, IDCODE 0x4F1F0F0F) over an Olimex ARM-USB-OCD — the probe the old lib couldn't enumerate. Vendored src/libs/libftd2xx removed. |
Move forward phase by phase: validate one with the user before starting
the next. Don't break the validated path
jtag_open → jtag_autoinit → jtag_mode 0 EXTEST while
refactoring.
Key technical decisions
Flashing path: BSCAN proxy, not EXTEST
The Viveris library has bus_over_jtag/spi_over_jtag.c which bit-bangs
SPI on FPGA pins placed in EXTEST. This is not usable for actual
flashing. Each SPI bit costs 2 full boundary-scan-register shifts
through JTAG, plus USB latency on the FTDI side. Order of magnitude:
~30 bytes/s effective, i.e. weeks to flash a 128 MB part. Useful
only for one-shot operations (read JEDEC ID, check wiring).
Real flashing path: load a small "BSCAN proxy" bitstream into the FPGA
fabric via standard JTAG configuration (CFG_IN). The proxy uses a
BSCANE2 primitive to bridge the JTAG USER1 instruction's DR shift
to the physical SPI pins, running at fabric speed (CCLK driven
internally — the STARTUPE3 problem disappears). Realistic throughput
50–200 KB/s, so ~10–40 min for 128 MB. This is what Vivado, OpenOCD's
jtagspi driver, and xc3sprog all do.
Per-target descriptor
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 fpgasub-struct —bsdl_filename, the Xilinx config IR opcodes (cfg_in/user1/jprogram/…),proxy_bitstream,caveatscpusub-struct —debugtransport (EmbeddedICE), work-RAM (ram_base/size) and on-chip flash region (flash_base/size)
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.
FTDI driver on libftdi1, not the proprietary libftd2xx
The FTDI MPSSE driver (drivers/ftdi_jtag/) runs on open-source
libftdi1 (libusb), not FTDI's vendored libftd2xx.a. Why: libftdi1
opens any USB VID:PID (so probes with custom ids — Olimex
ARM-USB-OCD 0x15ba:0x0003, etc. — enumerate with no FT_SetVIDPID
tricks) and auto-detaches the kernel ftdi_sio driver. The MPSSE
command construction, pin map and clocking are unchanged; only the
transport (enumerate/open/read/write/bitmode) moved to ftdi_* calls.
No proprietary binary in the repo. Build dep: pkg-config libftdi1 +
libusb-1.0. Validated by detecting an LPC2103 over an ARM-USB-OCD —
the probe the old lib couldn't even enumerate.
Digilent SMT2 modules need libdjtg, not raw MPSSE
Several Xilinx dev boards (KCU105, ZCU102, …) embed a Digilent JTAG-SMT2 / SMT2-NC for USB-JTAG. Even though it presents a stock FTDI FT232H over USB (VID:PID 0403:6014), it runs a proprietary firmware that does not respond to plain MPSSE commands — TCK toggles but the level-shifters/buffers stay disabled, so TDO floats high ("all ones" symptom). Standard FTDI driver path is dead on these boards.
Workaround: src/modules/drivers/digilent_jtag/ wraps libdjtg/libdmgr
(Digilent Adept Runtime), loaded via dlopen at runtime — no Digilent
binary or header in the repo. Because it's dlopen'd (degrades to "no
probe" if the libs are absent), it costs nothing to build in:
BS_ENABLE_DIGILENT defaults ON on UNIX (<dlfcn.h> required),
disable with -DBS_ENABLE_DIGILENT=OFF. Adept Runtime is only needed
at runtime to actually drive such a probe.
Probe config profiles (data/probes.yaml)
Probe wiring/electrical settings live in data/probes.yaml (parsed by
src/modules/probes/, libyaml), layered on top of the built-in
config.script defaults: a defaults: map applied on every jtag_open
(so opening without a profile is deterministic) plus named profiles:
selected with jtag_open <idx> <profile> (jtag_profiles lists them).
Each key/value is pushed into the script envvar store the driver reads at
open time. The mechanism is driver-agnostic (any set-able probe var),
but today only the FTDI driver (and minimally the Linux-GPIO one) reads
config envvars, so profiles mostly tune FTDI. Motivating case: the
embedded FlashPro on Microsemi kits (FT4232H ch.A) needs ADBUS4 high-Z —
the flashpro profile sets PROBE_FTDI_SET_PIN_DIR_ADBUS4: 0. See the
config-strategy design note below for where this is headed.
Xilinx caveats
On 7-Series / UltraScale / UltraScale+, CCLK is not a regular I/O
pin — it goes through the STARTUPE3 primitive. Cannot be driven
directly in EXTEST. Non-issue once we use the BSCAN proxy (CCLK is
driven by the fabric clock inside the proxy).
The FPGA must be in a configurable state before loading the proxy.
Issue JPROGRAM first to reset, then CFG_IN + shift the bitstream,
then JSTART and check DONE.
Probe / JTAG-link / device config strategy (design note)
Partly built, partly planned. Guiding idea: separate three concerns that are conflated today, and express the shared JTAG-link settings in driver-neutral terms that get resolved per session.
The three concerns
| Layer | What it owns | Where it lives | Applied |
|---|---|---|---|
| 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/targets.yaml (done) |
after IDCODE match |
The smell that motivated this
Frequency has no single home. Only the FTDI driver reads
PROBE_FTDI_TCK_FREQ_KHZ; our Digilent driver hardcodes 4 MHz
(digilent_jtag_drv.c); J-Link/LPT read nothing. And the PROBE_FTDI_*
namespace mixes probe wiring (pin map, ADBUS4) with link properties
(freq, RTCK, TRST timing). Frequency isn't an FTDI fact — it's a link
fact bounded by both the probe and the board/device.
Target model
- Driver-neutral link vars (
tck_khz,rtck,reset, …) set once (by user / probe defaults / device). Each driver consumes them: our Digilent driver reads them directly; the Viveris FTDI driver is fedPROBE_FTDI_TCK_FREQ_KHZvia a thin translation shim at open (no Viveris edit). J-Link/LPT can stay on their defaults. - Resolution: effective
tck = min(user request, probe max, device/ board max). A small session step computes it at open, and re-applies afterjtag_autoinitonce the device (hence its cap / programming method) is known — using thejtag_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.yamlgains an optionalmax_tck_khz;data/targets.yamlgains optionalmax_tck_khz+ aprogmethod tag (proxy_spi/svf).
Phasing
- A (done) — one canonical
JTAG_TCK_FREQ_KHZ(kHz): mirrored toPROBE_FTDI_TCK_FREQ_KHZatjtag_openfor the Viveris FTDI driver, read directly by our Digilent driver; unset → each driver's own default (FTDI 1000, Digilent 4000). Set it viaset, adata/probes.yamlprofile, ordefaults:. (FTDI path hardware-validated; Digilent path untested.) - B (done) — registry
max_tck_khzcaps the clock:jtag_autoinit, after identifying the chain, clampsJTAG_TCK_FREQ_KHZto the smallest device max and re-opens the probe once (via the stored probe id) to apply, then re-scans. Within-cap / unset just report the cap. Effectivetck = min(request, device max); probe-max (min(..., probe max)) is still TODO. - C (done) —
progmethod tag (proxy_spi/svf/none) on each registry entry, inferred when omitted (proxy →proxy_spi; Microsemi/ Lattice →svf); shown bytarget_info/target_listand available viafpga_prog_method_name()for backend dispatch. RTCK generalised as a neutralJTAG_RTCK(mirrored toPROBE_FTDI_JTAG_ENABLE_RTCK, FTDI-only). Reset abstraction deferred — it's a bundle of probe- specific pin/polarity/timing vars with no clean neutral form yet. The actualprogramdispatch command lands with the SVF player.
What exists already: the probe layer (data/probes.yaml) and the
device layer (data/targets.yaml). The new work is the JTAG-link
layer in the middle.
Programming backends: beyond Xilinx external flash (design note)
Not yet implemented — captured so the design is ready. Guiding vision: one generic SVF player as a near-universal backend, with native backends only where streaming / speed / control justify them.
Why two layers
Practically every JTAG-programmable device (Xilinx fabric config,
Lattice MachXO2/3, Microsemi IGLOO2/SmartFusion2, Altera, CPLDs, …) can
be programmed from an SVF file exported by its vendor tool. SVF is a
flat list of SIR/SDR/RUNTEST ops with the vendor's algorithm
already baked in — so a single SVF player, built on our existing
bscan_set_ir / bscan_shift_dr primitives, programs almost anything
with no per-vendor algorithm code.
Native backends are the exception, justified only when you want fine control, speed, or no vendor-export step. Our Xilinx external SPI flash path is the prime example and stays native: load the BSCAN proxy once, then stream raw binary with separate read/erase/program/verify, progress and partial ops. The equivalent SVF ("indirect flash") is huge (tens of MB of ASCII) and inflexible.
Where each path applies
| Target | Recommended path |
|---|---|
| Xilinx external SPI config flash | native proxy (bscan/+spi_flash/, done). SVF works but is bloated. |
| Xilinx fabric config (volatile) | SVF, or our bscan_load_bitstream (equivalent) |
| Lattice MachXO2/3 (internal flash) | SVF (Diamond/Radiant export). Native IEEE-1532 ISC optional, only for a self-contained .jed flow. |
| Microsemi IGLOO2 / SmartFusion2 | SVF/STAPL (Libero/FlashPro export). Native algorithm too complex/proprietary. |
| Altera, CPLDs, misc JTAG | SVF |
The SVF player (the real work)
Status: initial implementation in src/modules/svf/ + the svf_play <file>
command. Supports the single-device subset: SIR/SDR with
TDI/TDO/MASK/SMASK and the masked TDO compare, RUNTEST
(TCK/SCK + SEC), STATE (RESET/IDLE), ENDIR/ENDDR (IDLE only),
HIR/HDR/TIR/TDR (length 0 only), TRST, FREQUENCY. Built on the
bscan_* primitives (shift_ir/shift_dr/tap_reset/idle_cycles).
Validated on the live IGLOO2 with a hand-written IDCODE-check SVF (pass
and a deliberate mismatch). svf_play warms up the FTDI link first (its
first data read after open returns stale FIFO content). Not yet wired
into a generic program dispatch off the prog tag; no multi-device
headers, no non-IDLE end states. Remaining design below.
A player is more than shifting bits. It must handle:
SIR/SDRwithTDI/TDO/MASK— the masked TDO compare is what detects prog/erase failures; that's the main addition over today's primitives;RUNTESTdelays (erase/program waits, in TCK or time) → reusebscan_idle_cycles+ a sleep;HIR/HDR/TIR/TDRheaders/trailers andENDIR/ENDDR/STATE(multi- device chains, TAP state) — a standard subset covers ~all files.
It reuses probe drivers, jtag_core, bscan_set_ir/bscan_shift_dr
and IDCODE/BSDL detection unchanged. STAPL (.stp/.jam) is the
richer native format (a full language → needs an interpreter); more
work, defer — do SVF first.
Registry / dispatch
fpga_target is Xilinx-flavoured today (ir_cfg_in, ir_user1, …).
Add a "programming method" tag (proxy_spi / svf / optional
lattice_isc) selected off the family enum (add
FPGA_FAMILY_LATTICE_*, FPGA_FAMILY_MICROSEMI_*). An SVF player is
family-agnostic and needs no per-part opcodes — it can even run without
a registry entry.
Scope caveat. iCE40 is usually programmed over SPI directly (or one-time NVCM) with minimal JTAG — out of scope for this JTAG-centric tool.
Programming CPUs over JTAG: ARM7/9 via EmbeddedICE (design note)
Memory read works (cpu_read/cpu_halt/cpu_resume on ARM7TDMI
EmbeddedICE): halt, Thumb->ARM switch, system-speed LDMIA read, dumped
to bin/Intel HEX — validated by an LPC2103 32 KB flash dump. Context
save/restore (for clean resume + repeated reads) and the arm_flash
write backend are the remaining work. See "What's left" and the
arm7-debug-dclk-timing note in ~/.claude/ for the cycle-exact timing.
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)
Done: EmbeddedICE scan-chain access, halt/resume, Thumb->ARM, debug-speed
register read/write, and system-speed memory read (cpu_read).
Reliable in a power-on → one-halt → dump flow; reads clobber r0..r14/PC
with no context save/restore, so resume isn't clean and repeated halts in
one session degrade (power-cycle between dumps). Left: register context
save/restore (clean resume + repeated reads), then a per-MCU RAM flash
loader (LPC2xxx, AT91SAM7, …) and the arm_flash write backend. The
registry, dispatch, probe-profile and config layers are ready.
Embedded port (design note)
Not yet implemented — captured for a possible standalone programmer. No hard blocker: standalone JTAG programmers are all MCU-based. The architecture has the right seam (the driver function-pointer table).
Reference target: Arduino GIGA R1 WiFi — STM32H747XI (Cortex-M7 @ 480 MHz + M4 @ 240 MHz, 1 MB RAM, 2 MB flash), WiFi/BT, Arduino/Mbed OS. The comfortable high end. ESP32 is a lighter alternative (WiFi on-die, ~520 KB RAM); avoid bare AVR (RAM too small).
The seam — the MCU is the JTAG master. Replace src/modules/drivers/
with a GPIO bit-bang (or SPI-assisted: MOSI=TDI, MISO=TDO, SCK=TCK,
TMS bit-banged between scans → multi-MHz) driver behind
drv_TX_TMS / drv_TXRX_DATA. Everything above that — jtag_core, the
bscan_set_ir/bscan_shift_dr primitives, the fpga_target registry,
the SVF player and the flash logic — is portable C, reused as-is.
Dropped / replaced on the MCU:
- readline REPL → command interface over WiFi (HTTP/telnet) or USB-CDC;
os_interface(host fs/net) → Mbed OS filesystem + network (real implementations, little rework);- runtime BSDL parsing → pre-baked tables (the registry already does
this);
bsdl_parsercan be left out; libftdi1/ Digilent → gone (the MCU is the probe).
The one real rework: stream, don't malloc. bscan_shift_dr
currently allocates the whole shift buffer (malloc(nbits) — ~19 MB for
a Xilinx proxy). On an MCU the payload (proxy .bit, flash image, SVF)
lives on SD or a USB stick and must be shifted in chunks; WiFi
fetches the file / triggers the job. With an SD/USB source this is
natural (read a block, shift, repeat). 1 MB RAM relaxes buffer sizes but
does not remove the need (a 32 MB image never fits).
Ideal fit: an SVF player streamed from SD — a classic embedded
programmer design, and it matches the small-Lattice/IGLOO2-in-a-PSU use
case (small config, modest SDR vectors). Big Xilinx external-flash
images (huge SVF vectors) stay on the host or a large MCU.
Hardware notes:
- the GIGA R1 has no onboard microSD — add it via the GIGA Display Shield, an SPI SD module, or its USB-A host port (USB mass storage);
- 3.3 V GPIO → level-shift for 1.8 V JTAG targets (e.g. KCU105), same as on the host side.
Boundary-scan board test (design note)
Not yet implemented — captured for the strongest fit of this tool. Guiding idea: feed a board's netlist + the BSDL of each device on the JTAG chain, and automatically generate & run boundary-scan interconnect tests (opens / shorts / stuck-at) plus chain-integrity checks. This is bs_explorer's real niche: a job badly served by open source (OpenOCD does almost no boundary scan; the good tools are commercial — XJTAG, JTAG Technologies — or legacy like UrJTAG) and where proprietary debuggers (ST-LINK, JTAGICE) don't play at all.
What it tests (and doesn't)
Tests the interconnect between BS-accessible pins: drive a net from
one boundary-scan output (EXTEST) and sense it on the other BS pins of
that net. Detects opens (driven value not seen), shorts (two nets read
alike when driven differently), stuck-at, and chain integrity. Cannot
test nets with no BS pin, analog, or logic inside a chip. The Viveris
bus_over_jtag (SPI/I²C/parallel over EXTEST) also enables testing
connected memory as an optional cluster test.
Inputs
- Netlist —
net → [(refdes, pin), …]. Start with a neutral format (CSVnet,refdes,pinor YAML); a KiCad importer later. - BSDL per scannable device (already handled by
bsdl_parser: IDCODE, IR length, boundary register, per-cell function, pin↔cell, control/enable cell + disable value). - Board file (
board.yaml) — the glue: chain order (TDI→TDO),refdes → bsdl, netlist path, power/ground/clock nets to exclude, pull-up/down and series-resistor info, compliance/"safe" pins to force.
Architecture (a new bstest/ layer)
Reuses jtag_core (TAP, IR/DR shift), bsdl_parser, bscan_*, the YAML
config pattern. The central new primitive is a whole-chain boundary
register: today the Viveris pin API is single-device, but board test
must drive/sense all pins of all BS devices in one DR pass — the
chain BSR is the concatenation of every device's boundary register
(others in BYPASS), in the right bit order. This is the enabler, and it
lifts the "single device on the chain" assumption baked into the
current bscan_* primitives — validate it early on a real 2-device
chain.
Layers:
- A. Chain model — ordered devices (BSDL + IDCODE + length), checked
against a live
jtag_scan; map(refdes, pin) → device → BSDL port → global BSR bit(s)(data cell + control/enable cell + disable value). - B. Netlist ingest + net classification — drivable (≥1 BS output/bidir), sense-only, untestable (no BS pin), excluded (power/clock).
- C. Vector generation — give each testable net a unique binary code over N steps (N ≈ log2(#nets) to disambiguate every short; counting sequence). Per step: exactly one driver per net sets its bit, all other pins of the net go Hi-Z, shift the chain BSR (EXTEST), capture the input cells.
- D. Execution — per step, build the full-chain DR, shift, capture.
- E. Diagnosis + report — compare captured vs expected → open / short (with the net pair) / stuck-at / missing device; pass-fail per net + fault list (refdes/pin) + a coverage report (which pins are BS-testable).
The hard parts (honest)
- Safety: EXTEST drives real pins on a powered board → contention risk (a BS driver against a non-BS output). The generator must guarantee one active driver per net per step, tristate the rest, exclude power/clock nets, and honour the BSDL compliance patterns. Treat this as a generator invariant, not an option.
- Control/bidir cells: driving needs the enable cell set, sensing needs Hi-Z — all in the BSDL, but the mapping is the bulk of the work.
- Multi-device bit ordering (TDI-side device shifts last; IR concatenation).
- Pulls / series resistors skew sensing of undriven nets — model from
board.yaml. - Minimal, safe vector generation (counting sequence / adjacency colouring) — well documented, but the "smart" piece.
Phasing
| Phase | Content | Value |
|---|---|---|
| 1 | Chain model + infrastructure test (IDCODE / BYPASS / chain length) | Immediate, mostly on existing code |
| 2 | Whole-chain BSR primitive (drive/sense all pins, multi-device, EXTEST) | The enabler |
| 3 | Netlist ingest + (refdes,pin)→bit mapping + net classification |
— |
| 4 | Interconnect vector gen + execution + open/short/stuck diagnosis + report | The deliverable |
| 5 | Pulls/series, bidir, clusters, connected-memory tests via bus_over_jtag |
Refinement |
External references
- BSCAN proxy bitstreams:
quartiq/bscan_spi_bitstreams(MIT). Pre-built.bitfor many Xilinx parts; Migen sources to rebuild any part that's missing (needs Vivado). Building a proxy that isn't pre-built (e.g. the KU15P) is covered indoc/tutorial.md, Phase 2.5. - Reference host-side implementation:
openocd/src/flash/nor/jtagspi.c. Defines the proxy protocol (header with bit count + CS state, then payload). Don't reinvent — match what OpenOCD does so we share the same bitstreams. - Viveris library docs:
src/modules/jtag_core/headers are well commented.src/modules/config/config.scriptdocuments every runtime variable.
Build & test
mkdir build && cd build && cmake .. && make
./bs/bs # interactive REPL
Build needs libyaml (pkg-config yaml-0.1; Arch libyaml, Debian
libyaml-dev) for the registry, and libftdi1 + libusb-1.0
(pkg-config libftdi1; Arch libftdi, Debian libftdi1-dev) for the
FTDI/Olimex driver. Run bs from the repo root so it finds
data/targets.yaml (and 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
Adept Runtime system-wide (provides libdjtg.so + libdmgr.so).
No automated tests yet. Smoke test = banner appears, exit works.
After changes touching jtag_core, drivers/ftdi_jtag, or the
autoinit flow, manual hardware test required: probe + KU15P board
should scan and load xcku15p_ffve1517.bsd.
The FTDI driver compiles with int-to-pointer-cast warnings (Viveris's code on 64-bit Linux). Don't fix in this repo — that's upstream.
Commit conventions
- Messages in English, lowercase first word, imperative or short descriptive (match existing style: "phase 1: cleanup, REPL polish, README", "translate README to English").
- Title ≤ ~70 chars. Body wrapped ~80 chars, with bullets for what changed. Mention the why for non-obvious choices.
- Sign-off block:
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> - One logical change per commit. Don't bundle README + bugfix + refactor.
What does NOT belong here
- User preferences (tone, language used in chat, "always do X first")
→
~/.claude/ - Machine-local facts (which probes are physically attached, paths
outside the repo, validated-on-this-machine notes) →
~/.claude/ - Transient task state, in-progress decisions → conversation, not file
- Generated artifacts (
build/, downloaded proxy.bitif we choose not to vendor) →.gitignore