Skip to content
cccskills
FreePublished on GitHub

cpphdl-verilator-rtl

Use when writing or debugging C++HDL 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.

The English summary is being prepared. Showing the original description.

mirekez/cpphdl54 stars(GitHub 54 + cccskills 0)

Run score Not tested

See how to install

Files included (1)

  • SKILL.md10.1 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

C++HDL and Verilator RTL Tests

Use this skill when editing C++HDL 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 C++HDL 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.

  • C++HDL 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++ C++HDL 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 C++HDL structural assignment inside _assign(). Verilator requires strait data for ports, not functions

C++ Model Order

For C++ C++HDL 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 C++HDL 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 C++HDL 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

C++HDL 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.

C++HDL conversion development rules

  • There shouldnt be code worsening in SV generation just because cpphdl is unsure in something. SV code shouldnt make cast where it didnt in C++ and it is not 100% necessary (due to differences between C++ and SystemVerilog casting/assignments)

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

Report a problem with this skill