varch
무료Decompose an IP requirement into submodules, define interfaces, and generate an architecture document plus per-module requirement files and a top-level skeleton
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
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
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
설치 방법 보기설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.
This skill builds the IP-level test environment in two phases:
<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.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.
| 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.
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.
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.mkGenerate 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).
tb/<ip>/clock.vhExpand 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).
tb/<ip>/dut.vhExpand 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.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.
tb/<ip>/sti_<iface>.sv per data interfaceFor each data-plane input group from Step A1, expand templates/sti_template.sv:
sti_<iface>.sv, module sti_<iface>.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.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.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.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.cpu_read_check them.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.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.
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:
sco_<iface>.sv, module sco_<iface>.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.in_valid/in_ready/in_data) — it must
never drive the DUT. Wire the tap in dut.vh's {{SCO_INSTANCES}}.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.[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).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.
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.
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:
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";`include "dut.vh";initial that prints [FINISH] FAIL on timeout;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.
cpu_read_check against CPU-readable
registers. On mismatch it prints [ERROR] time= ... MISMATCH ... then
[FINISH] FAIL and $finish automatically.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.[ERROR] time= ...
line with the relevant signal values, then [FINISH] FAIL and $finish.
vtestrun harvests [ERROR] lines into the issue report.[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.
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.
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).
dut.vh sequences. No direct poking of DUT or sti internals from tb_*.sv.[FINISH] PASS / [FINISH] FAIL then $finish; cpu_read_check
auto-fails on mismatch; no token ⇒ FAIL.sti_*.sv, sco_*.sv, and dut.vh sequences — never hardcode the data width.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).sco_*.sv taps a DUT output and never drives it; it is configured/read over the CPU bus like any other slave.dut.vh, not in testcases.tb/; RTL stays Verilog-2005.function automatic is not retained per-indexSymptom: 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.)
# inside $(shell …) breaks Makefile parsingSymptom: 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.)
RS_LV must be 1-bit for the reset-release compareSymptom: 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.
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]).
always @(*) with a constant-only RHS may never fireSymptom: 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.
아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.
로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.
Decompose an IP requirement into submodules, define interfaces, and generate an architecture document plus per-module requirement files and a top-level skeleton
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Trace failing signals through the RTL hierarchy, classify root cause, and write a debug report per issue — no fix is proposed
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generate a design proposal, documentation, and RTL backbone from a requirement file
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Aggregate all per-module documentation into a single comprehensive IP document
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Generate documentation for any RTL module from Verilog source files
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.
Implement Verilog code from an approved design proposal, then lint and simulate
한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.