Open-source, vendor-agnostic FPGA debug cores: an Embedded Logic Analyzer (ELA) for waveform capture, an Embedded I/O (EIO) for runtime read/write of fabric signals, a JTAG-to-AXI4 Bridge (EJTAG-AXI) for memory-mapped bus access, and a JTAG-to-UART Bridge (EJTAG-UART) for console-style debug — all over JTAG. Drop them into any FPGA design and export captures to JSON, CSV, or VCD.
Includes single-instantiation Verilog wrappers for Xilinx 7-series, Xilinx UltraScale / UltraScale+, Lattice ECP5, Intel / Altera, Gowin, and Microchip PolarFire / SmartFusion2 / IGLOO2. ELA and EIO also provide VHDL wrappers for Xilinx 7-series, Lattice ECP5, Intel / Altera, and Gowin. The core RTL and Python host stack are fully portable.
📖 User manual — full walkthrough of the RTL cores,
host stack, CLI, RPC server, and desktop GUI. JTAG register / shift maps
are not duplicated in this README; see the manual (e.g. chapter 13 and
docs/specs/register_map.md) and
docs/specs/transport_api.md for canonical
specs.
- Description
- Features
- Quick start
- Usage
- Integrating the core into your design
- Support status
- Resource usage
- CI
- Building from source
- Project structure
- Author
- License
Jump within this page: ↑ Top
fpgacapZero is designed as an independent, vendor-agnostic debug stack. The same ELA, EIO, EJTAG-AXI, and EJTAG-UART concepts are exposed through small RTL modules, a stable register map, and a scriptable host stack.
| Area | Project direction |
|---|---|
| Portability | Shared core RTL with thin vendor TAP wrappers |
| Configurability | Compile-time parameters remove unused trigger, storage, timestamp, segment, readout, and mux features |
| Triggering | Lightweight default compare modes, optional relational compares, optional dual comparator, and optional multi-stage sequencer |
| Readback | Default 256-bit burst readout where supported, with simple 32-bit register reads for fallback or chain-limited builds |
| Host access | Python API, CLI, JSON-RPC, GUI, and export helpers |
| Data formats | JSON, CSV, VCD, and structured capture summaries |
| Licensing | Apache 2.0, usable in proprietary designs with explicit patent grant |
The result is intentionally parameter-driven: small designs can compile out unused features, while larger debug builds can enable richer trigger and readback behavior without switching cores.
- Drop-in FPGA visibility over JTAG -- capture internal signals, drive debug I/O, access AXI registers, and open a UART console without adding board pins or a soft CPU.
- Small when you need it small -- a practical 8-bit, 1024-sample ELA can be built around 596 LUTs + 0.5 BRAM with simple readout, or about 912 LUTs + 0.5 BRAM with the default single-chain fast readout on xc7a100t.
- Scales up by parameter, not by rewrite -- widen probes, deepen buffers, add trigger stages, timestamps, decimation, segmented captures, storage qualification, and external trigger I/O only when the design needs them.
- Useful triggers without bloat -- value match, edge, changed, optional relational compares, optional dual comparator, AND/OR combine, and a 2-4 stage sequencer for protocol-like captures.
- Pre/post-trigger waveform capture -- circular buffer, configurable trigger delay, named sub-signals, and export to VCD, CSV, or structured JSON.
- Fast readout paths where available -- optional burst readback packs multiple samples per JTAG scan to reduce host round trips, while simpler single-register readout remains available for small or limited TAP designs.
- Engineer-friendly host tools -- Python API, command-line capture,
optional PySide6 desktop GUI (
fcapz-gui), JSON-RPC server, and capture summaries that are easy to feed into automation. - Portable integration -- shared RTL core with thin vendor TAP wrappers for Xilinx 7-series, Xilinx UltraScale / UltraScale+, Lattice ECP5, Intel / Altera, Gowin, and Microchip PolarFire-family devices; optional LiteX helper for packing named probes.
- Extra debug channels -- Embedded I/O (EIO), JTAG-to-AXI4 (EJTAG-AXI), and JTAG-to-UART (EJTAG-UART) can live beside the ELA when the target device has enough user JTAG chains.
- Python 3.10+
- Any FPGA board with JTAG access (tested on Arty A7-100T)
- One of:
- OpenOCD with FTDI support (any vendor), or
- Vivado/XSDB (2022.2+) for the Xilinx hw_server backend
pip install -e ".[dev]"
pytest tests/ -v
python sim/run_sim.pypython sim/run_sim.py runs the shared RTL lint pass (iverilog -Wall)
first, then the default simulation regression. Use
python sim/run_sim.py --lint-only when you only want the RTL lint check.
Use the installed fcapz entry point for day-to-day ELA work. The legacy
python -m fcapz.cli form still works, but the package install path is
what contributors should rely on. GitHub Actions runs a subset of these checks
(see CI); run pytest tests/ -v locally before pushing so CLI, RPC, and
event-helper tests run too.
Install the GUI extra (PySide6, pyqtgraph, GTKWave/Surfer/WaveTrace optional on PATH):
pip install -e ".[gui]"
fcapz-guiConnect over hw_server or OpenOCD, capture from the ELA tab, inspect
waveforms in the embedded preview, or open captures in GTKWave (.gtkw
sidecar) or Surfer (--command-file sidecar, *.surfer.txt). Settings and
probe profiles live in %APPDATA%\\fpgacapzero\\gui.toml (Windows) or
~/.config/fpgacapzero/gui.toml (Unix). Next to that file, fcapz-gui-window.ini
stores main-window geometry, dock/tab layout, expandable section state, and any
Window → Save layout as… presets. Docks are detachable and tabbable; the log
dock supports level filter, substring search, optional stderr mirroring, and
Clear / Copy all from the File menu.
Connect runs in the background; Cancel stops the attempt by closing the
transport (TCP/JTAG may take a moment to unwind if the server hung). The
Connection panel sets Connect timeout (OpenOCD TCP, seconds) and HW ready
timeout (after programming a .bit, seconds); both are stored in gui.toml
under [connection] as connect_timeout_sec and hw_ready_timeout_sec.
Surfer: the GUI opens the native Surfer binary in its own window (same as
GTKWave). Upstream does not expose a supported way to dock the native Surfer UI inside
the Qt main window; true in-app embedding would mean a WebEngine surface (Surfer’s
WASM/web API) or server mode, not the current Popen path. When surfer is on
PATH, pytest tests/test_surfer_integration_smoke.py runs a small CLI smoke check.
CLI capture can spawn a viewer after export (VCD only):
fcapz capture --format vcd --out dump.vcd --open-in gtkwave ...
fcapz capture --format vcd --out dump.vcd --open-in surfer ...The --program flag programs the FPGA automatically before running the
command. Build the Arty A7 reference design bitstream first (see
Building from source), or use your own bitstream.
# hw_server backend (Vivado) — programs FPGA and probes in one command
fcapz --backend hw_server --port 3121 \
--program examples/arty_a7/arty_a7_top.bit probe
# OpenOCD backend (program separately, then probe)
openocd -f examples/arty_a7/arty_a7.cfg &
fcapz --backend openocd --port 6666 probeSample output:
{
"version_major": 0,
"version_minor": 3,
"core_id": 19521,
"sample_width": 8,
"depth": 1024,
"num_channels": 1
}core_id is the ASCII string "LA" packed as 0x4C41 (= 19521).
Analyzer.probe() raises RuntimeError if this magic does not match,
so a wrong-chain / wrong-bitstream / unprogrammed FPGA is rejected
before any other ELA register is touched.
fcapz --backend hw_server --port 3121 \
--program examples/arty_a7/arty_a7_top.bit \
capture \
--pretrigger 8 --posttrigger 16 \
--trigger-mode value_match --trigger-value 0 --trigger-mask 0xFF \
--out capture.jsonThis programs the FPGA, arms the trigger, waits for a match, reads back
samples via burst mode, and writes capture.json.
fcapz --backend hw_server --port 3121 \
--program examples/arty_a7/arty_a7_top.bit \
capture --pretrigger 8 --posttrigger 16 \
--trigger-value 0 --trigger-mask 0xFF \
--probes counter_lo:4:0,counter_hi:4:4 \
--out capture.vcd --format vcd --summarizeThe --summarize flag prints a structured JSON summary (edge counts, value
ranges, burst lengths) that an LLM can consume directly. The --probes flag
splits the 8-bit sample into named sub-signals in VCD output.
For wider designs, keep probe names in a .prob sidecar instead of typing the
lane map on every capture:
fcapz --backend hw_server --port 3121 \
capture --probe-file design.prob --format vcd --out capture.vcdfcapz [global options] <command> [command options]
Global options:
| Flag | Default | Description |
|---|---|---|
--backend |
hw_server |
openocd or hw_server |
--host |
127.0.0.1 |
Server address |
--port |
backend default | 6666 for OpenOCD, 3121 for hw_server |
--tap |
xc7a100t.tap |
OpenOCD TAP name or hw_server FPGA target |
--program |
(none) | Program FPGA with bitfile before command (hw_server) |
Commands:
| Command | Description |
|---|---|
probe |
Read core identity (version, sample width, depth) |
arm |
Arm the trigger without configuring |
configure |
Write capture configuration to hardware |
capture |
Configure, arm, wait for trigger, read samples, export |
eio-probe |
Read EIO core identity and probe widths |
eio-read |
Read EIO input probes |
eio-write |
Write EIO output probes |
axi-read |
Single AXI read via JTAG-to-AXI bridge |
axi-write |
Single AXI write via JTAG-to-AXI bridge |
axi-dump |
Read a block of AXI words |
axi-fill |
Fill AXI memory with a pattern |
axi-load |
Load a binary file into AXI memory |
uart-send |
Send data to UART TX via JTAG-to-UART bridge |
uart-recv |
Receive data from UART RX via JTAG-to-UART bridge |
uart-monitor |
Continuous UART receive (Ctrl+C to stop) |
Capture / configure options:
| Flag | Default | Description |
|---|---|---|
--pretrigger |
8 |
Samples to keep before trigger |
--posttrigger |
16 |
Samples to capture after trigger |
--trigger-mode |
value_match |
value_match, edge_detect, or both |
--trigger-value |
0 |
Trigger compare value |
--trigger-mask |
0xFF |
Bit mask (hex) |
--trigger-delay |
0 |
Post-trigger delay in sample-clock cycles (0..65535) — shifts the committed trigger sample N cycles after the trigger event |
--sample-width |
8 |
Bits per sample |
--depth |
1024 |
Buffer depth |
--sample-clock-hz |
100000000 |
For VCD timescale |
--channel |
0 |
Probe mux channel index |
--probes |
(none) | Signal definitions: name:width:lsb,... |
--probe-file |
(none) | .prob sidecar with named signal definitions |
--timeout |
10.0 |
Seconds to wait for trigger (capture only) |
--out |
(required) | Output file path (capture only) |
--format |
json |
json, csv, or vcd (capture only) |
--decimation |
0 |
Decimation ratio (capture every N+1 cycles; 0=off, requires DECIM_EN) |
--ext-trigger-mode |
disabled |
External trigger mode: disabled, or, and (requires EXT_TRIG_EN) |
--summarize |
off | Print LLM-friendly capture summary (capture only) |
--open-in |
(none) | After VCD capture, open viewer: gtkwave (.gtkw), surfer (.surfer.txt), wavetrace, or custom |
--gui-config |
per-user path | gui.toml for viewer paths and custom argv |
from fcapz import Analyzer, CaptureConfig, TriggerConfig, ProbeSpec
from fcapz import XilinxHwServerTransport
from fcapz import summarize, find_edges, ProbeDefinition
# Default ir_table is the Xilinx 7-series preset (USER1=0x02, USER2=0x03,
# USER3=0x22, USER4=0x23). For an UltraScale / UltraScale+ board, pass
# the named UltraScale preset instead — the IR opcodes differ:
# transport = XilinxHwServerTransport(
# port=3121,
# fpga_name="xcku040",
# ir_table=XilinxHwServerTransport.IR_TABLE_US, # USER1=0x24, ...
# )
transport = XilinxHwServerTransport(port=3121)
analyzer = Analyzer(transport)
analyzer.connect()
print(analyzer.probe())
config = CaptureConfig(
pretrigger=8,
posttrigger=16,
trigger=TriggerConfig(mode="value_match", value=0, mask=0xFF),
probes=[ProbeSpec("counter_lo", 4, 0), ProbeSpec("counter_hi", 4, 4)],
channel=0,
)
analyzer.configure(config)
analyzer.arm()
result = analyzer.capture(timeout=5.0)
# Export
analyzer.write_json(result, "capture.json")
analyzer.write_vcd(result, "capture.vcd") # named signals in VCD
# LLM event extraction
edges = find_edges(result)
summary = summarize(result, [ProbeDefinition("lo", 4, 0)])
print(summary)
# Continuous capture (auto-rearm)
for result in analyzer.capture_continuous(count=3):
print(f"got {len(result.samples)} samples")
analyzer.close()
# Embedded I/O
from fcapz.eio import EioController
from fcapz import XilinxHwServerTransport # reuse same transport type
eio = EioController(XilinxHwServerTransport(port=3121))
eio.connect()
print(eio.read_inputs()) # read probe_in from fabric
eio.write_outputs(0x1) # drive probe_out into fabric
eio.set_bit(0, 1) # set single bit without disturbing others
eio.close()from fcapz import EjtagAxiController
bridge = EjtagAxiController(transport, chain=4)
bridge.connect()
bridge.axi_write(0x40000000, 0xDEADBEEF)
value = bridge.axi_read(0x40000000)
bridge.close()# Send a string
fcapz --backend hw_server --port 3121 uart-send --data "Hello\n"
# Send hex-encoded bytes
fcapz --backend hw_server --port 3121 uart-send --hex 48656C6C6F0A
# Send a file
fcapz --backend hw_server --port 3121 uart-send --file firmware.bin
# Receive up to 64 bytes with 2-second timeout
fcapz --backend hw_server --port 3121 uart-recv --count 64 --timeout 2.0
# Receive a single line (stops at newline)
fcapz --backend hw_server --port 3121 uart-recv --line
# Continuous monitor (Ctrl+C to stop)
fcapz --backend hw_server --port 3121 uart-monitorfrom fcapz import EjtagUartController
uart = EjtagUartController(transport, chain=4)
uart.connect()
uart.send(b"Hello\n")
data = uart.recv(count=0, timeout=1.0)
line = uart.recv_line(timeout=1.0)
print(uart.status())
uart.close()For LLM-driven or scripted ELA control:
python -m fcapz.rpcSend JSON commands on stdin, receive responses on stdout. Responses now carry
schema_version, and ELA capture supports json, csv, or vcd plus
optional channel, probes, and summarize fields.
{"cmd": "connect", "backend": "hw_server", "port": 3121, "program": "examples/arty_a7/arty_a7_top.bit"}
{"cmd": "probe"}
{"cmd": "configure", "pretrigger": 8, "posttrigger": 16, "channel": 1}
{"cmd": "arm"}
{"cmd": "capture", "timeout": 5.0, "format": "vcd", "summarize": true,
"probes": [{"name": "counter_lo", "width": 4, "lsb": 0}]}Pick the wrapper for your FPGA vendor — one instantiation each for ELA and EIO:
// Swap the wrapper suffix to port to another FPGA:
// fcapz_ela_xilinx7 / fcapz_eio_xilinx7 — Xilinx 7-series (Artix-7,
// Kintex-7, Virtex-7,
// Spartan-7, Zynq-7000)
// fcapz_ela_xilinxus / fcapz_eio_xilinxus — Xilinx UltraScale and
// UltraScale+ (Kintex/Virtex
// UltraScale, Artix/Kintex/
// Virtex/Zynq UltraScale+)
// fcapz_ela_ecp5 / fcapz_eio_ecp5 — Lattice ECP5
// fcapz_ela_intel / fcapz_eio_intel — Intel / Altera
// fcapz_ela_gowin / fcapz_eio_gowin — Gowin GW1N / GW2A
// fcapz_ela_polarfire / fcapz_eio_polarfire — Microchip PolarFire-family
wire [127:0] my_signals; // up to 256+ bits
// ELA — embedded logic analyzer (one instantiation, all JTAG internals bundled)
fcapz_ela_xilinx7 #(
.SAMPLE_W(128), .DEPTH(4096),
.SINGLE_CHAIN_BURST(1), // default: control + fast burst readout on USER1
.TRIG_STAGES(4), .STOR_QUAL(1),
.DECIM_EN(0), // 1 = enable sample decimation (/N divider)
.EXT_TRIG_EN(0), // 1 = enable external trigger I/O ports
.TIMESTAMP_W(0), // 32 or 48 = cycle-accurate timestamps (0 = off)
.NUM_SEGMENTS(1) // >1 = segmented memory with auto-rearm
) u_ela (
.sample_clk (sys_clk),
.sample_rst (reset),
.probe_in (my_signals)
);
// EIO — embedded I/O (optional, co-exists with ELA on a separate USER chain)
wire [31:0] eio_out;
fcapz_eio_xilinx7 #(.IN_W(32), .OUT_W(32)) u_eio (
.probe_in (my_signals[31:0]),
.probe_out (eio_out)
);ECP5 / Gowin — ELA + EIO combined (limited JTAG chains):
// On ECP5 (2 chains) and Gowin (1 chain), EIO shares the ELA wrapper:
fcapz_ela_ecp5 #(
.SAMPLE_W(128), .DEPTH(4096),
.EIO_EN(1), .EIO_IN_W(32), .EIO_OUT_W(32)
) u_ela (
.sample_clk (sys_clk),
.sample_rst (reset),
.probe_in (my_signals),
.eio_probe_in (my_signals[31:0]),
.eio_probe_out(eio_out)
);VHDL wrappers are also provided in rtl/vhdl/.
LiteX designs can use the optional fcapz.litex.FcapzELA helper to add the
ELA RTL sources and instantiate the JTAG-accessible wrapper from Python:
from migen import ClockSignal, ResetSignal
from fcapz.litex import FcapzELA
soc.submodules.ela = FcapzELA(
platform,
vendor="xilinx7",
sample_clk=ClockSignal("sys"),
sample_rst=ResetSignal("sys"),
probes={
"wb_cyc": bus.cyc,
"wb_stb": bus.stb,
"wb_adr": bus.adr,
"wb_dat_w": bus.dat_w,
},
depth=1024,
)
soc.ela.write_probe_file("build/ela.prob", sample_clock_hz=int(sys_clk_freq))The helper packs named LiteX/Migen signals into probe_in, records bit
offsets in probe_fields, can emit a .prob sidecar for the host, and keeps
control/readback on the existing JTAG transport. It does not consume LiteX
CSR or Wishbone address space. See
docs/04_rtl_integration.md
for details.
| Vendor | Primitive | User chains | ELA | EIO (needs 1) | EJTAG-AXI (needs 1) | EJTAG-UART (needs 1) |
|---|---|---|---|---|---|---|
| Xilinx | BSCANE2 | 4 (USER1-4) | USER1 control + default burst; optional USER2 legacy burst | USER3 | USER4 | USER4 (shared) |
| Intel | sld_virtual_jtag | Unlimited | inst 0 by default; optional inst 1 | inst 2 | inst 3 | inst 5 |
| ECP5 | JTAGG | 2 (ER1+ER2) | ER1 by default; optional ER2 | EIO_EN=1 on ER1 |
deferred to v2 | deferred to v2 |
| Gowin | JTAG | 1 | No burst | EIO_EN=1 |
deferred to v2 | deferred to v2 |
| PolarFire-family | UJTAG | 2 (USER1+USER2) | USER1 control + USER2 burst | EIO_EN=1 on USER1 |
deferred to v2 | deferred to v2 |
Verified Xilinx 7-series IR codes (xc7a100t, Arty A7): USER1=0x02, USER2=0x03, USER3=0x22, USER4=0x23.
Note: On Xilinx 7-series, only 4 USER chains exist. The EJTAG-UART wrapper defaults to USER4 (same as EJTAG-AXI). To use both in one bitstream, assign UART to a free chain (e.g. USER3 if EIO is not used — the Arty A7 reference design does this). On Intel, each gets a unique virtual JTAG instance, so both always coexist.
Xilinx ELA wrappers default to SINGLE_CHAIN_BURST=1: USER1 carries both
control and 256-bit burst readout. Set SINGLE_CHAIN_BURST=0 only for the
legacy USER2 dual-chain burst path.
On Gowin, the single chain means no burst readback — sample data is read word-by-word through the sample DATA window (functional but slower). Details are in the manual (see below).
On PolarFire-family devices, UJTAG exposes two user instructions from one
primitive. To use ELA and EIO together, enable EIO_EN=1 on the ELA wrapper;
EIO shares USER1 through the wrapper's register-bus mux.
Bit-level DR layouts, ELA/EIO address maps, EJTAG-AXI / EJTAG-UART bridge formats, and identity checks are maintained in one place so they do not drift from the RTL.
- User manual index — start here; chapter 04 — RTL integration covers chains, IR presets, and how the cores attach to the TAP.
- Canonical spec:
docs/specs/register_map.md— full register / shift encodings for ELA, EIO, EJTAG-AXI, and EJTAG-UART (ground truth; chapter 13 is a short pointer into that file).
| Area | Status |
|---|---|
Xilinx hw_server backend |
Implemented and hardware-validated on Arty A7 (7-series) |
| OpenOCD backend | Implemented, needs more hardware validation |
Xilinx 7-series wrappers (*_xilinx7.v) |
Implemented and hardware-validated on Arty A7-100T |
Xilinx UltraScale / UltraScale+ wrappers (*_xilinxus.v) |
Implemented in RTL (BSCANE2, identical to 7-series); not yet hardware-validated |
| Lattice / Intel / Gowin TAP wrappers | Implemented in RTL, host validation still limited |
| Microchip PolarFire-family TAP wrappers | Implemented in RTL, host validation still limited |
| Runtime channel mux | Implemented in RTL and host API/CLI/RPC |
| EIO over real transports | Implemented — USER3 on Xilinx; wrapper-shared chain on ECP5, Gowin, and PolarFire-family devices |
| EJTAG-AXI bridge | Hardware-validated on Arty A7 (single, auto-inc, burst, strobe, and error paths) with the trimmed FIFO / DEBUG_EN=0 reference build |
| EJTAG-UART bridge | Hardware-validated on Arty A7 (loopback: send, recv, recv_line, status) |
| Sample decimation | Implemented in RTL and host API/CLI |
| External trigger I/O | Implemented in RTL and host API/CLI |
| Timestamp counter | Implemented in RTL and host API/CLI |
| Segmented memory | Hardware-validated on Arty A7 (4 segments, auto-rearm) |
| Raw TCF transport | Planned |
All features are compile-time optional via parameters. The sample buffer uses dual-port BRAM, so scaling wider or deeper adds BRAM, not LUTs.
Numbers below are Slice LUTs and FFs from Vivado synthesis
reports on xc7a100t (2025.2), except the last row, which uses the
same post–place & route totals as the shipped arty_a7_top
reference (Apr 2026). The small rows are wrapper-inclusive: they include
the JTAG TAP/register/readout plumbing, not only fcapz_ela.v.
| Config | Slice LUTs | FFs | BRAM | Notes |
|---|---|---|---|---|
| 8b x 1024, A-only, slow USER1 readout | 596 | 779 | 0.5 | Smallest practical 1024-sample ELA; DUAL_COMPARE=0, optional features off |
| 8b x 1024, A-only, single-chain fast readout | 912 | 1,234 | 0.5 | One BSCANE2; 256-bit USER1 burst via SINGLE_CHAIN_BURST=1 |
8b x 1024, dual comparator, REL_COMPARE=0 |
2,021 | 1,725 | 0.5 | Compatibility trigger shape; EQ/NEQ/edges/changed |
8b x 1024, dual comparator, REL_COMPARE=1, INPUT_PIPE=1 |
2,010 | 1,754 | 0.5 | Adds <, >, <=, >=; compare hit is registered for timing |
| 8b x 1024, +storage qualification | 2,521 | 1,749 | 0.5 | Same compatibility harness with STOR_QUAL=1 |
| 8b x 1024, 4-stage sequencer | 2,954 | 2,788 | 0.5 | TRIG_STAGES=4, STOR_QUAL=0 |
32b x 1024, dual comparator, REL_COMPARE=0 |
2,472 | 2,099 | 1.0 | Wider samples mainly add BRAM/FFs |
arty_a7_top (placed, pre-DEBUG_EN always-on debug) |
3,244 | 4,562 | 3.5 | Historical baseline for the same validation reference |
arty_a7_top (placed, DEBUG_EN=0) |
2,318 | 3,168 | 3.5 | Bridge debug telemetry disabled; previous reference before the EJTAG-AXI FIFO trim |
arty_a7_top (placed, trimmed EJTAG-AXI FIFOs, DEBUG_EN=0) |
2,371 | 3,356 | 1.5 | Current reference: ELA with INPUT_PIPE=1, DECIM_EN, EXT_TRIG_EN, TIMESTAMP_W=32, NUM_SEGMENTS=4 + EIO 8/8 + EJTAG-AXI + axi4_test_slave; bridge debug telemetry disabled; EJTAG-AXI command/response queues set to 16 entries |
Optional ELA parameters (DECIM_EN, EXT_TRIG_EN, TIMESTAMP_W,
NUM_SEGMENTS, channel mux, etc.) add registers, comparators, and
extra BRAM (e.g. timestamp storage); the reference row above is the
authoritative “full validation” footprint for that combination.
Per-core deltas are not additive in synthesis because Vivado optimises
across hierarchy.
The trimmed EJTAG-AXI row comes from the Arty A7-100T Vivado 2025.2
post-place report after gating bridge-only debug telemetry and reducing
small bridge queues to their XPM minimum depth. Versus the previous
always-on debug baseline, the current reference is -873 slice LUTs,
-1,206 FFs, and -2.0 BRAM tiles. Versus the earlier DEBUG_EN=0
reference, the FIFO trim trades about +53 LUTs and +188 FFs for
-2.0 BRAM tiles; placed hierarchy reports u_ejtagaxi at 883 LUTs,
1,270 FFs, and 0 BRAM.
Fmax (reference build): sys_clk @ 100 MHz with WNS positive after
route on arty_a7_top (xc7a100t, same build). JTAG tck_bscan is
constrained at 10 MHz (adapter-dependent in practice, often higher).
Both the EJTAG-AXI and EJTAG-UART cores use fcapz_async_fifo, a
reusable async FIFO module with per-domain reset (wr_rst + rd_rst).
USE_BEHAV_ASYNC_FIFO=1 (default) uses a portable behavioral gray-coded
pointer FIFO; USE_BEHAV_ASYNC_FIFO=0 wraps a vendor primitive (Xilinx
xpm_fifo_async). The Xilinx wrappers set it to 0 automatically.
Synthesis / P&R from Vivado 2025.2, xc7a100t (Arty A7). Fits on even the smallest Artix-7.
// Default small core (Xilinx — swap suffix for other vendors)
fcapz_ela_xilinx7 #(.SAMPLE_W(8), .DEPTH(1024)) u_ela (
.sample_clk(clk), .sample_rst(rst), .probe_in(signals)
);
// Full featured
fcapz_ela_xilinx7 #(.SAMPLE_W(8), .DEPTH(1024), .TRIG_STAGES(4), .STOR_QUAL(1)) u_ela (
.sample_clk(clk), .sample_rst(rst), .probe_in(signals)
);GitHub Actions runs on every push and pull request to main or master:
| Job | What it checks |
|---|---|
lint-python |
ruff E/F/W rules on the whole repo |
test-host |
pytest tests/ -v --tb=short with the default not hw marker filter, plus an explicit JTAG readback pipeline regression for burst and timestamp stabilization paths |
lint-rtl |
python sim/run_sim.py --lint-only — shared iverilog -Wall elaboration for the core RTL, vendor wrappers, and simulation stubs |
sim |
python sim/run_sim.py — runs the same iverilog -Wall lint pass, then the default RTL regression: ELA behavior, ELA focused regressions, ELA configuration matrix, burst readout, single-chain pipe readout, EIO, and channel mux testbenches |
Hardware integration tests run manually (require physical Arty A7-100T + hw_server).
Optional GUI + hardware checks in tests/test_gui_hw_capture.py are
documented in CONTRIBUTING.md (FPGACAP_GUI_HW=1, not run in CI).
Those board-level checks now require every adjacent Arty counter sample to
increment by +1 when decimation is disabled, so partial burst readback
corruption is caught instead of hidden by a shorter valid prefix.
vivado -mode batch -source examples/arty_a7/build_arty.tclProduces examples/arty_a7/arty_a7_top.bit.
python sim/run_sim.py
python sim/run_sim.py --lint-onlyThe default command runs iverilog -Wall lint before compiling and running
the testbenches. The ELA configuration matrix covers small/scalable build
shapes such as DUAL_COMPARE=0, USER1_DATA_EN=0, disabled feature
registers, and REL_COMPARE=1 with INPUT_PIPE=1. CI uses the same runner
so local regressions and GitHub Actions exercise the same RTL lint target
list.
# Unit tests (no hardware needed) — full suite recommended locally
python -m pytest tests/ -v
# Hardware integration tests (requires Arty A7 + hw_server + built bitstream)
python -m pytest examples/arty_a7/test_hw_integration.py -v
# Force-skip hardware integration tests (e.g. laptop without board)
FPGACAP_SKIP_HW=1 python -m pytest examples/arty_a7/test_hw_integration.py -v
# GUI + real JTAG (fcapz-gui) — opt-in; requires PySide6, pytest-qt, and a board.
# Default pytest excludes @pytest.mark.hw; see CONTRIBUTING.md for details.
FPGACAP_GUI_HW=1 python -m pytest tests/test_gui_hw_capture.py -v --tb=short \
--override-ini='addopts=-p no:cacheprovider'fpgacapZero/
rtl/
fcapz_ela.v ELA core
fcapz_eio.v EIO core
fcapz_ejtagaxi.v EJTAG-AXI core
fcapz_ejtaguart.v EJTAG-UART core
fcapz_async_fifo.v Portable async FIFO used by bridge cores
fcapz_regbus_mux.v Register-bus mux for shared-chain wrappers
fcapz_version.vh Version/core-id header
dpram.v Dual-port sample/timestamp RAM
reset_sync.v Reset synchronizer
trig_compare.v Trigger comparator unit
jtag_reg_iface.v 49-bit register protocol
jtag_pipe_iface.v Single-chain register + burst pipe
jtag_burst_read.v Legacy separate-chain burst readout
fcapz_ela_*.v ELA vendor wrappers
fcapz_eio_*.v EIO vendor wrappers
fcapz_ejtagaxi_*.v EJTAG-AXI vendor wrappers
fcapz_ejtaguart_*.v EJTAG-UART vendor wrappers
jtag_tap/
jtag_tap_*.v Vendor TAP primitive adapters
vhdl/
fcapz_ela_*.vhd VHDL ELA wrappers
fcapz_eio_*.vhd VHDL EIO wrappers
tb/
*_tb.sv, *_tb.v RTL simulations and focused regressions
fcapz_ela_config_matrix_tb.sv
ELA parameter/configuration matrix
fcapz_ela_xilinx7_single_chain_tb.sv
Wrapper-level single-chain readback regression
jtag_pipe_iface_tb.sv Single-chain pipe protocol regression
jtag_burst_read_tb.sv Legacy burst readout regression
fcapz_async_fifo_equiv_tb.v
Behavioral vs primitive FIFO equivalence
host/fcapz/
analyzer.py ELA API
transport.py OpenOCD and hw_server transports
cli.py, rpc.py CLI and JSON-RPC entry points
eio.py EIO controller
ejtagaxi.py AXI bridge controller
ejtaguart.py UART bridge controller
events.py Capture summary/event helpers
probes.py .prob sidecar parser/writer
litex.py LiteX ELA integration helper
gui/ PySide6 desktop GUI modules and assets
examples/arty_a7/
arty_a7_top.v Reference design top-level
arty_a7.xdc Pin constraints
build_arty.tcl Vivado batch build
arty_a7*.cfg OpenOCD configs
test_hw_integration.py Hardware integration tests
tests/
test_*.py Host, CLI/RPC, transport, LiteX, probe-file,
bridge, GUI, and viewer tests
docs/
README.md User-manual index
01_overview.md ... User-manual chapters
specs/ Canonical reference docs
architecture.md Core block diagram
register_map.md Register map specification (ground truth)
transport_api.md Transport interface spec
waveform_schema.md Export format spec
Leonardo Capossio — bard0 design — hello@bard0.com
Apache License 2.0 — see LICENSE for details.

