agentsclimarketplace

Vtestgen

Skill phamcuong21478/rtl-skills/skills/vtestgen

Scaffold a Makefile-driven, CPU-bus-controlled testbench project for an IP and generate a comprehensive set of self-checking testcases, validate each with make, and write a testcase listFrom its SKILL.md

Install
npx -y skills add phamcuong21478/rtl-skills --skill vtestgen

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 7 stars7 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
  • runs commandsInstructs the agent to run 4 commands, including `xvlog --version` and 3 more.

SKILL.md

25.1 KB, ~7.0k tokens by cl100k_base, as published. Nobody here has run it

Verilog Testcase Generator

Overview

This skill builds the IP-level test environment in two phases:

  • Phase A — Scaffold the bench project. Parse <ip>_top, then generate a reusable, Makefile-driven simulation project under tb/<ip>/: build system, clock/reset generators, a DUT-instance core, CPU-bus BFM tasks, and one CPU-controllable stimulus driver per data interface.
  • Phase B — Plan and generate the suite. Plan testcases across six mandatory categories, write each as a thin tb_*.sv that drives the bench only through the CPU bus, validate each with make, and write tc_list.md.

vtestrun later executes the suite through the same Makefile.

The input is an IP name. The outputs are:

tb/<ip>/Makefile              top dispatcher (auto-discovers tb_*.sv)
tb/<ip>/script/library.mk     per-testcase xvlog/xelab/xsim recipe
tb/<ip>/script/rtl.f          generated RTL hierarchy filelist
tb/<ip>/script/skip.list      names of skipped testcases, excluded by the Makefile
tb/<ip>/clock.vh              parameterized clock + sync-reset generators
tb/<ip>/dut.vh                reg-map, DUT signals/instance, sti instances, CPU BFM tasks + sequences
tb/<ip>/sti_<iface>.sv        one CPU-controllable stimulus driver per data interface
tb/<ip>/sco_<iface>.sv        one CPU-controllable scoreboard per checkable stream output (optional)
tb/<ip>/tb_<name>.sv          self-checking testcases (CPU-bus only)
tb/<ip>/tc_list.md            testcase index with status
tb/<ip>/sim/<tc>/             disposable build dirs

Template files for every artifact live in templates/ next to this SKILL.md. Treat them as the canonical shape; fill the {{...}} placeholders from the parsed IP. Testbench code is SystemVerilog (.sv) — permitted under tb/ (see .claude/skills/shared/CodingStyle.md).

Central design rule — the CPU bus is the single control plane. Every testcase drives the whole bench using only cpu_write / cpu_read / cpu_read_check (and the higher-level sequences built on them in dut.vh). The DUT is configured over the CPU bus; traffic generators live in separate sti_*.sv modules that are themselves CPU-bus slaves, so the test script configures and starts stimulus with cpu_write too.

Prerequisites

ToolPurposeCheck
xvlogcompile / lintxvlog --version
xelabelaboratexelab --version
xsimsimulatexsim --version
makebuild orchestrationmake --version

If a required tool is missing, report it and stop — do not fake validation.


Phase A — Scaffold the Bench Project

Step A1 - Understand the IP and parse the top

Read before generating anything:

  1. ./doc/<ip>.md — full IP behavior, architecture, interface protocols, register map.
  2. ./rtl/<ip>_top.v — exact top-level port list, signal names, and widths.

Parse the top module header and classify every port:

