Install
$ agentstack add skill-phamcuong21478-rtl-skills-vtestgen ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Verilog Testcase Generator
Overview
This skill builds the IP-level test environment in two phases:
- Phase A — Scaffold the bench project. Parse
_top, then generate a
reusable, Makefile-driven simulation project under tb//: 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//Makefile top dispatcher (auto-discovers tb_*.sv)
tb//script/library.mk per-testcase xvlog/xelab/xsim recipe
tb//script/rtl.f generated RTL hierarchy filelist
tb//script/skip.list names of skipped testcases, excluded by the Makefile
tb//clock.vh parameterized clock + sync-reset generators
tb//dut.vh reg-map, DUT signals/instance, sti instances, CPU BFM tasks + sequences
tb//sti_.sv one CPU-controllable stimulus driver per data interface
tb//sco_.sv one CPU-controllable scoreboard per checkable stream output (optional)
tb//tb_.sv self-checking testcases (CPU-bus only)
tb//tc_list.md testcase index with status
tb//sim// 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/.md— full IP behavior, architecture, interface protocols, register map../rtl/_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_ | | 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// and tb//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//Makefiletemplates/library.mk→tb//script/library.mk
Generate tb//script/rtl.f from templates/rtl.f: walk the instantiation hierarchy of _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//clock.vh
Expand templates/clock.vh: one always toggle per detected clock (period T_), one synchronous reset per clock domain using RS_LV (assert = RS_LV, deassert after 10*T_). 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//dut.vh
Expand templates/dut.vh:
{{REGISTER_MAP}}—localparam REG_ = 20'h..;for every CPU register.
Take real addresses from doc/.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 persti_*.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 persco_*.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//sti_.sv per data interface
For each data-plane input group from Step A1, expand templates/sti_template.sv:
- Name it
sti_.sv, modulesti_. - 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 BODYships 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
readyon 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//sco_.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_.sv, modulesco_. - 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 BODYto 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, deriveNW/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:
| 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//tb__.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_LV
default parsed from _top,
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: phamcuong21478
- Source: phamcuong21478/rtl-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.