Ports of real testbenches¶
The clearest way to judge a verification framework is to run testbenches
that other people wrote. experiments/open_core_ports takes verification
environments from other projects, mainly lowRISC’s
Ibex RISC-V core and its DV tree, and
ports them to cpptb. Every test runs under both harnesses and the results
are compared. An error that shows up on one side only is a port defect. An
error on both sides is a property of the core or the test.
This page summarizes what exists there and what came out of it. It is a different thing from the open-source core benchmarks, which time framework-authored testbenches over vendored cores. The ports here run upstream’s own tests: 944 directed programs, an architectural test suite, and a UVM block environment, with upstream sources pinned by exact commit and never modified.
Nothing under experiments/ is wired into make test, the benchmark
registry, or the performance guard. Each port’s README.md and RESULTS.md
are the authoritative record. This page carries the shape and the headline
numbers.
The ports at a glance¶
Port |
Upstream reference |
Result |
|---|---|---|
|
Ibex’s core-level UVM environment, |
944 directed tests: the same 912 pass on both harnesses; 871,825 instructions co-simulated against Spike |
|
Ibex’s icache UVM block environment |
all ten tests, ten seeds each, on both harnesses: 200 of 200 runs pass |
|
riscv-arch-test suite on Ibex Simple System |
|
|
Ibex’s CSR block bench and C++ reference model |
reproduces upstream’s run transaction for transaction; found a reference-model gap, filed upstream |
|
Ibex’s hand-written Verilator harness |
CoreMark 100 iterations: identical cycle count and validated score, |
|
riscv-dv random instruction generation (pyflow) |
generated programs run under both harnesses with Spike checking every retired instruction, no mismatches |
The core-level Ibex port¶
ports/core_ibex_cpptb is the largest port. It rewrites Ibex’s real
verification environment, dv/uvm/core_ibex, against cpptb, and runs
against ports/core_ibex_uvm, the unmodified UVM environment on Verilator.
Both harnesses drive the same Ibex (--config opentitan: PMP, icache with
ECC and scrambling, SecureIbex), elaborated with the same parameters, and
run byte-identical program binaries. The cpptb runner imports the baseline’s
compile rather than reimplementing it.
Across the 944-entry directed testlist:
912 of 944 pass, and they are the same 912 on both harnesses. 943 of 944 entries reach the identical outcome; the one difference is a test that runs out of wall clock on the baseline and out of cycles here. All 744 ePMP exit codes recovered from the execution trace agree exactly.
The run covers 14,335,393 cycles and 881,470 retired instructions, of which 871,825 are co-simulated against Spike instruction by instruction.
Record-and-replay closes the loop that a shared verdict cannot. The baseline writes down every pin its environment drives, and 375,100 cycles replay on the port with all thirteen output fields matching on every cycle.
Fault injection shows the checks are live rather than merely quiet. Forty instruction-memory corruptions in one program are caught 38 times. A data-memory sweep that the co-simulation scoreboard catches 17 times is caught zero times with the reference model switched off.
The whole testlist takes 105 seconds of wall clock against the baseline’s
2,365 at the same parallelism. The honest framework-to-framework number is
smaller. Measured like for like, with the same testbench decomposition on
both sides and the runs interleaved on one host, the ratio is 48.8x, and
about a fifth of the raw gap comes from testbench differences rather than
the frameworks. The port’s RESULTS.md records that decomposition and its
caveats in full.
Spike co-simulation¶
Every instruction-level claim above rests on lockstep co-simulation against
Spike, the RISC-V
reference simulator. The integration is upstream’s own. lowRISC maintains an
ibex_cosim branch of Spike with a co-simulation API, and Ibex’s dv/cosim
layer (spike_cosim.cc, cosim_dpi.cc) drives it. The ports pin that
branch and reuse the layer unmodified, in two shapes:
Bound in as RTL, through DPI. The Simple System ports compile Ibex’s own co-simulation checker and bind it into the design, exactly as upstream’s Verilator flow does. Both harnesses carry their own Spike, so a mismatch has to appear on both sides or neither.
Linked directly as C++.
core_ibex_cpptbholds theSpikeCosimobject in the testbench and calls its C++ interface, where the UVM environment holds a handle and calls the same functions through DPI imports. The co-simulation scoreboard is ported whole; nothing it checks was dropped.
The per-instruction check is substantive. The PC must match, a register write must match or be correctly suppressed, and synchronous traps must match, including the trapping PC. Every data-side memory access is compared on address, direction, data, and byte enables. Interrupt, NMI, debug-request, and performance-counter state are pushed into Spike each step so that asynchronous events land at the same instruction on both models.
Spike is built once, without root, into the experiment’s own tree:
make -C experiments/open_core_ports fetch # pinned sources, exact commits
make -C experiments/open_core_ports tools # local dtc/libelf/z3, no sudo
make -C experiments/open_core_ports spike # ~5 minutes, into deps/
build_spike.py configures the two flags the checker depends on
(--enable-commitlog, --enable-misaligned) and installs a pkg-config
file. Every consumer finds Spike through pkg-config alone.
Co-simulation also produced the one finding where the reference model was
wrong and the DUT was right. The pinned Spike registers menvcfg but not
menvcfgh, so on RV32 a write to the high half traps in the model, where
Ibex correctly implements it as read-only zero. The disagreement reproduces
identically on both harnesses.
The architectural tests¶
ports/riscv_arch_tests runs the official
riscv-arch-test suite on
Ibex Simple System. Applicability is derived from each test’s declared
requirements rather than maintained by hand. Three configurations are
measured: small (98 of 98 pass, at 1.086x upstream’s wall time),
bmfull (a materially different core with PMP, a writeback stage, and
bitmanip; 177 of 193 pass, and all 16 failures are identical on both
harnesses), and cosim (98 of 98 with Spike in lockstep on both sides, at
0.989x). Checking every retired instruction costs about 2.6x the
uncosimulated run, and it costs both harnesses the same.
Ibex upstream has both an architectural test suite and a reference model,
but never uses them together. The cosim configuration here is that missing
combination. A six-configuration matrix mirroring Ibex’s own CI (small
through opentitan) additionally runs upstream’s four co-simulated
programs: 11 of 11 runnable combinations pass on both harnesses, with
16,977,869 instructions checked.
ports/riscv_dv closes the stimulus side. riscv-dv’s Python flow generates
random programs that run under both harnesses with Spike checking every
retired instruction, including a run under asynchronous interrupt stimulus
that matched 1,046,965 instructions with no mismatches.
The block-level ports¶
ports/ibex_icache_cpptb ports all ten tests of Ibex’s icache UVM
environment. 200 of 200 runs pass across ten seeds on both harnesses. A
replay harness drives the baseline’s recorded stimulus into the port: 180 of
180 replays pass, with DUT outputs matching cycle for cycle over 4,699,689
cycles. This port appears throughout the docs as the origin of the
write-model work, and its RESULTS.md documents
the Verilator randomization defect the comparison surfaced.
ports/ibex_cs_registers ports the CSR block bench against upstream’s C++
reference model and reproduces upstream’s run transaction for transaction,
until transaction 1,119, where the reference model itself has a gap: ePMP
MML write suppression is not modeled. The finding was confirmed against the
RTL’s documented behavior and
filed upstream. The control
run also showed why paired running matters. The unmodified upstream bench on
Verilator 5.050 drives zero transactions and prints TEST PASSED.
What porting found¶
Porting somebody else’s testbenches exercises every tool in the chain. The
tally, with a reduced reproducer and a working workaround for each entry:
eleven Verilator defects, five of them in constrained randomization (one of
those, a randomize() with constraint naming a variable item being
silently dropped, was behind 22 of 27 failing test classes); fifteen in Ibex
and lowRISC’s DV library, including all 132 assertions compiling away under
Verilator both upstream and here, which is why the ports now run them by
default; seven in riscv-dv; one in Spike (the menvcfgh case above); and
eight limitations of cpptb itself, of which the discovery and timing-backend
defects were fixed in the framework and the rest are recorded.
experiments/open_core_ports/FINDINGS.md is the master list.
Reproducing¶
Everything is pinned and fetched, never vendored. sources.toml records
exact commits, licenses, and the purpose of every dependency, and fetch.py
(standard library only) verifies and unpacks them. The per-port commands
live in each port’s README; the shared prerequisites are the three make
targets shown above. Ports write only under their
own directory, work/, or results/. Upstream sources under deps/ are
never modified.