ClassHow to detectBench treatment
clockname matches clk* / *_clkgenerate in clock.vh, period param T_<NAME>
resetname matches rst* / *_rst* / *_ngenerate sync reset in clock.vh
CPU bus*cpu*, or *_we/*_oe/*_addr/*_din/*_dout, or an APB/AXI-lite-ish groupwire to cpu_* and the BFM tasks; build the register map
data-plane inremaining inputs (valid/data/sof/eof/...) grouped by prefixdriven by an sti_*.sv module
data-plane out / statusremaining outputsstatus outputs: checked via CPU-readable status registers. A stream output (continuous valid/data beats) gets a sco_*.sv scoreboard (Step A6) for per-beat checking

Also extract parameters (name + default, e.g. DATA_W, ID_BIT, RS_LV).

If the IP has no CPU bus, still generate the CPU bus in the bench as the control plane for the sti_* drivers, and connect the DUT's data ports directly to the sti outputs / to observation logic. Note this to the user.

Present a short parse summary (params, classified ports, detected clocks, detected data interfaces → planned sti_* drivers, detected stream outputs → planned sco_* scoreboards) before generating files. Adjust on feedback.

Step A2 - Create the skeleton, copy the build system, generate rtl.f

Create folders tb/<ip>/ and tb/<ip>/script/ (and sim/ is created on demand by the Makefile). Never delete existing files; if Makefile / script/library.mk already exist, diff against the template and only offer to update, do not overwrite silently.

Copy verbatim:

  • templates/Makefiletb/<ip>/Makefile
  • templates/library.mktb/<ip>/script/library.mk

Generate tb/<ip>/script/rtl.f from templates/rtl.f: walk the instantiation hierarchy of <ip>_top following .claude/skills/shared/HierarchyFilelist.md, and list every required file as RTL_FILES += $(RTL_FOLDER)/... or $(LIB_FOLDER)/..., top module first. Both Makefiles include this fragment, so it is the single source of the compiled filelist (no wildcard globbing).

Step A3 - Generate tb/<ip>/clock.vh

Expand templates/clock.vh: one always toggle per detected clock (period T_<NAME>), one synchronous reset per clock domain using RS_LV (assert = RS_LV, deassert after 10*T_<NAME>). One clock is the "system" clock ({{SYS_CLK}}, used by watchdog/run-length); the CPU bus uses {{CPU_CLK}} (may be the same clock).

Step A4 - Generate tb/<ip>/dut.vh

Expand templates/dut.vh:

  • {{REGISTER_MAP}}localparam REG_<NAME> = 20'h..; for every CPU register. Take real addresses from doc/<ip>.md or the RTL decode; if none exists, emit a clearly-marked TODO block with example registers.
  • {{DUT_SIGNALS}} — declare every DUT port with exact widths/names: bench-driven inputs and the CPU bus as reg/handled by tasks, DUT outputs as wire, data-plane inputs as wire (driven by sti modules).
  • {{DUT_PARAM_MAP}} / {{DUT_PORT_MAP}}.NAME(value) lines; connect the CPU port group to cpu_*, data-plane ports to the matching sti_* outputs, clocks/ resets to the generated names. The DUT read-data output connects to a dut_cpu_dout wire so it can be muxed.
  • {{STI_INSTANCES}} — one instance per sti_*.sv (Step A5), each given a unique STI_ADDR on a 0x100-word window (STI_BASE, STI_BASE+'h100, ...), sharing the CPU bus. Bind RS_LV (and DATA_W) to the DUT's values so the driver resets at the DUT's polarity — a driver left at the default RS_LV=0 while the DUT is RS_LV=1 is held in reset for the whole run.
  • {{SCO_INSTANCES}} — one instance per sco_*.sv (Step A6), each given a unique SCO_ADDR on a 0x100-word window (SCO_BASE, SCO_BASE+'h100, ...), sharing the CPU bus and tapping a DUT output. Bind RS_LV (and DATA_W) to the DUT's values, same as the sti drivers. Leave empty (and drop SCO_BASE) if no scoreboard is generated.
  • Each slave owns a 256-word window, so its registers — including any width-dependent payload/capture that spans NW = DATA_W/32 words — never reach the next slave's base (good for DATA_W up to 8192). Keep every slave base 0x100-aligned and any word-addressed payload within its window.
  • {{STI_DOUT_OR}} / {{SCO_DOUT_OR}} — OR of each sti / sco instance's *_dout for the read mux. Substitute 32'h0 for either term when that group is empty.
  • {{CPU_CLK}} / {{CPU_CLK_UPPER}} — the CPU-domain clock name used by the BFM tasks.

The three BFM tasks (cpu_write, cpu_read, cpu_read_check) are already in the template; keep them, only fix the clock name. cpu_read_check prints [FINISH] FAIL + $finish on mismatch — this is the failure path of the contract.

Each access is held for a parameterized number of CPU clocks — CPU_WR_LAT for writes, CPU_RD_LAT for reads (declared by the testcase, default 1). Set these higher when the DUT's CPU slave needs more than one cycle per access; read data is sampled at the end of the CPU_RD_LAT window while cpu_oe is still asserted. For a slave with a variable-latency ready/ack handshake, poll that signal in the task instead of using a fixed latency.

Put reusable high-level sequences in dut.vh, not in the testcases. Any multi-step CPU sequence more than one testcase will repeat — load a key/config block, trigger+poll a "done" bit, load a payload word-by-word, fire a beat and wait for capture, read a payload back — belongs here as a task built on the BFM tasks (e.g. load_key, cfg_run, sti_load, sti_read, sti_beat). Make them width-generic by looping 0..NW-1 over the word-addressed sti_* registers. This keeps every tb_*.sv to pure intent and lets a testcase at a different DW reuse the exact same sequences. The template already ships a commented arm_stream / check_stream pair (the driver↔scoreboard runtime-sync handshake from Step A5): uncomment and keep it when the bench has an sti_*/sco_* pair, drop the sco half otherwise.

