quartiq ships no UltraScale+ proxy, so the KU15P .bit must be built from xilinx_bscan_spi.py (Migen + Vivado) after adding the part to the generator's device table. Put the operational steps in the tutorial's Phase 2.5 (where users look for a bitstream); CLAUDE.md just points to it.
14 KiB
Tutorial — from probe detection to SPI flash
This walks through the full bs_explorer flow on a Xilinx Kintex
UltraScale+ KU15P board connected via an FTDI MPSSE probe. The
commands are identical for any FPGA registered in modules/fpga/; only
the IDCODE and BSDL filename change.
Prerequisites
- A JTAG probe physically wired to the target's TCK/TDI/TDO/TMS/TRST.
libftd2xxreachable at runtime (already vendored underlibs/libftd2xx/).- The target's BSDL in
bsdl_files/(KU15P:xcku15p_ffve1517.bsdis bundled). - An entry for the target in
modules/fpga/fpga.c(KU15P is bundled). See Adding a new FPGA below. - For SPI flashing, eventually: a BSCAN proxy bitstream — see the Phase 2.5 caveat at the end.
If your board uses a Digilent JTAG-SMT2 / SMT2-NC module (KCU105,
ZCU102, …), you need the optional Digilent backend: install the Adept
Runtime system-wide and configure with cmake -DBS_ENABLE_DIGILENT=ON ... Plain MPSSE does not work on those modules — see the README and
the Digilent SMT2 block in CLAUDE.md for the why.
Build & launch
mkdir build && cd build
cmake .. && make
./bs/bs
You should see:
Boundary Scan Explorer v2.6.7.1
Based on Viveris jtag-boundary-scanner
3 probe driver(s) available.
Type 'help' or '?' to list commands, 'exit' or Ctrl-D to quit.
<Tab> completes commands.
bs_explorer>
<Tab> completes commands; help <cmd> shows per-command help;
Ctrl-D or exit quits.
1. Detect and open the probe
bs_explorer> jtag_probes
[0] 0x00000000 Digilent USB Device 210308AB06A6
[1] 0x00000300 Digilent: JtagSmt2NC
Open a probe by the index in brackets:
bs_explorer> jtag_open 1
The 0x… value next to each index is the raw probe id and is also
accepted (jtag_open 0x300) — handy in scripts where you'd rather pin
the exact backend than rely on enumeration order.
If jtag_open fails: check lsusb for the probe VID:PID, make
sure the user has access to the USB device (udev rule or group), and
confirm no other process holds the probe (e.g. openocd).
2. Scan the JTAG chain
The fastest path is jtag_autoinit: it scans the chain and
auto-loads every BSDL in bsdl_files/ whose IDCODE matches a device.
bs_explorer> jtag_autoinit
Expected output for a single-FPGA board:
1 device(s) found
Device 0 (04A56093 - XCKU15P_FFVE1517) - BSDL Loaded : ...xcku15p_ffve1517.bsd
If the device count is 0 or the IDCODE is 0xFFFFFFFF/0x00000000:
TDI/TDO are likely swapped, TRST not released, or the probe is
mis-wired. Power-cycle and re-check the harness before going further.
3. Identify the FPGA against the registry
fpga_info walks the chain and matches each IDCODE against the
compile-time registry in modules/fpga/fpga.c:
bs_explorer> fpga_info
Device 0 IDCODE 0x04A56093 -> Xilinx Kintex UltraScale+ XCKU15P [Xilinx UltraScale+]
caveat: CCLK routed via STARTUP primitive (not drivable in EXTEST)
If you get not in registry, add an entry — see
Adding a new FPGA.
fpga_list prints the whole registry without needing a probe.
4. (Optional) Sanity-check the low-level JTAG primitives
Before doing anything fancy, you can verify that bscan_set_ir and
bscan_shift_dr actually move bits correctly on your hardware. Drop
the BSDL state (so jtag_core doesn't fight us on IR caching) and
shift IDCODE manually:
bs_explorer> jtag_open 0 # index from jtag_probes
bs_explorer> jtag_scan # detects devices, does NOT load BSDL
bs_explorer> bscan_set_ir 9 6 # IDCODE opcode (KU15P: 0x09, IR=6 bits)
bs_explorer> bscan_shift_dr 32
DR = 04 A5 60 93 # bytes printed MSB-first
Match → primitives are healthy. Mismatch → wiring or clock issue, fix
that before moving on. The opcode and IR length come from the
fpga_target (and ultimately the BSDL INSTRUCTION_OPCODE block).
5. Read the SPI flash JEDEC ID via EXTEST (validation only)
This is the slow path — useful to confirm the SPI pins are wired correctly to the flash, not a viable way to flash megabytes. See Phase 2.5 for the production path.
Put the FPGA in EXTEST, then map the four SPI signals onto the FPGA's BSDL pin names:
bs_explorer> jtag_autoinit
bs_explorer> jtag_mode 0 EXTEST
bs_explorer> jtag_spi_cs 0 <PIN_CS> 0
bs_explorer> jtag_spi_clk 0 <PIN_CLK> 0
bs_explorer> jtag_spi_mosi 0 <PIN_MOSI> 0
bs_explorer> jtag_spi_miso 0 <PIN_MISO> 0
Pin names depend on the board: dump jtag_pins 0 to discover
them. On Xilinx FPGAs, the SPI flash is typically wired to the
configuration bank (e.g. D00_MOSI_0, D01_DIN_0, FCS_B_0) —
except CCLK, which goes through the STARTUPE3 primitive and is
not drivable in EXTEST (the CCLK_VIA_STARTUP caveat on the target).
Send the JEDEC ID command (0x9F + 3 dummy bytes):
bs_explorer> jtag_spi_xfer 9F000000
SPI TX: 9F 00 00 00
SPI RX: FF XX YY ZZ # XX YY ZZ identifies the flash vendor/part
This takes several seconds even for 4 bytes — that's the ~30 bytes/sec EXTEST ceiling. Don't try to read the whole flash this way; you'd be there for weeks.
6. Add a new FPGA target
The registry in modules/fpga/fpga.c holds the per-part facts that
can't be derived from the BSDL alone (or are tedious to). The XCKU040
entry already there was added exactly with the steps below — use it as
your template.
a. Drop the BSDL
Put the part's .bsd in bsdl_files/. Source: Xilinx/AMD device page
under "Design Files / BSDL", Intel in the Quartus install, Lattice per
part. jtag_autoinit will then auto-load it by IDCODE.
b. Pull the facts out of the BSDL
Everything you need is in the file:
grep -iE "INSTRUCTION_LENGTH|IDCODE_REGISTER|\b(USER1|CFG_IN|JPROGRAM|JSTART|JSHUTDOWN|ISC_DISABLE)\b" \
bsdl_files/xcku040_ffva1156.bsd
For the XCKU040 this yields IR length 6, and the private opcodes
USER1=000010 (0x02), CFG_IN=000101 (0x05), JPROGRAM=001011
(0x0B), JSTART=001100 (0x0C), etc. The IDCODE_REGISTER string is
XXXX...0010 0000 1001 0011 — the top four bits are the silicon
revision and read as X (don't-care), which is why the registry
masks them off.
c. Fill in an fpga_target
| Field | What it is | XCKU040 |
|---|---|---|
name |
human-readable label | "Xilinx Kintex UltraScale XCKU040" |
idcode |
IDCODE pattern (version nibble as 0) | 0x03822093 |
idcode_mask |
bits that must match; 0x0FFFFFFF ignores the Xilinx revision nibble |
0x0FFFFFFF |
family |
FPGA_FAMILY_XILINX_7/US/USP |
FPGA_FAMILY_XILINX_US |
bsdl_filename |
basename in bsdl_files/ |
"xcku040_ffva1156.bsd" |
ir_length |
IR width in bits | 6 |
ir_cfg_in / ir_user1 / ir_jprogram / ir_jstart / ir_jshutdown / ir_isc_disable |
private IR opcodes (0 = N/A) | from the BSDL |
proxy_bitstream |
BSCAN proxy .bit in bscan_proxies/, or NULL |
"bscan_spi_xcku040.bit" |
caveats |
bit-flags for hardware gotchas (see below) | FPGA_CAVEAT_CCLK_VIA_STARTUP |
The resulting entry (verbatim from fpga.c):
{
.name = "Xilinx Kintex UltraScale XCKU040",
.idcode = 0x03822093,
.idcode_mask = 0x0FFFFFFF,
.family = FPGA_FAMILY_XILINX_US,
.bsdl_filename = "xcku040_ffva1156.bsd",
.ir_length = 6,
.ir_cfg_in = 0x05,
.ir_user1 = 0x02,
.ir_jprogram = 0x0B,
.ir_jstart = 0x0C,
.ir_jshutdown = 0x0D,
.ir_isc_disable = 0x16,
.proxy_bitstream = "bscan_spi_xcku040.bit",
.caveats = FPGA_CAVEAT_CCLK_VIA_STARTUP,
},
What caveats means
caveats is a bit-field of FPGA_CAVEAT_* flags (in fpga.h) marking
known hardware gotchas that change how the tool must drive the
part. It is not a free-text note — each flag is something the code
(or you) can branch on. Set it to 0 when the part has none you know
of. fpga_info prints any flag that is set, as a human-readable line.
Currently one flag exists:
FPGA_CAVEAT_CCLK_VIA_STARTUP— on Xilinx 7-Series / UltraScale / UltraScale+, the SPI configuration clock CCLK is not a normal I/O pin: it is routed through theSTARTUP/STARTUPE3primitive and therefore cannot be toggled in EXTEST boundary scan. Practical effect: the slow EXTEST SPI bit-bang can't clock the flash on these parts — you must use the BSCAN proxy (Phase 2.5), where CCLK is driven by the fabric internally and the problem disappears. Set this flag for any 7-Series/US/US+ part.
To introduce a new caveat: add a #define FPGA_CAVEAT_xxx (1u << n)
in fpga.h, OR it into the relevant entries, and (if it should be
visible) print it in cmd_fpga_info in script.c.
d. Rebuild and verify
The registry is compile-time — no runtime registration:
cd build && make
./bs/bs
bs_explorer> jtag_autoinit
bs_explorer> fpga_info # should show your part, family, and any caveats
Phase 2.5: SPI through the BSCAN proxy (bridge bitstream)
Talking to the SPI flash via EXTEST is fine for a JEDEC ID but useless
for real flashing (~30 B/s, days to weeks for a config part). The
production path loads a tiny BSCAN proxy bitstream into the FPGA
fabric, then runs SPI through the USER1 instruction at fabric speed
(~50–200 KB/s). The proxy uses a BSCANE2 primitive to bridge the
USER1 DR shift to the flash pins, and drives CCLK from the fabric
internally — so the STARTUPE3/CCLK problem of EXTEST disappears.
Get the bridge bitstream
Pre-built proxies live in quartiq/bscan_spi_bitstreams (MIT). Drop
the one for your part in bscan_proxies/:
curl -L -o bscan_proxies/bscan_spi_xcku040.bit \
https://raw.githubusercontent.com/quartiq/bscan_spi_bitstreams/master/bscan_spi_xcku040.bit
The registry entry for the part points at this file via its
proxy_bitstream field (e.g. the XCKU040 entry → bscan_spi_xcku040.bit).
When your part isn't pre-built (e.g. the KU15P)
quartiq ships .bit only for the parts its generator knows — it has
no UltraScale+ proxy (its single UltraScale entry is the KU040), so
the KU15P has to be built from source. You need (o)Migen + Vivado
(2022.2; ISE 14.7 for older parts). From a clone of the quartiq repo,
per its README:
python3 -m venv --system-site-packages .venv
./.venv/bin/pip install -r requirements.txt
PATH=$PATH:/opt/Xilinx/Vivado/2022.2/bin \
./.venv/bin/python3 xilinx_bscan_spi.py ...
The XCKU15P first has to be added to the generator's device table (a Migen platform entry) — it's not just a command-line part flag.
Once built, drop bscan_spi_xcku15p.bit into bscan_proxies/ (it's
MIT, like the KU040 — keep bscan_proxies/LICENSE.quartiq) and set the
proxy_bitstream field on the KU15P entry in modules/fpga/fpga.c
(currently NULL).
Load the bridge and talk SPI
bs_explorer> jtag_open 1
bs_explorer> jtag_autoinit
bs_explorer> bscan_load_bitstream 0 bscan_proxies/bscan_spi_xcku040.bit
bs_explorer> bscan_jedec 0
JEDEC ID: 20 BB 19 (manufacturer 0x20, device 0xBB19)
(Validated on a KCU105: 0x20 = Micron, 0xBB19 = MT25QU256, the
board's 1.8 V 256 Mbit config flash. Other boards will show different
device bytes.)
bscan_load_bitstream runs JPROGRAM → CFG_IN → shift → JSTART, which
reconfigures the FPGA fabric: the design currently running on the
part is wiped and replaced by the proxy. This is undone by a
power-cycle (the configuration flash reloads the original design at the
next boot), but be aware the board stops doing whatever it was doing.
bscan_jedec issues the SPI Read Identification command (0x9F,
also called RDID) and reads back 3 bytes — the chip's JEDEC ID. JEDEC
is the standards body (JESD216 / JEP106) that defines this identifier;
every serial flash answers 0x9F the same way:
- byte 0 — manufacturer, a code assigned by JEDEC (
0x20Micron,0xEFWinbond,0xC2Macronix,0x01Cypress/Infineon,0x9DISSI, …); - bytes 1–2 — device ID (memory type + capacity), vendor-specific.
So the JEDEC ID is how you discover which flash is wired up without prior knowledge — Phase 3 will use it to look the part up in a chip database and pull its page/sector sizes and command set.
A sane answer here (manufacturer 0x20 = Micron, the KCU105's config
flash) also confirms the whole proxy path end to end: the
bscan_spi_xfer() framing, the MSB-first bit order, and the TDO
read-latency skew.
The transfer primitive
bscan_spi_xfer(jc, t, tx, txlen, rx, rxlen) in
modules/bscan_spi/bscan_spi.c performs one CS-framed transaction:
clock out txlen MOSI bytes, then read rxlen MISO bytes. It builds
the quartiq/OpenOCD jtagspi DR frame
(marker | bit-count | MOSI | latency-skip | MISO) and matches
OpenOCD's src/flash/nor/jtagspi.c so the same bitstreams work. Generic
flash read/erase/program/verify (Phase 3) will be built on top
of this primitive.
Troubleshooting cheat sheet
| Symptom | Likely cause |
|---|---|
jtag_probes returns nothing |
FTDI not enumerated. Check lsusb, udev permissions, conflicting process. |
jtag_autoinit finds 0 devices |
TDI/TDO swap, TRST held low, voltage mismatch, or chain broken. |
All IDCODEs read 0xFFFFFFFF |
TDO floats high — broken TDO link, wrong voltage reference, or a Digilent SMT2 module being driven via raw FTDI MPSSE (use the Digilent backend instead). |
All IDCODEs read 0x00000000 |
TDO tied low or no clock reaching the target. |
fpga_info says "not in registry" |
Add the part to fpga_registry[]. |
bscan_shift_dr 32 doesn't return the expected IDCODE |
Wrong IR opcode/length, wrong device index, or a multi-device chain (current primitives assume single device). |
jtag_spi_xfer is hopelessly slow |
That's expected via EXTEST — switch to BSCAN proxy (Phase 2.5). |
Where to go from here
helpin the REPL lists all commands;help <cmd>gives details.CLAUDE.mdat the repo root captures the architecture, roadmap and technical decisions (machine-independent).README.mdis the user-facing summary.- The original Viveris library and its docs live untouched under
modules/jtag_core/,modules/bsdl_parser/,modules/bus_over_jtag/.