"quirk" was unclear jargon; "caveat" matches the wording already used in
the README/CLAUDE.md ("Xilinx caveats"). Renames the struct field, the
FPGA_QUIRK_* macro, the fpga_info output and the docs. No behaviour
change.
287 lines
11 KiB
Markdown
287 lines
11 KiB
Markdown
# 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](#adding-a-new-fpga-target) below.
|
||
- For SPI flashing, eventually: a BSCAN proxy bitstream — see the
|
||
[Phase 2.5 caveat](#phase-25-spi-through-the-bscan-proxy-bridge-bitstream) 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
|
||
|
||
```sh
|
||
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](#adding-a-new-fpga-target).
|
||
|
||
`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](#phase-25-spi-through-the-bscan-proxy-bridge-bitstream) 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
|
||
|
||
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 caveats as
|
||
appropriate (e.g. `FPGA_CAVEAT_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
|
||
(~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/`:
|
||
|
||
```sh
|
||
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 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 (`0x20` Micron,
|
||
`0xEF` Winbond, `0xC2` Macronix, `0x01` Cypress/Infineon, `0x9D`
|
||
ISSI, …);
|
||
- **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
|
||
|
||
- `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/`.
|