20 KiB
Tutorial — from probe to programmed part
This walks through the full bs_explorer flow: detect a probe, scan the
chain, identify the device, and program it. There are two programming
paths and the tutorial covers both:
- Xilinx external SPI configuration flash, via the BSCAN proxy — shown on a Kintex UltraScale+ KU15P / UltraScale KU040;
- any other part (Lattice, Microsemi, …), by playing a vendor-exported SVF — shown on a Microsemi IGLOO2 M2GL010T.
The early steps are identical for any device in data/targets.yaml;
only the IDCODE and BSDL filename change.
Prerequisites
- A JTAG probe physically wired to the target's TCK/TDI/TDO/TMS/TRST.
libftdi1+libusb-1.0installed (Arch:libftdi; Debian:libftdi1-dev) — open source, drives FTDI/Olimex probes.- The target's BSDL in
data/bsdl_files/(KU15P:xcku15p_ffve1517.bsdis bundled). - An entry for the target in
data/targets.yaml(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, …), the Digilent backend is built in by default on Linux — just
install the Adept Runtime system-wide so libdjtg.so/libdmgr.so are
present at runtime. 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 && cd ..
./build/bs # run from the repo root: data/probes.yaml, data/targets.yaml,
# data/bsdl_files/ and data/bscan_proxies/ are looked up in the CWD
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).
Probe profiles
Some probes need pin tweaks the built-in defaults don't cover — e.g. the
embedded FlashPro on Microsemi eval kits (an FT4232H whose JTAG sits
on channel A = index 0 and needs ADBUS4 left high-Z, or the chain stays
silent). data/probes.yaml captures these as named profiles; apply one when
opening:
bs_explorer> jtag_profiles
2 probe profile(s) in data/probes.yaml:
flashpro
ft2232h
bs_explorer> jtag_open 0 flashpro
Applied probe profile 'flashpro'.
Probe Ok !
jtag_close releases the current probe (frees its USB handle) — use it
to hand the probe to another tool, or to program two boards on different
probes in turn: jtag_close, then jtag_open the next one and
jtag_autoinit to rescan.
2. Scan the JTAG chain
The fastest path is jtag_autoinit: it scans the chain and
auto-loads every BSDL in data/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
target_info walks the chain and matches each IDCODE against the
registry in data/targets.yaml:
bs_explorer> target_info
Device 0 IDCODE 0x04A56093 -> Xilinx Kintex UltraScale+ XCKU15P [Xilinx UltraScale+]
prog: proxy_spi
caveat: CCLK routed via STARTUP primitive (not drivable in EXTEST)
prog: is the programming backend the registry assigns the part —
proxy_spi (Xilinx external flash, §Phase 2.5)
or svf (§Programming via SVF).
If you get not in registry, add an entry — see
Adding a new FPGA.
target_list prints the whole registry without needing a probe.
JTAG clock (optional)
Each driver has its own default TCK (FTDI 1 MHz, Digilent 4 MHz). To set
a single driver-neutral clock, before jtag_open:
bs_explorer> set JTAG_TCK_FREQ_KHZ 1000
It's applied at open (mapped to the FTDI driver's variable, or read
directly by the Digilent one). A registry entry may declare a
max_tck_khz; if your requested clock exceeds it, jtag_autoinit clamps
it and re-opens the probe at the safe rate:
WARNING : JTAG clock 2000 kHz exceeds the device max 1000 kHz; clamping.
Re-opening at 1000 kHz and re-scanning.
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 — data/targets.yaml at the repo root — holds the
per-part facts that can't be derived from the BSDL alone (or are tedious
to). It's parsed at runtime, so adding a part is editing YAML, no
rebuild. 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 data/bsdl_files/. Source: Xilinx/AMD device page
under "Design Files / BSDL", Intel in the Quartus install, Lattice per
part, Microsemi/Microchip via Libero. 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" \
data/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. Add a YAML entry
Append a list item under fpgas: in data/targets.yaml:
| Key | What it is | XCKU040 |
|---|---|---|
name |
human-readable label (quoted) | "Xilinx Kintex UltraScale XCKU040" |
idcode |
IDCODE pattern (version nibble as 0) | 0x03822093 |
idcode_mask |
bits that must match; 0x0FFFFFFF ignores the Xilinx revision nibble (default 0xFFFFFFFF) |
0x0FFFFFFF |
family |
xilinx_7/us/usp, microsemi_igloo2/smartfusion2, lattice_machxo2/3 |
xilinx_us |
bsdl |
basename in data/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 (omit = 0/N/A) | from the BSDL |
proxy_bitstream |
BSCAN proxy .bit in data/bscan_proxies/ (omit if none) |
bscan_spi_xcku040.bit |
caveats |
space/comma-separated flag names (omit if none) | cclk_via_startup |
max_tck_khz |
max safe JTAG TCK in kHz; jtag_autoinit clamps + re-opens if exceeded (omit = unspecified) |
— |
prog |
programming backend proxy_spi/svf/none (omit → inferred: a proxy ⇒ proxy_spi, Microsemi/Lattice ⇒ svf) |
proxy_spi |
The resulting entry (verbatim from data/targets.yaml):
- name: "Xilinx Kintex UltraScale XCKU040"
idcode: 0x03822093
idcode_mask: 0x0FFFFFFF
family: xilinx_us
bsdl: 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: cclk_via_startup
prog: proxy_spi
Omit any field that doesn't apply: a missing proxy_bitstream means
"none yet", a missing caveats means none, a missing idcode_mask
defaults to exact match (0xFFFFFFFF).
What caveats means
caveats is a space/comma-separated list of flag names, each backed by
an FPGA_CAVEAT_* bit 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. Omit the
field when the part has none. target_info prints any flag that is set,
as a human-readable line.
Currently one flag exists:
cclk_via_startup(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 for any 7-Series/US/US+ part.
To introduce a new caveat: add a #define FPGA_CAVEAT_xxx (1u << n)
in fpga.h, teach parse_caveats() in fpga.c its YAML name, use that
name in the YAML, and (if it should be visible) print it in
cmd_target_info in script.c.
d. Verify — no rebuild
The registry is loaded at runtime, so just (re)start bs_explorer from the repo root and check:
./build/bs
bs_explorer> target_list # your part should appear, with its source file
bs_explorer> jtag_autoinit
bs_explorer> target_info # should show your part, family, and any caveats
(target_list reads the registry without needing a probe.)
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 data/bscan_proxies/:
curl -L -o data/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
anything in the UltraScale+ family (XCKU15P, XCKU3P, XCKU11P, XCZU…,
Virtex US+, …) has to be built from source. You need (o)Migen + Vivado
(2022.2 for UltraScale/+; ISE 14.7 only for Spartan-6 and earlier).
Step-by-step walkthrough in doc/build_xilinx_proxy.md, worked out on the KU15P.
Once built, drop bscan_spi_<part>.bit into data/bscan_proxies/ (it's
MIT, like the KU040 — keep data/bscan_proxies/LICENSE.quartiq) and set the
proxy_bitstream field on the matching entry in data/targets.yaml.
Load the bridge and talk SPI
bs_explorer> jtag_open 1
bs_explorer> jtag_autoinit
bs_explorer> bscan_load_bitstream 0 data/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
src/modules/bscan/bscan.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.
Programming via SVF (Lattice, Microsemi, …)
The BSCAN-proxy path above is Xilinx-specific (external SPI config flash).
For everything else, the universal path is to play an SVF file
exported by the vendor tool — Libero / FlashPro Express (Microsemi),
Diamond / Radiant (Lattice), Vivado (Xilinx fabric), Quartus, … The
vendor bakes the programming algorithm into the SVF; bs_explorer just
replays its SIR/SDR/RUNTEST vectors and checks the masked TDO
compares that flag a failed erase/program.
bs_explorer> jtag_open 0 flashpro # the probe profile for your kit
bs_explorer> jtag_autoinit # target_info should show 'prog: svf'
bs_explorer> svf_play design.svf
...
SVF done: 1342 commands, 1338 scans, 71 compares
A TDO mismatch stops play and points at the failing vector:
ERROR : line 842: SDR TDO mismatch at bit 17 (len 696)
— usually a wrong device, a too-fast clock, or a part that isn't erased/unlocked.
You can sanity-check the player without a programming file using a tiny
hand-written IDCODE check (after STATE RESET the TAP auto-loads IDCODE
into DR):
! idcode.svf — masked IDCODE check (top nibble = revision)
STATE RESET;
SDR 32 TDO (0F8031CF) MASK (0FFFFFFF);
bs_explorer> svf_play idcode.svf
SVF done: 2 commands, 1 scans, 1 compares
Supported subset (single-device chain): SIR/SDR with
TDI/TDO/MASK/SMASK and the masked compare; RUNTEST (TCK/SCK
counts and SEC delays); STATE (RESET/IDLE); ENDIR/ENDDR (IDLE only);
HIR/HDR/TIR/TDR (length 0 only); TRST; FREQUENCY. SMASK is
parsed but not applied. Multi-device headers/trailers (non-zero) and
non-IDLE end states are rejected with a clear error.
Generating the SVF is the closed step. There is no open-source
bitstream/SVF generator for Microsemi (or most vendors) — you need the
vendor tool to produce the SVF (Libero has a free tier covering the
small IGLOO2 parts). bs_explorer only plays it, which is fully open.
One command: program
Rather than remember which backend a part uses, program <dev> <file>
dispatches on the device's registry prog method:
bs_explorer> jtag_autoinit
bs_explorer> program 0 design.svf # prog=svf -> plays the SVF
svf plays the file; proxy_spi points you at the flash workflow
(bscan_load_bitstream + flash_write/flash_verify); arm_flash
routes to the ARM backend.
CPU targets (ARM7/9) — structure only
The registry also describes CPUs (kind: cpu): an ARM debug
transport (debug: embeddedice), work-RAM and an on-chip flash region.
target_list shows them and program routes prog: arm_flash to the
ARM backend — but that backend (halt the core over JTAG, load a RAM
flasher, program internal flash) is not implemented yet. An Olimex
ARM-USB-OCD is an FT2232, so it opens with the existing FTDI driver via
the arm-usb-ocd probe profile. See the ARM-debug design note in
CLAUDE.md.
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. |
target_info says "not in registry" |
Add an entry to data/targets.yaml. |
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). |
Detected fine, then reads turn to garbage / 0x00000000 mid-session |
Target board lost power — JTAG floats (the USB probe stays enumerated regardless). Re-power the board. |
FT4232H FlashPro: jtag_scan finds 0 devices |
JTAG is on channel A (index 0) and needs ADBUS4 high-Z — open with the profile: jtag_open 0 flashpro. |
svf_play mismatches only on the very first compare |
FTDI link warm-up; svf_play handles it, but a bare bscan_shift_dr straight after jtag_open may need a jtag_scan first. |
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
src/modules/jtag_core/,src/modules/bsdl_parser/,src/modules/bus_over_jtag/.