USB-Blaster and SVF programming¶
How the application writes an FPGA's configuration flash without Quartus: what the build emits, what the engine does with it, and where the knowledge lives.
Why this exists¶
The FX3 can update the FPGA's application image over USB, but only once there is working gateware in the flash to do it through — the flash bridge lives in the gateware itself. A board that has never been programmed, or one holding gateware from before the bridge existed, cannot be reached that way at all. The FX3 has no path to the FPGA's configuration pins, so no amount of firmware work changes it.
What can reach it is the DE0-Nano's on-board USB-Blaster, on the same mini-USB connector that powers the board. Until now that meant a Quartus install — several gigabytes, unfree, and x86_64-linux only — whose entire role for someone building a board was to write a file the project already publishes.
So the project publishes the JTAG vectors as well, and the application plays them:
In ddd-gui the same engine sits behind Tools ▸ Firmware ▸ Bring up a new or legacy board…, which programs a board from nothing to fully up to date in one flow — see Bringing up a new or legacy board for the user's view of it, and bringup_orchestrator.h for the ordering rule it enforces. ddd-jtag is the same code with nothing above it, which is what makes it the bench harness.
The division of labour¶
The important decision on this page is what is not implemented here.
| Where it lives | |
|---|---|
| How a Cyclone IV is configured; how Altera's serial flash loader reaches an EPCS64; which register means "erased"; how long an erase takes | Quartus, at build time |
| What a JTAG session looks like: instruction registers, data registers, waits, expected answers | The .svf the build emits |
| How to walk a TAP state machine and turn scans into TCK cycles | ddd-gui/src/capture/svf_player.{h,cpp} |
| How to make a USB-Blaster produce those cycles | ddd-gui/src/capture/usb_blaster_cable.{h,cpp} |
| In which order the FX3 and the FPGA may be programmed | ddd-gui/src/capture/bringup_orchestrator.{h,cpp} |
Nothing device-specific is in the application. A different FPGA, a different flash or a newer Quartus changes the .svf and nothing else.
What the build emits¶
nix build .#bitstream and fpga/build-local.sh both run one extra conversion after the .jic:
quartus_cpf -c -q 4.5MHz -g 3.3 -n p \
DomesdayDuplicatorProvisioning_write_jic.cdf \
DomesdayDuplicatorProvisioning.svf
It reads the same chain description quartus_pgm reads, so the two routes write the same content to the same addresses. The .svf joins the .jic in provisioning/ and in the provenance record.
Sizes, measured on this project's own provisioning content (Quartus 25.1, 8 MB EPCS64):
| Size | |
|---|---|
DomesdayDuplicatorProvisioning.jic |
8.4 MB |
DomesdayDuplicatorProvisioning.svf |
18.4 MB |
the same .svf, gzipped |
251 KB |
the same content as .jbc (Jam STAPL byte code) |
538 KB |
The .svf is verbose hexadecimal text and compresses about seventy-five to one, which is worth knowing before it is put anywhere size matters.
The frequency is not decoration¶
-q 4.5MHz does more than annotate the file. The converter turns every wait into a count of TCK cycles at that rate: the same provisioning content emitted at 6 MHz has a third more cycles in every wait and describes exactly the same hundred-second erase.
-q 4.5MHz RUNTEST 9900 TCK; ... RUNTEST 450000000 TCK;
-q 6MHz RUNTEST 13200 TCK; ... RUNTEST 600000000 TCK;
So a cycle count in one of these files is a duration in disguise, and a cable clocking faster than the file declares would cut every one of them short — including the erase a flash needs in milliseconds and knows nothing about JTAG. The USB-Blaster's clock is generated by the cable's own logic and is not the host's to set.
The player therefore reads the FREQUENCY statement and holds each wait open for the time it stands for, sleeping out any remainder after the cycles have gone. When the cable is slower than the file declares — the ordinary case over full-speed USB — this costs nothing, because the time has passed already.
Reproducibility¶
Two conversions of one .jic are byte for byte identical; touching the .jic and converting again changes four bytes, all of them in the header comment naming the input file and its modification time. bitstream-provenance.py masks that line for the canonical digest, exactly as it masks the four fields Quartus stamps into a .sof, so a rebuild of a release commit can be compared against the record.
The player¶
SvfPlayer is a parser and a TAP state machine, and nothing else. It reads a statement at a time and turns it into cycles:
SIR/SDR— walk to the shift state, clock the bits with TMS low, raise TMS on the last one to leave as it goes, then walk to the stateENDIR/ENDDRnamed. What comes back on TDO is compared underMASK, and a mismatch stops the run naming the line and both values.RUNTEST— clock in Run-Test/Idle, then hold the wait open as described above.STATE,ENDIR,ENDDR,FREQUENCY,TRST— bookkeeping and navigation.TDI,MASKandSMASKcarry over between scans of the same kind;TDOdoes not, so a scan that names none compares nothing.
Two kinds of file are refused rather than half-understood:
- one with a non-empty
HIR/HDR/TIR/TDR, which describes a JTAG chain with more than one device on it. This project's board has one, and guessing would mean shifting a flash image into whatever was actually there. - one that drives TRST, a line the cable does not have.
Paths through the sixteen TAP states are found by breadth-first search rather than a hand-written table, and Test-Logic-Reset is excluded as a stepping stone: it is one clock away from several states and would turn up in plenty of shortest paths, and passing through it in the middle of a flash write resets the instruction register.
The cable¶
The USB-Blaster is an FTDI FT245 in front of a small CPLD. The host writes a byte stream and the CPLD reads it in one of two modes, chosen by the top bit of each command byte.
| Command byte | Cost per TCK cycle | |
|---|---|---|
| Bit-bang | 0 READ LED TDI nCS nCE TMS TCK |
two bytes |
| Byte-shift | 1 READ and a six-bit count of data bytes to follow |
one eighth of a byte |
Byte-shift holds TMS low and clocks eight cycles per data byte, so it carries everything except the last bit of a scan — the one that raises TMS — and, importantly, the waits: a provisioning run spends far more cycles idling than shifting, and clocking those a bit at a time would cost eight times the traffic.
Every read is a bit-bang read, and that is a bench finding rather than a design preference (B-V1, 2026-08-17). Byte-shift mode defines the same READ bit; asked to use it, this cable returns FF for every byte — no information at all rather than the wrong information — while the same bits read correctly one cycle at a time. Byte-shift shifting is not in doubt: reading a Cyclone IV's IDCODE with the first 24 bits byte-shifted and the last 8 bit-banged returns the correct top byte, so the shift had advanced exactly 24 places. Only the answer is missing, and why is not understood.
Avoiding it costs nothing measurable. Of the 73,297,811 bits this project's provisioning file shifts, 103 are read — one ten-thousandth of one per cent — so the fast path still carries everything that takes time, and a read costs sixteen command bytes a byte instead of two. The driver therefore never byte-shifts a scan whose TDO is captured, and the byte-shift read path is deleted rather than left for somebody to reach for.
Reads carry the chip's own framing: an FT245 puts two modem-status bytes at the front of every USB packet it sends, and they are not data.
Before pointing this at a board, read its IDCODE. Seven statements, 42 bits, nothing written — and the smallest possible proof that a JTAG cable works at all. TESTING.md's B-V1 carries the file; the first cable session skipped that step and spent a whole bring-up run discovering what it says in a second.
Altera never published this protocol, but it has been described and independently implemented several times over two decades. What is implemented here is written from those public descriptions of the protocol; no code was taken from any of them, deliberately — openFPGALoader is AGPL-3.0 and taking a line of it would change this project's licence position.
Platforms¶
The cable is available on all three platforms. Only the byte pipe under it differs: usb_blaster_libusb.cpp on Linux and macOS, usb_blaster_winusb.cpp on Windows, chosen at configure time by src/capture/CMakeLists.txt exactly as the capture backend is. Everything above the pipe — the protocol, the player, every test either has — is one implementation everywhere.
On Linux the cable needs fx3/programmer/configs/70-domesday-duplicator.rules installed (Linux device access) — the one file covers the cable as well as the Duplicator itself — and Quartus's own jtagd will hold the cable open whenever it is running.
On Windows the cable needs WinUSB bound to 09FB:6001, once per machine, with Zadig — see Windows — MSI. Windows binds drivers by USB identifier, and a Quartus install puts Altera's own USB-Blaster driver on that identifier; only one driver can hold the cable, so binding WinUSB takes it away from Quartus until it is put back in Device Manager. A cable that is attached and bound to something else is reported as exactly that rather than as absent: the driver can see it on the bus through CM_Get_Device_Interface_List whatever it is bound to, and only opening it needs WinUSB.
macOS binds nothing and needs no setup.
The shell tool¶
Without --dry-run this writes the FPGA's configuration flash through the cable, which is a deliberate manual act and is never run automatically (AGENTS.md §4). It reports how long the run took, which is a number this project needs and cannot get any other way.
It is also the one path with no ordering above it. The bring-up flow programs the FX3 before the FPGA, because the original firmware and the current gateware drive the same interconnect line, and BringUpOrchestrator refuses to do them the other way round. This tool has no such refusal: it plays what it is given at whatever is attached. On a board still running the original Duplicator firmware, that means writing current gateware underneath it — so power-cycle such a board only after its FX3 has been dealt with, or use the wizard, which cannot get it wrong.
--dry-run plays the file into a cable that goes nowhere. Everything this application is responsible for runs — the parse, the state machine, the vectors, the counts — and the two things a device would be needed for are skipped: nothing is compared against the answers the file expects, and its waits are not held open. That makes it a complete check of a programming file on a machine with no hardware attached:
$ ddd-jtag --dry-run DomesdayDuplicatorProvisioning.svf
Played 36996 statements: 73146035 bits shifted, 471147960 idle clocks,
file asks for at most 4.50 MHz
Those two counts are what a run costs: about 68 MB of USB traffic in byte-shift mode, and the idle clocks alone stand for 105 seconds of waiting that the flash actually needs.
Testing¶
Everything above is covered without hardware, and deliberately: the fake byte pipe sees exactly what the cable would put on the wire, so the mode choices, the bit packing and the status-byte framing are checked against fixtures; the fake cable records the vector stream, so the state machine's route to every shift is checked cycle by cycle. Fixtures include a real Quartus-emitted file, because a parser that only ever meets its author's idea of the format meets the real thing for the first time on a bench.
What no test can settle is whether the far end agrees — whether TDO is sampled on the right half of the cycle, and how fast the cable really clocks. That is B-V1 in TESTING.md §6.