| name | cpphdl-verilator-rtl |
| description | 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. |
CppHDL and Verilator RTL Tests
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().
- 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();
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:
- Trace valid/ready on both sides of the boundary: DUT master output, C++ responder input, C++ responder output, DUT responder input.
- Check whether the valid pulse exists only between two
eval() calls.
- Compare the harness cycle order with a known passing test such as an L2 cache Verilator test.
- 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/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.