Skip to content

Commit c26a55b

Browse files
authored
Merge pull request #20 from lcapossio/feature/cocotb-regression
Add cocotb regression and Verilator ELA coverage runner
2 parents 3fb028c + 95eaa93 commit c26a55b

14 files changed

Lines changed: 3541 additions & 11 deletions

.github/workflows/ci.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,16 +126,30 @@ jobs:
126126
- name: Run Verilator lint and MULTIDRIVEN self-test
127127
run: python sim/run_verilator_lint.py --self-test
128128

129-
# -- 6. RTL simulation (iverilog + vvp) ---------------------------
129+
# -- 6. RTL simulation (cocotb + Icarus) --------------------------
130+
# Sharded so the 16-target ELA suite runs in parallel with the protocol
131+
# benches; cuts wall-clock at the cost of one extra job slot.
130132
sim:
131-
name: RTL simulation (iverilog + vvp)
133+
name: RTL simulation (cocotb + Icarus) [${{ matrix.shard }}]
132134
runs-on: ubuntu-latest
135+
strategy:
136+
fail-fast: false
137+
matrix:
138+
shard: [protocol, ela]
133139
steps:
134140
- uses: actions/checkout@v4
135141
- name: Install iverilog
136142
run: sudo apt-get install -y iverilog
137143
- uses: actions/setup-python@v5
138144
with:
139145
python-version: "3.11"
140-
- name: Run all RTL testbenches
141-
run: python sim/run_sim.py
146+
cache: pip
147+
cache-dependency-path: pyproject.toml
148+
- name: Install cocotb dependencies
149+
run: pip install -e ".[hdl]"
150+
- name: Run cocotb RTL regression (protocol shard)
151+
if: matrix.shard == 'protocol'
152+
run: python sim/run_cocotb.py --runner native --clean --skip-ela
153+
- name: Run cocotb RTL regression (ELA shard)
154+
if: matrix.shard == 'ela'
155+
run: python sim/run_cocotb.py --runner native --clean ela

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ dfx_runtime.txt
1515
# Build artifacts
1616
dist/
1717
packages/
18+
build/cocotb_ela/
19+
build/cocotb/
20+
build/verilator_ela_coverage/
1821
*.bit
1922
*.bin
2023
!host/fcapz/gui/assets/default_window_state_v6.bin
@@ -23,6 +26,8 @@ packages/
2326
# Simulation artifacts
2427
sim/*.vvp
2528
sim/*.vcd
29+
sim/ghdl/
30+
work-obj*.cf
2631

2732
# OS
2833
.DS_Store
@@ -32,3 +37,6 @@ Thumbs.db
3237
tests/_tmp/
3338
.codex_pytest_tmp*/
3439
.claude/
40+
41+
# Local-only working notes (per PROJECTS.md no_commit/ convention)
42+
no_commit/

README.md

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -124,17 +124,24 @@ and readback behavior without switching cores.
124124
### Developer quick start
125125

126126
```bash
127-
pip install -e ".[dev]"
127+
pip install -e ".[dev,hdl]"
128128
pytest tests/ -v
129-
python sim/run_sim.py
129+
python sim/run_cocotb.py --runner native --clean
130130
```
131131

132-
`python sim/run_sim.py` runs the shared RTL lint pass (`iverilog -Wall`)
133-
first, then the default simulation regression. Use
134-
`python sim/run_sim.py --lint-only` when you only want the RTL lint check.
132+
`python sim/run_cocotb.py --runner native --clean` runs the default cocotb RTL
133+
simulation regression. Use `python sim/run_sim.py --lint-only` when you only
134+
want the shared RTL lint check (`iverilog -Wall`).
135135
Run `python sim/run_verilator_lint.py --self-test` when changing RTL; it runs
136136
the full Verilog RTL matrix through Verilator driver lint for issues such as
137137
one register assigned from two always blocks.
138+
Run `python sim/run_verilator_ela_coverage.py --runner wsl` when changing ELA
139+
behavior and you want Verilator simulation plus merged line/toggle and bound
140+
functional coverage for the Verilog ELA benches.
141+
Run `python sim/run_cocotb_ela.py --runner wsl --hdl verilog` to execute the
142+
shared cocotb ELA core stimulus. The same Python tests can target a VHDL ELA
143+
core with `--hdl vhdl --vhdl-source <file>` when a VHDL core implementation is
144+
available.
138145

