bscan_load_bitstream + bscan_jedec confirmed end to end on a KCU105: reads the Micron MT25QU256 config flash (0x20 BB 19) through the quartiq XCKU040 proxy. Replace the illustrative JEDEC output with the real one.
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+]
|
||
quirk: 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` 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
|
||
(~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/`.
|