VSV — Visualizing SystemVerilog & Verilog
Real-time block-diagram visualisation for SystemVerilog and Verilog source
files, inside VS Code. Open any .sv / .svh / .svi / .v / .vh file
and a side panel renders the structure as a clean schematic that updates
as you type. VSV understands both halves of a typical SystemVerilog project:
- Design view — RTL synthesis, module instances, generate-for grids,
regfile / memory inference. Three named sub-views: Ports / Schematic /
Logic — see below.
- Verification view — UVM class hierarchy via containment, full UVM
component topology (env → agent → driver/monitor/sequencer), TLM
.connect() wiring, virtual-interface binding, and the DUT module
the testbench drives. (Opt-in via VSV: Show Verification View.)
The two views are entirely separate panels of the same diagram — pick
whichever matches what you're reading.
Features
VSV cycles between three views with VSV: Switch View (or run
VSV: Show Diagram to open the panel):
Ports view. The module as a single labelled rectangle with
every input / output port as a diamond on the boundary.
Schematic view. Sub-module instances laid out left-to-right,
with wires derived from named port connections and assign-alias resolution.
generate-for blocks are expanded into an N-D grid — inner loop left-to-right,
outer loop top-to-bottom — so a 4×4 systolic array shows up as a 16-cell grid.
Logic view. The behavioural body of a single module is
synthesised into primitive shapes:
| Shape |
Meaning |
Notes |
| rounded square + bold symbol |
arithmetic / logic / comparison / shift operator (+, -, *, &, \|, ==, <<, [], {}, …) |
Symbol is the literal operator from the source |
| right-pointing triangle + bubble |
logical or bitwise NOT (!, ~) |
Bubble marks the inversion tip |
| vertical trapezoid |
2:1 ?: MUX or N:1 case MUX |
West side = data inputs; top = sel; legs are labelled with the literal case values (2'b00, default, …) |
| dashed ellipse + literal |
constant / numeric literal (8'h2A, 1'b0, …) |
Drives whatever consumes it |
| rectangle "FF" + signal caption |
edge-triggered flip-flop |
D west, Q east, clk + rst on top; warning-coloured border + dashed stroke when the FF was inferred from an incomplete always_comb |
| rectangle "latch" + signal caption |
level-sensitive latch |
Always warning-coloured (latches are easy to write by accident) |
rectangle + horizontal dividers + [W × D] |
regfile / memory (1-D or N-D unpacked array) |
Write group on west top, read group below, we / clk on top |
rectangle + two-line label instance / (module) |
sub-module instantiation |
Ports labelled with the resolved module's port names |
| rectangle + top title + faint inside wash |
code-structure block (fallback) |
One per always* / assign when the full Logic view would be too dense |
| diamond |
module-level input or output port |
Inputs on the west boundary, outputs on the east |
| small filled circle on a wire junction |
fan-out solder point |
Drawn only where a single signal branches in 3+ directions (IEEE 315 convention) |
if/else chains expand to MUX cascades; incomplete always_comb
branches trigger an inferred-latch warning in the status bar.
When a module has too many primitives for the detailed Logic view
(default threshold: 1500 nodes — picorv32's main core is ~3000),
the Logic view falls back to a code-structure summary: one rectangle
per always* / assign source-code construct, labelled with the literal
source keyword + line number. Click any block to jump to the source.
Wire colours follow schematic convention:
- Yellow — wire flowing from a module input port
- Green — wire flowing into a module output port
- Blue — internal wire
Edge endpoints are snapped to their port anchors, so wires meet the
shape edge cleanly with no visible stub. Edge labels carry the literal
signal name (↑clk, acc, 2'b00, …) and are placed at the
arc-length midpoint of the polyline.
Clock and reset signals always enter their target boxes from the top;
their source ports float to the upper-left corner of the diagram.
Click to navigate
Click any node, edge, or generate-for box in the diagram to jump to the
corresponding line in the source file. The diagram works as a navigable
map of the module.
Verification view (UVM / class hierarchy + topology)
The design view (Ports / Schematic / Logic) deliberately hides non-synthesisable
SystemVerilog — classes, programs, UVM macros, fork/join, etc. — so the
schematic stays focused on the hardware. A separate opt-in panel
renders the verification view with two sub-modes (a toolbar
dropdown switches between them):
- Inheritance — class hierarchy rendered as containment: the
parent class is the outer rounded rectangle, every derived class
is nested inside it. UVM hierarchies are shallow (3–5 levels at
most), so nesting reads instantly without the arrow-soup of a
traditional UML class diagram. Library bases (
uvm_test,
uvm_env, uvm_driver, …) are the outermost containers, with
workspace classes inside them; deeper user chains
(base_test → smoke_test) nest further. implements (SV2009
interface-class realisation) remains a dashed hollow-triangle
edge — it's the only relationship that crosses inheritance chains.
- Topology (auto-default when a
uvm_test-derived class is
detected) — UVM component containment tree. Each
xxx::type_id::create("inst", this) call inside a build_phase
becomes a nested compound box under its parent. TLM .connect()
calls in connect_phase resolve to green arrows. Drivers /
monitors that hold a virtual <iface_name> vif; field bind to an
interface bus node, and that interface bus binds to the DUT
module instance found in the testbench top — so the full path
from a UVM transaction down to the design's pins is on one canvas.
Classes are coloured by UVM role family (test / env / agent / driver /
monitor / sequencer / scoreboard / subscriber / sequence /
sequence_item / object / reg / component), with the role written as a
UML stereotype subtitle (<<uvm_driver>>, <<uvm_env>>, …). Hovering a
node shows the type, family, parameter overrides, every
`uvm_*_utils macro the body declared, and the file:line.
Clicking jumps to the class … declaration.
A Legend button on the toolbar opens a popover that explains
what every arrow style and family colour means.
Open the panel with VSV: Show Verification View (Ctrl/Cmd-Shift-P).
Open a file with classes but no module and the design view will offer
a one-click switch.
Trying it on a real UVM testbench
Three self-contained example testbenches ship under test/fixtures/:
| Fixture |
What it shows |
alu_uvm_tb.sv |
Verifies the existing alu.sv (8-bit ALU). Full UVM stack + alu_if interface + tb_top instantiating the DUT. |
uvm_minimal.sv |
Self-contained counter testbench — simple_if + simple_counter DUT + minimal_tb_top. Smallest end-to-end example. |
uvm_two_agents.sv |
Multi-agent / multi-interface example — apb_if + axi_if bound to a single dual_bus_dut. Also exercises a 2-level test inheritance (smoke_test extends base_test). |
Open any of them and run VSV: Show Verification View to see:
- The full UVM component tree (test → env → agent → driver / monitor /
sequencer + scoreboard)
- TLM
.connect() edges (driver ↔ sequencer, monitor.ap → scoreboard.imp)
- The interface bus(es) bridging the agents to the DUT
- The DUT module rendered as a hardware-style box with its actual
ports listed (inputs on the left, outputs on the right)
Multi-module files
Files that declare two or more modules expose a Module dropdown in
the diagram toolbar. The selection is sticky across re-renders, so a
save / edit doesn't snap the view back to the first module.
Live updates
The diagram re-renders 300 ms after every keystroke. External changes
(git checkout, save from another editor) refresh the active diagram too.
Layout results are cached per file content + module + stage, so
re-opening a previously-rendered file is near-instant.
Layout engine
Layout is done by ELKjs with a custom
post-processor that pins clk/rst to the upper-left column, rebuilds
edge bends as 90° orthogonal routes, and resolves FF feedback cycles
so MUXes feeding FF.D always sit to the left of the flip-flop.
Commands
| Command |
Default behaviour |
VSV: Show Diagram |
Opens / reveals the diagram panel for the active file |
VSV: Show Verification View |
Opens the UVM / class hierarchy view |
VSV: Switch View |
Cycles Ports → Schematic → Logic → Ports (design view only) |
VSV: Hide Diagram |
Closes the panel and stops background re-render |
VSV: LVS Check |
Compare every declared port against what the diagram drew (writes a per-port report to the "VSV LVS" output channel) |
VSV: Reset Macro Overrides |
Clear all manual \define` overrides set via the toolbar |
Configuration
| Setting |
Description |
vsv.includePaths |
Additional search directories for `include lookups (absolute or workspace-relative). The directory of the file containing the `include is always tried first. |
Supported SystemVerilog / Verilog subset
- Module declarations with
#(parameter …) blocks (Verilog-2001 ANSI and SV)
- ANSI port lists, packed and unpacked array dimensions
assign, always_ff, always_comb, always_latch, always @(posedge …),
always @* and bare always @(*)
if / else, case with default, unique / priority qualifiers
- Sub-module instantiations with named (
.port(expr)) port connections
generate-for / generate-if blocks (1-D and N-D nested), with
compile-time parameter folding ($clog2, $bits, $pow, etc.)
- Unsized fill literals (
'0, '1, 'x, 'z), sized base literals
(8'b…, 16'h…, 32'h ABCD_EF01 with intra-literal whitespace)
- Verilog-2001 indexed part-selects
[base +: width] / [base -: width]
wire foo = expr; declaration-with-initializer (treated as assign)
(* attr_name *) synthesis attribute pragmas (silently ignored)
`include / `define / `ifdef / `ifndef directives,
including nested-paren macro arguments
- Sim-only constructs (
initial / final, $display / $readmemh /
assert / etc., fork/join, function/task, class, package /
import) are silently skipped so synthesisable code still renders
Memory / regfile inference
reg [W-1:0] mem [D-1:0]; declarations (1-D or N-D) accessed with at
least one dynamic index collapse into a single regfile node:
- One
(waddrN, wdataN, weN, clk) port group per write — clk is taken
from the enclosing always_ff sensitivity list and weN is the AND
of all enclosing if (cond) chain conditions.
- One
(raddrN, rdataN) port group per distinct read address.
- Multi-dimensional addresses (
mem[way][idx]) get one sub-port per
dimension (waddr0_d0, waddr0_d1, …).
Reads with fully-constant indices fall through to the per-slot driver
path so generate-for replicated arrays continue to behave as N
distinct cells.
Known limitations
Design view (Ports / Schematic / Logic)
- The Logic view falls back to the Schematic view when a module is purely
structural (no behavioural body to synthesise).
- Direction of sub-module ports is inferred from the port name
(
*_in / *_out / *_i / *_o patterns) and from undriven-wire
context. Expressions passed to a port whose direction can't be
determined heuristically may render with the wrong arrow direction.
always_comb-inferred latches are detected but not yet highlighted
inline in the source editor.
- Very large modules (>1500 primitive nodes) render as a coarse
code-structure summary; the per-gate Logic view is not available for them.
Verification view (UVM)
The verification view is static-analysis only — it reads the source
as written, not as it would execute under a UVM simulator. Things it
deliberately doesn't reflect:
- Factory overrides (
set_type_override_by_type,
set_inst_override_by_name) are NOT applied. A class registered
via `uvm_component_utils is rendered with its declared type;
runtime type swaps don't change the diagram.
- config_db lookups are recognised only as the declared
virtual <iface_name> vif; field on a driver / monitor.
Arbitrary uvm_config_db#(T)::get(...) paths and non-interface
context items are not traced.
- TLM resolution walks dot-paths against the instance tree
(
agent.mon.ap → sb.imp). The last segment is assumed to be a
TLM port; resolution stops at the deepest matched instance.
Expressions involving array indexing or arbitrary method calls
aren't resolved into a graph edge.
- DUT detection requires the testbench to instantiate the
interface inside a Verilog module (the
tb_top pattern with a
<iface_name> vif(...) instance). When no such module is in the
workspace, the interface renders as a ghost with no pin binding.
- Sequences and phases —
start_item / finish_item /
phase.raise_objection are not visualised. Sequence relationships
are captured purely through class inheritance.
- UVM register model —
uvm_reg / uvm_reg_block /
uvm_reg_field are tagged with the reg family and rendered as
ordinary classes; field-to-register containment is not auto-traced.
- Parameterized class types —
class my_drv #(type T) extends uvm_driver #(T) renders with the parameter list as an annotation
next to the class name but the type parameter isn't tracked across
type bindings.
- Mixed workspaces — when several unrelated testbenches are
open at once, every leaf-test renders by default. Use the
toolbar's
Test: dropdown to focus on one when the canvas gets
busy.
- SV2009
interface class declarations are parsed and any
implements clause appears as a dashed edge, but interface-class
inheritance is not yet collapsed into containment the way regular
class extension is.
Release notes
See CHANGELOG.
| |