139146
Use the installed `fcapz` entry point for day-to-day ELA work. The legacy
140147
`python -m fcapz.cli` form still works, but the package install path is
@@ -695,7 +702,7 @@ GitHub Actions runs on every push and pull request to `main` or `master`:
695702
| `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 |
696703
| `lint-rtl` | `python sim/run_sim.py --lint-only` — shared `iverilog -Wall` elaboration for the core RTL, vendor wrappers, and simulation stubs |
697704
| `lint-rtl-verilator` | `python sim/run_verilator_lint.py --self-test` -- full-project Verilog RTL driver lint plus an intentional `MULTIDRIVEN` fixture proving the gate catches one reg driven by multiple always blocks |
698-
| `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, core manager, and channel mux testbenches |
705+
| `sim` (matrix: `protocol`, `ela`) | sharded cocotb RTL regression on Icarus, with `iverilog -Wall` enabled per bench and a `pyproject.toml`-keyed pip cache. The `protocol` shard runs `python sim/run_cocotb.py --runner native --clean --skip-ela` (trig compare, JTAG pipe/burst, EIO, core manager, channel mux, Xilinx7 single-chain wrapper, async-FIFO equivalence, EJTAG-AXI, EJTAG-AXI reset regression, EJTAG-UART). The `ela` shard runs `python sim/run_cocotb.py --runner native --clean ela` and exercises the 16-target cocotb ELA suite. |
699706

700707
Hardware integration tests run manually (require physical Arty A7-100T + hw_server).
701708
Optional **GUI + hardware** checks in `tests/test_gui_hw_capture.py` are
@@ -727,6 +734,9 @@ Produces `examples/arty_a7/arty_a7_top.bit`.
727734
python sim/run_sim.py
728735
python sim/run_sim.py --lint-only
729736
python sim/run_verilator_lint.py --self-test
737+
python sim/run_cocotb.py --runner wsl
738+
python sim/run_verilator_ela_coverage.py --runner wsl
739+
python sim/run_cocotb_ela.py --runner wsl --hdl verilog
730740
```
731741

732742
The default command runs `iverilog -Wall` lint before compiling and running
@@ -739,6 +749,29 @@ a full-project Verilog RTL matrix and stricter procedural-driver checks; its
739749
self-test must fail a deliberately bad multi-driver fixture before the job is
740750
considered valid.
741751

752+
The Verilator ELA coverage command builds and runs `fcapz_ela`,
753+
`fcapz_ela_bug_probe`, and `fcapz_ela_config_matrix` as Verilator simulations.
754+
It writes merged coverage to `build/verilator_ela_coverage/merged.dat` and
755+
annotated source under `build/verilator_ela_coverage/annotated/`. The runner
756+
also binds `tb/fcapz_ela_func_cov.sv` into each `fcapz_ela` instance so
757+
`--coverage-user` records functional events such as arm/reset, trigger commit,
758+
pre/post stores, overflow, segment completion, burst starts, and optional
759+
feature register activity.
760+
761+
The cocotb ELA command runs shared Python stimulus and scoreboarding directly
762+
against an HDL `fcapz_ela` top. It defaults to Icarus for Verilog and GHDL for
763+
VHDL, relaunching through WSL when requested. The Verilog target uses the same
764+
core sources as the SystemVerilog benches; the VHDL target expects a VHDL core
765+
source passed with `--vhdl-source` unless `rtl/vhdl/core/fcapz_ela.vhd` exists.
766+
It writes a small language-agnostic functional coverage JSON report under
767+
`build/cocotb_ela/`.
768+
769+
The general cocotb command runs the non-ELA cocotb replacements for the RTL
770+
simulation benches (`trig_compare`, JTAG pipe/burst, EIO, core manager,
771+
channel mux, Xilinx7 single-chain wrapper, async FIFO equivalence, EJTAG-AXI,
772+
EJTAG-AXI reset regression, and EJTAG-UART), and then runs the ELA cocotb suite
773+
unless `--skip-ela` is passed.
774+
742775
### Tests
743776

744777
```bash

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ include = ["fcapz*"]
4343
dev = ["pytest>=7.0", "pytest-qt>=4.2", "ruff>=0.4.0"]
4444
gui = ["PySide6>=6.6", "pyqtgraph>=0.13"]
4545
gui-preview = ["fpgacapzero[gui]"]
46+
hdl = ["cocotb>=1.9,<2"]
4647
litex = ["litex"]
4748