Step A5 - Generate tb/<ip>/sti_<iface>.sv per data interface

For each data-plane input group from Step A1, expand templates/sti_template.sv:

  • Name it sti_<iface>.sv, module sti_<iface>.
  • Keep the CPU-slave wrapper (write-decode of ENA/RATE/MODE/SEED/CLR, read-decode into cpu_dout, address self-decode from STI_ADDR) unchanged — this is what makes the driver controllable from the test script via cpu_write only.
  • The GENERATOR BODY ships MODE-selectable (chosen at run time via the MODE register, no recompile): MODE_LFSR (pseudo-random, replicated to fill DATA_W), MODE_COUNT (byte-stream counter-up), and MODE_FILE (replay DATA_W-wide words from PATTERN_FILE via $readmemh). Keep the modes that fit this interface, add ones it needs (packet burst, AXI master, sof/eof framing, ...), and match the DUT's handshake (valid/ready) and data width. A per-project "specific pattern" belongs in a hex file driven by MODE_FILE, not hardcoded in the body.
  • Runtime sync with the mirroring sco_*. The driver and scoreboard are two independent generators; the CLR register arms each one back to the start of its sequence (reseed from SEED, counters to 0) without a full DUT reset. The testcase syncs them by: write the same MODE/SEED to both → pulse CLR on both → enable the sco → enable the driver. This holds across DUT latency and back-pressure because the sco advances per accepted output beat, but only when the DUT carries the stream 1:1 in order (FIFO/CDC/passthrough). For a DUT that drops/reorders/transforms beats, the mirror is invalid — use a reference-model sco_* (Step A6). Put the arm-and-enable handshake in a dut.vh sequence so every testcase reuses it.
  • Wire the handshake ready on both sides. The driver consumes the DUT input ready via its out_ready port — it holds each beat until accepted and advances only then, so it stays in lock-step with the sco under back-pressure; tie out_ready to 1'b1 in dut.vh if the input has no ready. The DUT output ready is only tapped by the sco — something else must drive it: tie it high for a plain run, or add a small ready/throttle driver (an sti_*-style CPU slave) for back-pressure testcases.
  • Expose every output the DUT data port needs; expose status counters and any captured output as readable registers so testcases can cpu_read_check them.
  • Make the driver width-generic — never hardcode the data width. Parameterise by the DUT's data width (DW, plus UW/EW sidebands), derive everything from it (localparam NW = (DW+31)/32;, NB = DW/8;). Every payload/capture register and out_data must be [DW-1:0]; the shipped modes already size off DW. A driver hardcoded to one width silently works at the default DW then fails the first multi-block (DW=N*128) testcase.
  • If a testcase must push an exact payload over the CPU bus (rather than via MODE_FILE), expose it as word-addressed registers — the CPU is 32-bit — at a base offset (PT word i at STI_PT+i, i = 0..NW-1) decoded with a range check + variable indexed part-select (pt[(cpu_addr-STI_PT)*32 +: 32]), exactly the same way sco_template.sv word-addresses its capture payload. Such a payload occupies NW CPU words, so the slave's address stride must clear it (see Step A4).

