CppHDL and Verilator RTL Tests
SkillAI & modelsUse 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.
No other account needed.
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
returnin_work()method, better putif (reset)in the end of_work()method or useif (reset) { ... } else { ... }. Slang does not supportdisablein 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
resetthrough the sameeval_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_VERILATORis called only afterdut.eval()at the rising edge, registered RTL can miss a one-cyclervalid/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/breadybehavior 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(...)orcopy_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 onclk. _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
rvalidis 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
.elfVerilator 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
constexprvalues in a specialized struct package must be concrete values for that specialization. Generated struct packages must not contain unresolved template parameter names such asWIDTH,CONV_TYPE,FMT, orTAG. - Template
Moduleclasses 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
Moduleclasses 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
constexprvalues 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