SystemVerilog Foundations
Verification guidelines, data types, arrays, and procedural flow — the grammar of the language.
What verification actually means
Verification is the art of proving that a design does what its specification says it does. The specification is a human-language document — ambiguous, incomplete, sometimes self-contradictory — and the RTL is one engineer's reading of it. Your job is to offer the second reading. Where the two interpretations disagree, there is a bug.
The goal is to find the bug before the customer does.
The verification plan
Verification begins with a plan derived from the hardware spec. The plan catalogs what features must be exercised and by which techniques — directed tests, constrained-random stimulus, assertions, formal, or emulation. A good plan names corners the designer likely missed.
Directed vs constrained-random
A directed test exercises a specific feature with hand-written stimulus. It finds the bugs you thought of. Constrained-random testing (CRT) uses a pseudo-random number generator with rand/randc and constraints to explore the stimulus space you didn't think of.
The anatomy of a serious testbench
A serious testbench is not a flat file of initial blocks. It is organized in layers, each with a single responsibility:
- Signal layer — the physical wires of the DUT.
- Command layer — drivers that turn transactions into signal wiggles; monitors that turn signal wiggles back into transactions.
- Functional layer — generators, agents, self-checkers, scoreboards.
- Scenario layer — sequences that orchestrate interesting traffic patterns.
- Test layer — the top-level test and its coverage goals.
Each layer communicates with the next through transactions, not signals. This is the architecture the remainder of this reference is teaching you to build.
The logic type and beyond
SystemVerilog introduced logic as an improvement on Verilog's reg. It's a 4-state type (0, 1, X, Z) usable anywhere a net is, but with one critical restriction: a single driver. This restriction catches netlist bugs at elaboration time, before you waste simulation cycles.
logic is almost always the right choice. Reach for wire only when you genuinely have multiple drivers (e.g., a tri-state bus).Integer types
SystemVerilog gives you precise, bounded integer types in addition to Verilog's integer:
| Type | Width | States |
|---|---|---|
bit | 1 (configurable) | 2-state (0/1) |
byte | 8 | 2-state |
shortint | 16 | 2-state |
int | 32 | 2-state |
longint | 64 | 2-state |
logic | configurable | 4-state |
2-state types simulate faster and are what you want for testbench counters, indices, and variables that should never see X or Z. 4-state types are what you want for DUT signals where X-propagation is a legitimate signal.
Packed, unpacked, and the collection types
SystemVerilog distinguishes packed from unpacked dimensions by where you put them relative to the variable name.
int lo_hi [0:15]; // Verilog-style bounds int array [16]; // SystemVerilog C-style size int grid [8][4]; // 2-D, 8 rows × 4 cols foreach (grid[i,j]) grid[i][j] = i*4 + j;
Dimensions to the left of the name are packed: contiguous bits, sliceable across element boundaries, ideal for modeling hardware registers. Dimensions to the right are unpacked: separate storage per element, ideal for collections of records.
Dynamic collections
Three variable-length structures, each suited to a different shape of data:
| Type | When to reach for it | Declaration |
|---|---|---|
| Dynamic array | Runtime-sized, indexed by integer. Packet payloads. | int d[]; |
| Queue | Deque with push/pop at both ends. Stimulus buffers, scoreboards. | int q[$]; |
| Associative | Sparse, any key type — 64-bit addresses, strings, enums. | int mem[bit[63:0]]; |
Tasks, functions, and routine arguments
The distinction between a task and a function is the difference between a subroutine that may wait and one that may not. A function executes in zero simulation time; it may not contain # delays, @ event controls, wait statements, or calls to tasks. A task may contain all of them.
A void function is a function declared to return nothing. Use one when you want the zero-time discipline of a function but have no useful value to return — void function monitor_drive();.
Argument directions
Routine arguments may be declared input, output, inout, or ref. The first three copy values at call and return. ref passes by reference: the routine sees live updates to the caller's variable.
ref to avoid copy cost.task transfer( input bit [31:0] addr, // copied in output bit [31:0] data, // copied out ref mailbox mbx, // live reference input int retries = 3 // default value ); // … endtask
Type-safe stimulus
An enum lets you name a set of values — states, commands, modes — and gives you type safety at compilation.
typedef enum bit[1:0] { RESP_OKAY = 2'b00, RESP_EXOKAY = 2'b01, RESP_SLVERR = 2'b10, RESP_DECERR = 2'b11 } axi_resp_e; axi_resp_e resp; resp = RESP_OKAY; // type-safe assignment if (resp == RESP_SLVERR) // …
A struct groups related fields into a record. Use typedef struct packed for bit-level packing (registers, protocols) and plain typedef struct for testbench-side records.