Additional sti_*.sv files can be added later; they are picked up automatically by the sti_*.sv glob in the Makefiles.

Step A6 - Generate tb/<ip>/sco_<iface>.sv per checkable stream output (optional)

Generate a scoreboard only for data-plane outputs that produce a continuous stream (valid/data beats), where per-beat checking adds value over reading a final status register. Skip it for IPs whose only outputs are status registers — those are checked directly with cpu_read_check. If no scoreboard is generated, also drop the SCO_INSTANCES/SCO_DOUT_OR/SCO_BASE hooks from dut.vh.

For each such output, expand templates/sco_template.sv:

  • Name it sco_<iface>.sv, module sco_<iface>.
  • Keep the CPU-slave wrapper unchanged (write-decode of ENA/MODE/SEED/CLR, read-decode of ERRCNT/CHKCNT + word-addressed first-mismatch capture, address self-decode from SCO_ADDR). This is what lets the test script configure and read the scoreboard via the CPU bus only.
  • The scoreboard taps the DUT output (in_valid/in_ready/in_data) — it must never drive the DUT. Wire the tap in dut.vh's {{SCO_INSTANCES}}.
  • Replace only the COMPARE BODY to match the interface handshake. Default: the scoreboard regenerates the expected stream itself from MODE/SEED/ PATTERN_FILE — the same MODE_LFSR/MODE_COUNT/MODE_FILE sources as sti_*.sv — so no expected payload is pushed over the CPU bus. The testcase just sets the scoreboard's MODE/SEED to match the driver feeding the DUT. Alternatives: build a reference model that recomputes expected from observed DUT inputs (use only when the transform is non-trivial); or, for irregular expected data, add a word-addressed EXP register written by the testcase.
  • On a mismatch the scoreboard prints [ERROR] ... then [FINISH] FAIL + $finish (self-fail, consistent with cpu_read_check). ERRCNT is still exposed so a testcase may instead defer the verdict with cpu_read_check(SCO_ERRCNT, 32'd0).
  • Width-generic like the sti drivers — parameterise by DATA_W, derive NW/NB from it, word-address the capture payload, never hardcode the width.

Additional sco_*.sv files are picked up automatically by the sco_*.sv glob in the Makefiles.


Phase B — Plan and Generate the Suite

Step B1 - Plan the test suite

List all planned testcases across the six mandatory categories:

CategoryCoverage target
ResetDUT reaches defined idle state after reset; all outputs at documented reset values
BasicOne test per documented feature or operating mode
EdgeBoundary data values (0, all-ones, max-minus-1), empty/full conditions, single-cycle bursts
Back-pressureValid/ready handshakes held off; pipeline stalls and drains correctly under sustained back-pressure
Error injectOut-of-spec stimulus, concurrent events, overflow/underflow conditions
StressBack-to-back transactions with no idle cycles; maximum throughput for at least 1000 clock cycles

Present the full plan as a table before writing any code. Adjust on feedback.

Step B2 - Write each testcase

For each planned testcase, create tb/<ip>/tb_<category>_<name>.sv by expanding templates/tb_basic.sv. File name lowercase, underscores, no spaces, tb_ prefix (so the Makefile auto-discovers it).

Each testcase must:

  • declare the clock-period params (T_*), RS_LV (set to the DUT's own RS_LV default parsed from <ip>_top, so the bench resets at the DUT's polarity), the cpu access latencies (CPU_WR_LAT, CPU_RD_LAT), and clk/rst regs, then `include "clock.vh";
  • declare the DUT parameters, then `include "dut.vh";
  • contain a watchdog initial that prints [FINISH] FAIL on timeout;
  • have a single test-script initial block that uses only the reusable sequences from dut.vh plus cpu_write/cpu_read/cpu_read_check to: release reset, configure the DUT, configure+start the sti_* drivers, let traffic run, check status/captured output via cpu_read_check, and finish with [FINISH] PASS.

Keep the testcase thin — sequence calls and the golden check, no bus-level poking and no direct access to DUT or sti internals.

Checking and verdict convention

  • Status/register outputs are checked with cpu_read_check against CPU-readable registers. On mismatch it prints [ERROR] time= ... MISMATCH ... then [FINISH] FAIL and $finish automatically.
  • Stream outputs are checked by their sco_* scoreboard: the test script sets the scoreboard's MODE/SEED to match the driver feeding the DUT and enables it with cpu_write; the scoreboard regenerates the expected stream and self-fails on the first mismatched beat. For a deferred verdict, end the run and assert cpu_read_check(SCO_ERRCNT, 32'd0) instead. Either way the test script still touches the bench only through the CPU bus.
  • For extra diagnostic context before a manual failure, print an [ERROR] time= ... line with the relevant signal values, then [FINISH] FAIL and $finish. vtestrun harvests [ERROR] lines into the issue report.
  • A passing testcase ends with [FINISH] PASS then $finish. This token is the single machine-readable verdict (see .claude/skills/shared/CodingStyle.md).

