Vtestgen
Claude Code skills & agents that automate a full RTL/Verilog design flow - architecture, RTL coding, lint, simulation, self-checking testbenches, regression, debug, Vivado synthesis, and docs - driven from a one-page IP spec.
npx -y skills add phamcuong21478/rtl-skills --skill vtestgenAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 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.
What its author says it does
Copied from the file, not written here
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 list
SKILL.md
25.1 KB, 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 undertb/<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_*.svthat drives the bench only through the CPU bus, validate each withmake, and writetc_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
| Tool | Purpose | Check |
|---|---|---|
| xvlog | compile / lint | xvlog --version |
| xelab | elaborate | xelab --version |
| xsim | simulate | xsim --version |
| make | build orchestration | make --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:
./doc/<ip>.md— full IP behavior, architecture, interface protocols, register map../rtl/<ip>_top.v— exact top-level port list, signal names, and widths.
Parse the top module header and classify every port:
| Class | How to detect | Bench treatment |
|---|---|---|
| clock | name matches clk* / *_clk | generate in clock.vh, period param T_<NAME> |
| reset | name matches rst* / *_rst* / *_n | generate sync reset in clock.vh |
| CPU bus | *cpu*, or *_we/*_oe/*_addr/*_din/*_dout, or an APB/AXI-lite-ish group | wire to cpu_* and the BFM tasks; build the register map |
| data-plane in | remaining inputs (valid/data/sof/eof/...) grouped by prefix | driven by an sti_*.sv module |
| data-plane out / status | remaining outputs | status 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/Makefile→tb/<ip>/Makefiletemplates/library.mk→tb/<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 fromdoc/<ip>.mdor 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 asreg/handled by tasks, DUT outputs aswire, data-plane inputs aswire(driven by sti modules).{{DUT_PARAM_MAP}}/{{DUT_PORT_MAP}}—.NAME(value)lines; connect the CPU port group tocpu_*, data-plane ports to the matchingsti_*outputs, clocks/ resets to the generated names. The DUT read-data output connects to adut_cpu_doutwire so it can be muxed.{{STI_INSTANCES}}— one instance persti_*.sv(Step A5), each given a uniqueSTI_ADDRon a0x100-word window (STI_BASE,STI_BASE+'h100, ...), sharing the CPU bus. BindRS_LV(andDATA_W) to the DUT's values so the driver resets at the DUT's polarity — a driver left at the defaultRS_LV=0while the DUT isRS_LV=1is held in reset for the whole run.{{SCO_INSTANCES}}— one instance persco_*.sv(Step A6), each given a uniqueSCO_ADDRon a0x100-word window (SCO_BASE,SCO_BASE+'h100, ...), sharing the CPU bus and tapping a DUT output. BindRS_LV(andDATA_W) to the DUT's values, same as the sti drivers. Leave empty (and dropSCO_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/32words — never reach the next slave's base (good forDATA_Wup to 8192). Keep every slave base0x100-aligned and any word-addressed payload within its window. {{STI_DOUT_OR}}/{{SCO_DOUT_OR}}— OR of each sti / sco instance's*_doutfor the read mux. Substitute32'h0for 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, modulesti_<iface>. - Keep the CPU-slave wrapper (write-decode of
ENA/RATE/MODE/SEED/CLR, read-decode intocpu_dout, address self-decode fromSTI_ADDR) unchanged — this is what makes the driver controllable from the test script viacpu_writeonly. - The
GENERATOR BODYships MODE-selectable (chosen at run time via theMODEregister, no recompile):MODE_LFSR(pseudo-random, replicated to fillDATA_W),MODE_COUNT(byte-stream counter-up), andMODE_FILE(replayDATA_W-wide words fromPATTERN_FILEvia$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 byMODE_FILE, not hardcoded in the body. - Runtime sync with the mirroring
sco_*. The driver and scoreboard are two independent generators; theCLRregister arms each one back to the start of its sequence (reseed fromSEED, counters to 0) without a full DUT reset. The testcase syncs them by: write the sameMODE/SEEDto both → pulseCLRon 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-modelsco_*(Step A6). Put the arm-and-enable handshake in adut.vhsequence so every testcase reuses it. - Wire the handshake
readyon both sides. The driver consumes the DUT inputreadyvia itsout_readyport — it holds each beat until accepted and advances only then, so it stays in lock-step with the sco under back-pressure; tieout_readyto1'b1indut.vhif the input has noready. The DUT outputreadyis only tapped by the sco — something else must drive it: tie it high for a plain run, or add a small ready/throttle driver (ansti_*-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_checkthem. - Make the driver width-generic — never hardcode the data width. Parameterise
by the DUT's data width (
DW, plusUW/EWsidebands), derive everything from it (localparam NW = (DW+31)/32;,NB = DW/8;). Every payload/capture register andout_datamust be[DW-1:0]; the shipped modes already size offDW. A driver hardcoded to one width silently works at the defaultDWthen 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 (PTwordiatSTI_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 waysco_template.svword-addresses its capture payload. Such a payload occupiesNWCPU 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, modulesco_<iface>. - Keep the CPU-slave wrapper unchanged (write-decode of
ENA/MODE/SEED/CLR, read-decode ofERRCNT/CHKCNT+ word-addressed first-mismatch capture, address self-decode fromSCO_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 indut.vh's{{SCO_INSTANCES}}. - Replace only the
COMPARE BODYto match the interface handshake. Default: the scoreboard regenerates the expected stream itself fromMODE/SEED/PATTERN_FILE— the sameMODE_LFSR/MODE_COUNT/MODE_FILEsources assti_*.sv— so no expected payload is pushed over the CPU bus. The testcase just sets the scoreboard'sMODE/SEEDto 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-addressedEXPregister written by the testcase. - On a mismatch the scoreboard prints
[ERROR] ...then[FINISH] FAIL+$finish(self-fail, consistent withcpu_read_check).ERRCNTis still exposed so a testcase may instead defer the verdict withcpu_read_check(SCO_ERRCNT, 32'd0). - Width-generic like the sti drivers — parameterise by
DATA_W, deriveNW/NBfrom 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:
| Category | Coverage target |
|---|---|
| Reset | DUT reaches defined idle state after reset; all outputs at documented reset values |
| Basic | One test per documented feature or operating mode |
| Edge | Boundary data values (0, all-ones, max-minus-1), empty/full conditions, single-cycle bursts |
| Back-pressure | Valid/ready handshakes held off; pipeline stalls and drains correctly under sustained back-pressure |
| Error inject | Out-of-spec stimulus, concurrent events, overflow/underflow conditions |
| Stress | Back-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 ownRS_LVdefault 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
initialthat prints[FINISH] FAILon timeout; - have a single test-script
initialblock that uses only the reusable sequences fromdut.vhpluscpu_write/cpu_read/cpu_read_checkto: release reset, configure the DUT, configure+start thesti_*drivers, let traffic run, check status/captured output viacpu_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_checkagainst CPU-readable registers. On mismatch it prints[ERROR] time= ... MISMATCH ...then[FINISH] FAILand$finishautomatically. - Stream outputs are checked by their
sco_*scoreboard: the test script sets the scoreboard'sMODE/SEEDto match the driver feeding the DUT and enables it withcpu_write; the scoreboard regenerates the expected stream and self-fails on the first mismatched beat. For a deferred verdict, end the run and assertcpu_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] FAILand$finish.vtestrunharvests[ERROR]lines into the issue report. - A passing testcase ends with
[FINISH] PASSthen$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 asimulate.logalready exists for that testcase. After editing, clear the stale result first —make cleanf(drops non-PASS sim dirs) orrm -rf sim/tb_<name>— then re-run.
| Result | Action |
|---|---|
| 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] token | testcase ended without printing the verdict — fix the result block; clear + rerun |
[FINISH] FAIL | the 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.vhsequences. No direct poking of DUT or sti internals fromtb_*.sv. - Verdict: print
[FINISH] PASS/[FINISH] FAILthen$finish;cpu_read_checkauto-fails on mismatch; no token ⇒ FAIL. - Width-generic
sti_*.sv,sco_*.sv, anddut.vhsequences — never hardcode the data width. - Reset-polarity-generic:
sti_*.sv/sco_*.svreset against theirRS_LVparameter, never a hardcoded!rst_n;dut.vhbindsRS_LVon every sti/sco instance to the DUT's value (the bench reset inclock.vhis alreadyRS_LV-driven). - Scoreboards are CPU-slave observers: a
sco_*.svtaps 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.