CppHDL and Verilator RTL Tests

SkillAI & models

Use when writing or debugging CppHDL tests that mix C++ testbench models with Verilated RTL, especially reset ordering, register inputs, AXI4 responder wiring, eval sequencing, _assign/_strobe discipline, and avoiding C++/SystemVerilog simulation mismatches.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the CppHDL and Verilator RTL Tests skill

What this skill tells your AI

The instructions your AI receives, as published by mirekez/cpphdl in .agents/skills/SKILL.md and read by ahel’s review.

Use this skill when editing CppHDL tests or harnesses that instantiate a Verilated module and connect it to C++ models such as Axi4Ram, Axi4RegionMux, UART, CLINT, or accelerator devices.

MAIN RULES:

  • Never try to explicitly evaluate ports or combs values! They calculated implicitly by calling comb-functions chain. Combs and ports are lambdas in cpphdl and can refresh all chain on demand. Value is cached until next clock start
  • Never call _assign() in work cycle - this function only binds lambdas to ports, it does not calculate anything!
  • Define variables only in the beginning of methods (this is not our requirement, this is requirements of verilator and yosys_slang)
  • Try to not use return in _work() method, better put if (reset) in the end of _work() method or use if (reset) { ... } else { ... }. Slang does not support disable in always blocks

Hard Rules

  • Put _ASSIGN(...), _ASSIGN_REG(...), , _ASSIGN_COMB(...), _ASSIGN_I(...), _ASSIGN_REG/COMB_I(...), _ASSIGN_INDEXED(...), and _ASSIGN_REG/COMB_INDEXED(...) assignments only in _assign().
  • Assign a CppHDL interface once with assignIf(...); do not assign its individual ports separately. Make the connection in the immediate common parent's _assign():
void _assign()
{
    assignIf(driver, responder, driver.source_out, responder.sink_in);
}

Do not replace this with one assignment per valid, ready, data, or other interface member. assignIf resolves both interface directions and expands the flattened SystemVerilog connections consistently.

  • CppHDL forbids assignments across hierarchy levels, matching synthesizable SystemVerilog module encapsulation. A module may connect its own ports and the exposed ports/interfaces of its immediate child modules, but it must not reach through a child into a grandchild or assign a child module's internal state. Expose an intermediate port or interface and connect the hierarchy one level at a time.
  • Put .strobe() and .apply() calls only in the parent _strobe() method. Do not call them from _work(), helper methods, comb functions, or _assign().
  • _assign() is structural elaboration. It should run once before the simulation cycle loop, not once per cycle. Never run any _assign() after _work() cycles begin
  • In Verilator mode, write all Verilated input ports before eval().
  • For registered logic, make the C++ harness ordering match the RTL edge being sampled. Do not rely on C++ CppHDL update order accidentally matching Verilator.
  • In rare case if you use Verilator for 0-clock latency module testing, to see comb output from Verilator use eval() with clk = 0. This will update combs without updating regs
  • sys_clock is mandatory global variable to be incremented once per main loop cycle, it is used to cache combinational variables functions results
  • it's better to put if (reset) section in the end of _work() method to do not forget to call nested _work() methods in case of reset
  • better use operators >> and << and | when possible instead of bits(a,b) and [] for logic<>, if it does not make logic much longer (bits() is slower)

Verilator Cycle Pattern

For a Verilated DUT connected to C++ responder models, use this shape unless the local test has a proven reason to differ:

void eval_dut(bool reset)
{
    dut.reset = reset;
    dut.input_a = input_a;
    dut.input_b = input_b;
    AXI4_RESPONDER_FROM_VERILATOR(dut, ram.axi_in, 0);
    dut.eval();
}

void cycle(bool reset = false)
{
    dut.clk = 0;
    eval_dut(reset);

    ram._work(reset);

    AXI4_RESPONDER_FROM_VERILATOR(dut, ram.axi_in, 0);
    dut.clk = 1;
    eval_dut(reset);

    ram._strobe();

    dut.clk = 0;
    eval_dut(reset);
    ++sys_clock;
}

The important point is not the exact helper names. The important point is that responder outputs are presented to the Verilated DUT before the clock edge that samples them, and C++ model state is strobed after that edge.

Reset

  • Drive reset through the same eval_dut(reset) path as normal cycles so all inputs and AXI responder signals are valid during reset.
  • Hold reset for multiple cycles when the DUT contains RAM init FSMs or caches.
  • Keep C++ model _work(reset) and _strobe() in the same parent cycle order used after reset.
  • Do not special-case reset by calling only dut.eval() and skipping connected C++ models unless the test explicitly needs disconnected reset behavior.

AXI4 Responder Wiring

Use AXI4_RESPONDER_FROM_VERILATOR(dut, responder.axi_in, index) whenever a Verilated DUT master port reads a C++ AXI responder.