For an algorithmic block, also emit a second testcase at a non-default width (DW=N*128, etc.) that reuses the same dut.vh sequences — it proves the generated sti_*.sv is genuinely width-generic.

Step B3 - Self-check each testcase with make

Run from inside tb/<ip>/:

make tb_<category>_<name>

This compiles RTL + sti_*.sv + the one testcase, elaborates, and simulates under a timeout.

The self-check is a structural gate, not a verdict. It passes when the testcase produces a clean [FINISH] token — PASS or FAIL — within the timeout. The goal is a well-formed, runnable testcase, not a passing DUT. Determining the authoritative pass/fail is vtestrun's job. Do not tune a testcase to force [FINISH] PASS: that can hide a real DUT bug.

Re-run note: make tb_<name> is a no-op while a simulate.log already exists for that testcase. After editing, clear the stale result first — make cleanf (drops non-PASS sim dirs) or rm -rf sim/tb_<name> — then re-run.

ResultAction
compile error (xvlog)fix signal name/width/port-map in dut.vh or the sti module; clear + rerun
elaborate error (xelab)add the missing RTL/lib file to rtl.f; confirm each sti_*.sv compiled; clear + rerun
sim timeout (no [FINISH])stimulus never started or DUT stalled — check the cpu_write enable sequence / handshake; clear + rerun
no [FINISH] tokentestcase ended without printing the verdict — fix the result block; clear + rerun
[FINISH] FAILthe testcase ran cleanly — self-check passes. First confirm the testcase's own expected value is correct: if it is wrong, fix it; if the expected value is right and the DUT disagrees, leave it — that is a real finding for vtestrun/vdebug. Either way mark the testcase OK.

Maximum 2 rewrite attempts per testcase. If after 2 attempts it still cannot produce a clean [FINISH] (compile/elaborate error, or timeout with no token), record it as SKIPPED in tc_list.md and continue. Do not block the suite on one testcase.

Step B4 - Write the testcase list

After all testcases are written and self-checked, write tb/<ip>/tc_list.md:

# Testcase List: <ip>

## Bench

Makefile-driven project at `tb/<ip>/`. Run a testcase with `make tb_<name>`;
summarize with `make report`. RTL filelist: `tb/<ip>/script/rtl.f`. `SKIPPED`
testcases are mirrored into `tb/<ip>/script/skip.list`, which the Makefile excludes
from `make`/`make report` (the `.sv` files stay in place).

## Testcases

| # | File | Module | Category | Description | Status |
|---|------|--------|----------|-------------|--------|
| 1 | tb_reset_basic.sv | tb_reset_basic | Reset | DUT reaches idle after reset | OK |
| 2 | tb_basic_stream.sv | tb_basic_stream | Basic | Single packet flows end-to-end | OK |
| 3 | tb_edge_empty.sv | tb_edge_empty | Edge | Empty input condition | SKIPPED |

Status values: OK (well-formed testcase — self-check produced a clean [FINISH], PASS or FAIL), SKIPPED (could not produce a clean [FINISH] after 2 attempts). A testcase whose [FINISH] FAIL reflects a real DUT bug is still OK here — the failure is reported by vtestrun, not hidden at generation time.

