Files
bs_explorer/doc/tutorial.md
François 1814c4cf0c doc: explain what a JEDEC ID is
The bscan_jedec command and tutorial referenced the JEDEC ID without
defining it. Describe the 0x9F RDID command and the manufacturer +
device byte layout, in the tutorial and the command help.
2026-05-23 17:18:09 +02:00

11 KiB
Raw Blame History

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.
  • libftd2xx reachable at runtime (already vendored under libs/libftd2xx/).
  • The target's BSDL in bsdl_files/ (KU15P: xcku15p_ffve1517.bsd is 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+]
           quirk: 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 quirk 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

For an FPGA that's not in the registry yet:

  1. Drop the BSDL in bsdl_files/. The file you want is on the vendor's site (Xilinx: in the device download under "BSDL files"; Intel: in Quartus install dir; Lattice: per part).

  2. Read the facts you need from the BSDL:

    attribute IDCODE_REGISTER ...      -> IDCODE pattern (4-bit version
                                           masked, lower 28 bits matter)
    attribute INSTRUCTION_LENGTH ...   -> ir_length
    attribute INSTRUCTION_OPCODE ...   -> opcodes for IDCODE, EXTEST,
                                           SAMPLE, BYPASS, and private
                                           instructions for the family
                                           (USER1, CFG_IN, JPROGRAM,
                                           JSTART, JSHUTDOWN, ISC_DISABLE
                                           on Xilinx)
    
  3. Add an entry to fpga_registry[] in modules/fpga/fpga.c, mirroring the existing KU15P entry. Set proxy_bitstream to NULL for now; wire it up when you have one. Set quirks as appropriate (e.g. FPGA_QUIRK_CCLK_VIA_STARTUP for any Xilinx 7-Series/UltraScale/UltraScale+).

  4. Rebuild. The registry is compile-time, no runtime registration.

  5. Verify with fpga_info after jtag_autoinit.

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 (~50200 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).

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 XX XX  (manufacturer 0x20, device 0xXXXX)

(The exact device bytes depend on the part fitted; on the KCU105 the manufacturer byte should read 0x20 = Micron.)

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 (0x20 Micron, 0xEF Winbond, 0xC2 Macronix, 0x01 Cypress/Infineon, 0x9D ISSI, …);
  • bytes 12 — 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

  • help in the REPL lists all commands; help <cmd> gives details.
  • CLAUDE.md at the repo root captures the architecture, roadmap and technical decisions (machine-independent).
  • README.md is the user-facing summary.
  • The original Viveris library and its docs live untouched under modules/jtag_core/, modules/bsdl_parser/, modules/bus_over_jtag/.