Program an FPGA, wait for it to boot, find its UART again, send a command, check the answer, and keep the whole thing as a reproducible run.
FPGAFlow does not implement JTAG. It drives openFPGALoader for programming and pySerial for the UART, and joins those two halves into one workflow.
- Python 3.9 or newer
- openFPGALoader on
PATH, or passed explicitly with--loader /path/to/openFPGALoader - A board openFPGALoader supports
git clone git@github.com:delaidam/FPGAFlow.git
cd FPGAFlow
pip install -e .That installs two commands, fpgaflow and fpgaflow-gui. Check the bench:
fpgaflow doctorEvery command defaults to the tangnano20k profile. Pass --board for
anything else.
fpgaflow boards # boards openFPGALoader knows; * marks a local profile
fpgaflow cables # cables openFPGALoader knows
fpgaflow detect # serial ports, ranked UART candidates, USB scan
fpgaflow detect --json # the same, machine-readablefpgaflow flash top.fs # into SRAM, gone on power loss
fpgaflow flash top.fs --target flash --verify # into flash, persistentfpgaflow send '$1'Program, wait for the board to boot, find the UART again, send commands, and write a run directory:
fpgaflow run top.fs '$1'
fpgaflow run top.fs '$1' '$2' --logs runsBoards differ in what a fresh bitstream does to the UART. An FT2232 carries JTAG and UART on one USB device that never re-enumerates, so the port stays put and only the design needs time to start. A design that implements its own USB serial instead drops off the bus and comes back as a new device.
FPGAFlow spends the boot delay watching the bus rather than sleeping through it. If the UART disappears, its return is the signal to reconnect and the run continues as soon as it is back, instead of waiting out a delay that was only ever a guess. If it never disappears, the delay is honoured as before. The watching comes out of the delay, so nothing is waited for twice.
--boot-wait sets the upper bound; a profile's reset_delay_s is the default.
fpgaflow run top.fs '$1' --boot-wait 5--expect takes a regular expression the response must contain. Repeat it to
check further commands: the Nth pattern is matched against the response to the
Nth command, and commands without a pattern are left unchecked.
fpgaflow run top.fs '$1' --expect 'v1\.2'
fpgaflow run top.fs '$1' '$2' --expect 'v1\.2' --expect '^ok'Patterns are compiled before the programmer runs, so a typo in a regex cannot leave you with a reprogrammed board and no verdict.
run and send report through their exit status:
| Code | Meaning |
|---|---|
0 |
Everything ran, and every expectation matched |
1 |
The board answered, but a response did not match its --expect pattern |
2 |
The run could not be completed: bad arguments, programming failed, no UART found |
1 and 2 are kept apart on purpose. 1 is a regression in the design, 2 is
a problem with the bench, and a script can tell them apart without parsing
output:
fpgaflow run top.fs '$1' --expect 'v1\.2'
case $? in
0) echo "pass" ;;
1) echo "wrong answer from the design" ;;
2) echo "bench is broken" ;;
esacdoctor follows the same convention: 0 when the bench is ready, 2 when a
check fails.
Each fpgaflow run writes runs/<timestamp>/:
| File | Contents |
|---|---|
metadata.json |
timestamp, OS, board, target, bitstream path, SHA-256 and MD5, baudrate, commands, expectations, and the verdict for each response |
programmer.log |
everything openFPGALoader printed |
serial.log |
each command with its response as text and hex, and whether it matched |
result.json |
success flag, responses, unmet expectations |
The bitstream is hashed before it is programmed, so a run directory records exactly which build produced which response.
Serial device names are not stable. The same interface is /dev/ttyUSB1 on one
machine and COM5 on another, and an FT2232 presents JTAG and UART as two
interfaces of a single chip. FPGAFlow ranks the available ports on USB metadata
— VID, PID, serial number, location, interface — and selects one only when a
single candidate clearly wins. Otherwise it lists the candidates and stops
rather than guessing wrong:
fpgaflow detectWhen the ranking needs help:
fpgaflow run top.fs '$1' --port /dev/ttyUSB1
fpgaflow run top.fs '$1' --vid 0x0403 --pid 0x6010 --interface 1
fpgaflow run top.fs '$1' --serial BOARD-BIdentical boards share a VID and PID, so USB metadata alone cannot separate
them and FPGAFlow stops rather than guessing. --serial names one by its USB
serial number, which fpgaflow detect prints for every port.
A serial number identifies one specific board, so it filters rather than ranks:
if the named board is not attached, the run fails instead of quietly falling
back to another board of the same model. Naming a device with --port
overrides it.
openFPGALoader is the source of truth for board and cable names, so FPGAFlow keeps no copy of that list. A local profile only adds what openFPGALoader does not know: UART baudrate, USB matching rules, interface hints, and how long the board takes to boot.
From src/fpgaflow/profiles/boards.json:
{
"boards": {
"tangnano20k": {
"description": "Sipeed Tang Nano 20K / Gowin GW2AR",
"programmer": { "backend": "openfpgaloader", "board": "tangnano20k" },
"serial": {
"vid": "0x0403",
"pid": "0x6010",
"interface": 1,
"baudrate": 115200,
"line_ending": "lf",
"reset_delay_s": 2.0
}
}
}
}A board with no local profile still programs normally; it simply has no UART
hints. Point at your own file with --profile-file boards.json.
Only tangnano20k has been checked against real hardware. The others follow
from the cable each board uses: every Tang board here drives the same FT2232
as the Tang Nano 20K, and the ULX3S carries a single-channel FT231X. If a
profile is wrong for your board, fpgaflow detect shows what is actually
attached and --vid, --pid and --interface override the profile:
fpgaflow detect
fpgaflow run top.fs '$1' --board tangnano9k --interface 0fpgaflow-guiSelect a bitstream, pick SRAM or flash, fill in an expect pattern if you want
one, and run it. The GUI shells out to the same CLI, so there is no second
implementation of the programming logic: the EXPECT ... -> match lines and the
exit code in its output come from fpgaflow run itself.
pip install -e ".[dev]"
pytest -qThe suite needs no hardware. Both the programmer backend and pySerial are faked, so the full flash-and-talk workflow is exercised on any machine.
This project started from my own use of openFPGALoader on Linux and the simple fact that I did not want to be bothered with typing the same FPGA programming and UART commands over and over again.
The workflow was always basically the same. I had to choose the correct bitstream, program the FPGA through JTAG, wait for the board to reset, find the correct serial interface, open the UART connection, send a command, and then inspect the response. None of these steps are particularly difficult on their own, but repeating them manually every time quickly becomes annoying and error-prone.
The problem is also that JTAG programming and UART communication are two separate things. openFPGALoader handles the FPGA programming side, while a serial tool is needed to communicate with the design after it has been loaded. FPGAFlow exists to connect those two parts into one workflow. It is not trying to replace openFPGALoader or implement its own JTAG programmer.
The raw UART output is intentional. FPGAFlow does not immediately assume that the response follows the documentation perfectly or that the protocol is implemented exactly as expected. It shows both the decoded text and the raw hexadecimal bytes, which makes it useful for debugging embedded protocols and investigating mismatches between a specification and the actual hardware.
The scope is deliberately limited. FPGAFlow is not a synthesis tool, a place-and-route tool, a Verilog editor, a waveform viewer, or a replacement for Gowin, Vivado, Quartus, or openFPGALoader itself. It is a workflow tool that connects programming, hardware discovery, serial communication, and repeatable testing into one place.
The reason for building it is straightforward: FPGA development should not require manually repeating the same sequence of low-level commands every time a bitstream changes. If a board can be detected, programmed, reconnected, queried, and logged through one consistent workflow, that is already a useful improvement.
MIT. See LICENSE.