Then regenerate tb/<ip>/script/skip.list as a mirror of the current SKIPPED rows: overwrite it with exactly the testcase names (e.g. tb_edge_empty) whose status is SKIPPED, one per line. Always rewrite from the current tc_list.md — do not append. This way a previously-skipped testcase that now self-checks clean is OK in tc_list, so it drops out of skip.list automatically and the Makefile runs it again; a testcase still skipped stays excluded. tc_list.md is the single source of truth; skip.list is just its machine-readable projection for the Makefile. If no testcase is skipped, write an empty skip.list (or omit it).

Conventions (must hold)

  • CPU-bus-only: test scripts touch the bench only through the CPU BFM tasks and dut.vh sequences. No direct poking of DUT or sti internals from tb_*.sv.
  • Verdict: print [FINISH] PASS / [FINISH] FAIL then $finish; cpu_read_check auto-fails on mismatch; no token ⇒ FAIL.
  • Width-generic sti_*.sv, sco_*.sv, and dut.vh sequences — never hardcode the data width.
  • Reset-polarity-generic: sti_*.sv/sco_*.sv reset against their RS_LV parameter, never a hardcoded !rst_n; dut.vh binds RS_LV on every sti/sco instance to the DUT's value (the bench reset in clock.vh is already RS_LV-driven).
  • Scoreboards are CPU-slave observers: a sco_*.sv taps a DUT output and never drives it; it is configured/read over the CPU bus like any other slave.
  • Reusable sequences live in dut.vh, not in testcases.
  • Testbench is SystemVerilog under tb/; RTL stays Verilog-2005.

Known Issues and Fixes

[xsim] - unpacked array local to a function automatic is not retained per-index

Symptom: A reference-model sco_*.sv (or any TB function) that builds a table in a local unpacked array — e.g. reg [31:0] rk [0:31]; inside a function automatic — produces wrong results; on inspection every element reads back as element 0 (the array does not hold per-index writes across the function body in xsim 2020.2). For a cipher reference model this silently runs every round with rk[0], so the scoreboard flags a (spurious) DUT mismatch. Cause: xsim mis-handles unpacked-array automatic-function locals. Fix: use a packed vector and part-select instead: reg [1023:0] rkv; rkv[32*i +: 32] = val; ... rkv[32*j +: 32]. Packed locals work reliably. (Module-level unpacked reg arrays in RTL are fine — this is specific to automatic-function locals.)

[make] - # inside $(shell …) breaks Makefile parsing

Symptom: make aborts with "unterminated call to function 'if': missing ')'". Cause: GNU make treats # as a comment even inside $(shell …), so a sed program containing # (e.g. sed 's/#.*//' for the skip.list) truncates the line. Fix: escape it — sed 's/\#.*//'. (Already fixed in templates/Makefile; keep it escaped if you hand-edit.)

[xsim] - testcase RS_LV must be 1-bit for the reset-release compare

Symptom: every testcase hangs to the watchdog with no output; the test script is stuck at wait (rst_n_clk === ~RS_LV). Cause: declaring parameter RS_LV = 0 makes it 32-bit, so ~RS_LV = 32'hFFFF_FFFF never === the 1-bit rst_n_clk. Fix: declare parameter RS_LV = 1'b0 in every testcase (as the tb_basic.sv template does). Keep it 1-bit.

[xsim] - cannot part-select a sized literal

Symptom: syntax error near '[' on a line like cpu_write(REG, 128'h..[127:96]). Cause: a part-select/bit-select directly on a sized literal is illegal. Fix: assign the constant to a localparam/reg first, then slice that (localparam [127:0] V = 128'h..; ... V[127:96]).

[Verilog] - always @(*) with a constant-only RHS may never fire

Symptom: an output driven by always @(*) y = {W{1'b0}}; stays X in sim. Cause: the block has an empty sensitivity list (no signals on the RHS), so it is not guaranteed to execute. Fix: drive constants with a continuous assign (make the port a wire), or include a real signal in the block.

What ships with it: 8 files

36.9 KB alongside SKILL.md

templates/

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.