Typical use:

AXI4_DRIVER_FROM_VERILATOR(ram.axi_in, dut, 0, u<32>, copy_to_logic<PORT_BITS>);
ram._assign();

// Each eval path, before dut.eval():
AXI4_RESPONDER_FROM_VERILATOR(dut, ram.axi_in, 0);

Pitfalls:

  • If AXI4_RESPONDER_FROM_VERILATOR is called only after dut.eval() at the rising edge, registered RTL can miss a one-cycle rvalid/bvalid.
  • If responder signals are forced too early without first evaluating the DUT's current master outputs, instruction or data refill can hang from reset.
  • Keep rready/bready behavior legal and stable. A testbench may hold ready high, but the DUT still must observe valid/ready on a clock edge.
  • For wide AXI data, use the established conversion helper, for example verilator_logic_to_wide(...) or copy_to_logic<PORT_BITS>(...); do not hand-copy only one word of a beat.

Register Inputs

Set Verilated register-like inputs directly before eval():

dut.reset_pc_in = reset_pc;
dut.memory_base_in = memory_base;
dut.mem_region_size_in[0] = region0_size;

Do not assign these through _ASSIGN()/_ASSIGN_REG() in Verilator-only code. _ASSIGN() is for CppHDL structural assignment inside _assign(). Verilator requires strait data for ports, not functions

C++ Model Order

For C++ CppHDL models connected around a Verilated DUT:

  • _assign() wires ports once.
  • _work(reset) computes next values for C++ models.
  • Verilated eval() computes and samples RTL depending on clk.
  • _strobe() commits C++ model registers and memory buffers.

Keep memory buffer .apply() inside the memory module _strobe() path only. If a parent calls .apply() manually, C++ and Verilator tests can diverge.

Debugging Handshake Hangs

When a Verilator-only hang appears:

  1. Trace valid/ready on both sides of the boundary: DUT master output, C++ responder input, C++ responder output, DUT responder input.
  2. Check whether the valid pulse exists only between two eval() calls.
  3. Compare the harness cycle order with a known passing test such as an L2 cache Verilator test.
  4. Remove temporary debug prints before finishing.

Common signatures:

  • C++ passes, Verilator hangs on first read: responder rvalid is generated by the C++ model, but is not presented before the Verilated clock edge.
  • Verilator hangs during instruction refill immediately after reset: responder feedback was presented before the Verilated master outputs were evaluated for the current low-clock phase.
  • Direct C++ device test passes, CPU .elf Verilator test hangs: the device logic may be fine; inspect the mixed C++/Verilator boundary first.

Using Axi4Driver, Axi4Responder and Axi4If interface

Use this skill when editing CppHDL models or tests or harnesses to verilator connected to/from Axi4

Hard Rules

  • Use structures with Axi4Driver and Axi4Responder instead of declaring separated registers for Axi4 signals
  • Use Axi4If interface instead of defining _PORT() for each Axi4 signal
  • Use operator= of Axi4If if you need to assign driver or responder to AxiIf
  • Use Module method assignIn if you need to bind 2 Axi4If interfaces of 2 different modules
  • If nothing of this works use macroses like AXI4_DRIVER_FROM AXI4_RESPONDER_FROM or similar with _VERILATOR suffix
  • Try to use right types like u<ID_WIDTH>, not uint32_t for variables when you need to use them

Developing RTL modules, devices, examples

Use this skill when developing and debugging new CppHDL module, device, example or functionality

Hard Rules

  • If you see bug in cpphdl functionality and fixing it - develop a new test in root tests/ folder or add section to correspondent old test
  • If you see bug in Tribe cpu code or modules or devices and fixing it - develop a new test in tribe_cpu/tests folder or add section to correspondent old test

CppHDL Template Specialization Conversion

Use these rules when changing template handling in the cpphdl converter.

  • Template structs/unions must be emitted only as concrete specializations. Numeric, type, and textual/string-like template arguments are all part of the generated package/type name. Do not generate an unspecialized primary-template package for a template struct.
  • Static constexpr values in a specialized struct package must be concrete values for that specialization. Generated struct packages must not contain unresolved template parameter names such as WIDTH, CONV_TYPE, FMT, or TAG.
  • Template Module classes keep numeric template parameters as SystemVerilog module parameters. Numeric parameters should not be added to the generated module name, and method/member expressions should preserve the symbolic parameter where possible.
  • Template Module classes use type and textual/string-like template parameters as specialization identity. These parameters are added to the generated module name and select a separate SV module for each type/text combination.
  • When a fixed type specialization contributes static constexpr values used by a module, resolve those fixed type constexprs to concrete values. Numeric module parameters remain symbolic.

Signals

GitHub stars
49
Forks
3
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
cpphdl-verilator-rtl
Source
github.com/mirekez/cpphdl