Blog
[nextpnr-xilinx post-route estimates on xc7a200tfbg484-2, not sign-off timing; a stand-alone unit on synthetic pins, not a full design; JTAG result from our own bench] Two bench questions became two commands. tri fpga-seeds treats Fmax as a range over placement seeds: a 321-LUT ternary dot-product unit ran 164.15 to 200.40 MHz over 20 seeds, and 62.68 to 76.75 MHz with its pins spread across the package, with the same netlist on every run. tri fpga-jtag reads which chip is on the chain, never programs, and tells a silent TDO from a broken cable. On our bench it caught an XC7A200T where an XC7A100T was expected.
Two questions come up on every open-toolchain FPGA bench before any result is worth reporting. Which chip is actually on the other end of the JTAG cable? And is the Fmax in the log a property of the design, or of the one placement seed that produced it? We turned both into commands, tri fpga-jtag and tri fpga-seeds, and ran them on our own bench and on a 321-LUT ternary dot-product unit we are cross-checking for another group.
The short answer to the second question: across 20 placement seeds, that unit ranged from 164.15 to 200.40 MHz. Moving its pins ranged it from 62.68 to 76.75 MHz. With the netlist and cell counts identical in both cases, the seed changed Fmax by 1.22x and the pinout by about 2.6x. Every Fmax here is nextpnr's post-route estimate from its own delay model, not sign-off timing.
nextpnr-xilinx prints a post-route Max frequency per clock, and the number moves with --seed. A single seed is one sample from a distribution. fpga-seeds either parses a set of existing logs (--parse) or runs the sweep itself (--run, one nextpnr process per seed, at low priority). It writes the input hashes and the nextpnr version before the first seed and refuses to overwrite earlier logs. Per seed it takes LUT, FF and CARRY4 counts, the last post-route Fmax of the slowest clock, and the logic/routing split of the critical path. It prints min, median, max and the max/min ratio, and it flags any seed whose cell counts differ, because then the seeds did not place the same netlist.
| xc7a200tfbg484-2, nextpnr-xilinx, 100 MHz target, post-route estimate | Seeds | LUT / FF / CARRY4 | Fmax min / median / max (MHz) | Meets 100 MHz |
|---|---|---|---|---|
| Compact pinout | 20 | 321 / 114 / 14 on every seed | 164.15 / 187.16 / 200.40 | 20 of 20 |
| Pins spread across the package | 10 | 321 / 114 / 14 on every seed | 62.68 / 71.42 / 76.75 | 0 of 10 |
| Corrected unit (one more negation bit), compact pinout | 20 | 331 / 114 / 15 on every seed | 178.00 / 194.34 / 221.58 | 20 of 20 |
The slowest compact-pinout seed is faster than the fastest spread-pinout seed by a factor of 2.14. In the spread runs the critical path has 0.4 ns of logic and 12.6 to 15.5 ns of routing: a two-LUT path that crosses the die three times between I/O columns. A stand-alone unit with synthetic pins measures where its pins were put. Our earlier single-seed estimate for this unit, 71.28 MHz, reproduces exactly at seed 1 with the spread pinout; it was a pinout result, not a datapath result. We have said so to the group whose unit it is, and they agree that it does not belong in a speed comparison.
The third row is the corrected version of the same unit. Its negation is one bit wider, so -1 x (-128) gives +128 instead of wrapping to -128. That costs ten LUTs and one CARRY4 after routing, and on the same compact pinout the estimate for all 20 seeds still clears 100 MHz.
fpga-jtag reads IDCODEs and never programs. It handles two cable families. For a Xilinx Platform Cable USB II (or a DLC10 clone; USB VID 0x03fd), the cable comes up as 0x0013 with no firmware. With --load the command loads the firmware with fxload, without sudo, and waits for it to re-enumerate as 0x0008. It then runs xc3sprog and prints the firmware and CPLD versions and every device on the chain, by name and revision. For FTDI cables it asks openFPGALoader which probe is attached and runs --detect. --expect XC7A100T turns the result into an exit code: 0 for a match, 1 for no chain or a different chip, 2 for no cable.
On our bench that exit code did its job. The only cable attached today was the FTDI probe of the AX7203, and fpga-jtag --expect XC7A100T reported loc 0: 0x03636093 XC7A200T and exited 1. That is the right chip for that board and the wrong chip for the step we were about to take. It is a step we would rather have stopped at a mismatch than at a programming error.
When there is no chain at all, the command shifts raw bits through TDO and reads what comes back. All ones means nothing is driving TDO. The cable itself works, so the next things to check are the cable status LED, target power, the header, pin-1 orientation and the TDO wire, in that order. All zeros points at the cable side. That distinction is what separates a cable problem from a board problem, and all ones is what our second bench, an XC7A100T on a DLC10 clone, returned at its last check.
One detail for anyone writing a similar tool: openFPGALoader masks the revision nibble of the IDCODE, so it reports 0x03636093 where xc3sprog reports 0x13636093 for the same part. fpga-jtag names the part on the FTDI path and does not invent a revision.
Each command carries a --self-test that runs its parsers on recorded output: 17 checks for jtag (IDCODE names, firmware lines, the stuck-high and stuck-low TDO verdicts, the openFPGALoader revision mask) and 11 for seeds (unfinished logs, multiple clocks, differing netlists). tri fpga-tools runs every board-loop self-test together; it reports 14 of 14. Re-parsing the logs behind the table above reproduces the hand-made CSVs value for value.
Work with me
I audit RTL and build independent, bit-exact models, then take the result through synthesis and, when useful, onto an Artix-7 board. The first conformance module is free.