4849
[tool.pytest.ini_options]

rtl/fcapz_ela.v

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1472,7 +1472,10 @@ module fcapz_ela #(
14721472
((rd_addr_jtag - ADDR_TS_DATA_BASE[15:0]) >> 2) % TS_WORDS
14731473
) * 32;
14741474
end else if (WORDS_PER_SAMPLE == 1) begin
1475-
jtag_rdata <= {{(32-SAMPLE_W){1'b0}}, rd_data_sync1};
1475+
// Use sample_chunk_word so SAMPLE_W==32 does not
1476+
// emit the {0{1'b0}} zero-replication concat that
1477+
// Some lint and synthesis flows reject.
1478+
jtag_rdata <= sample_chunk_word(rd_data_sync1, 0);
14761479
end else begin
14771480
jtag_rdata <= sample_chunk_word(
14781481
rd_data_sync1,

sim/_runner_utils.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# Copyright (c) 2026 Leonardo Capossio - bard0 design - <hello@bard0.com>
3+
4+
"""Shared helpers for the cocotb / Verilator simulation runners under sim/.
5+
6+
Keeps WSL relaunch logic and JUnit XML parsing in one place so fixes do not
7+
have to be mirrored across run_cocotb.py, run_cocotb_ela.py and
8+
run_verilator_ela_coverage.py.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import platform
14+
import shlex
15+
import subprocess
16+
import xml.etree.ElementTree as ET
17+
from pathlib import Path
18+
19+
20+
def windows_to_wsl_path(path: Path) -> str:
21+
resolved = path.resolve()
22+
drive = resolved.drive.rstrip(":").lower()
23+
if not drive:
24+
return resolved.as_posix()
25+
rest = resolved.as_posix().split(":/", 1)[1]
26+
return f"/mnt/{drive}/{rest}"
27+
28+
29+
def translate_wsl_arg(arg: str) -> str:
30+
"""Translate a Windows-style absolute path argument to its WSL equivalent.
31+
32+
Non-path args pass through untouched. Recognises both `C:\\foo` and `C:/foo`
33+
spellings. Backslashes inside non-drive args are not rewritten because they
34+
may be meaningful (regex, escapes); callers that want POSIX paths should
35+
pass them through windows_to_wsl_path explicitly.
36+
"""
37+
if len(arg) >= 3 and arg[0].isalpha() and arg[1:3] in (":\\", ":/"):
38+
drive = arg[0].lower()
39+
rest = arg[3:].replace("\\", "/")
40+
return f"/mnt/{drive}/{rest}"
41+
return arg
42+
43+
44+
def running_in_wsl() -> bool:
45+
return "microsoft" in platform.release().lower()
46+
47+
48+
def relaunch_in_wsl(root: Path, argv: list[str]) -> int:
49+
"""Re-exec the current script inside WSL bash, translating known path args."""
50+
script_args = [
51+
windows_to_wsl_path(root / argv[0]),
52+
*(translate_wsl_arg(a) for a in argv[1:]),
53+
]
54+
script = "cd {} && python3 {}".format(
55+
shlex.quote(windows_to_wsl_path(root)),
56+
" ".join(shlex.quote(arg) for arg in script_args),
57+
)
58+
return subprocess.call(["wsl.exe", "-e", "bash", "-lc", script], cwd=root)
59+
60+
61+
def check_results(results_xml: Path) -> None:
62+
"""Raise SystemExit if the cocotb JUnit XML reports any failure/error."""
63+
results = ET.parse(results_xml).getroot()
64+
failures = len(list(results.iter("failure")))
65+
errors = len(list(results.iter("error")))
66+
if failures or errors:
67+
raise SystemExit(
68+
f"cocotb reported failures={failures} errors={errors}: {results_xml}"
69+
)

0 commit comments

Comments
 (0)