A complete quick-reference for SystemVerilog. Covers every major language construct beyond Verilog — data types, always blocks, interfaces, classes, assertions, coverage, randomization, packages, and more. Designed for engineers who know Verilog and want to leverage the full power of SystemVerilog for both design and verification.


What SystemVerilog Adds to Verilog

SystemVerilog (IEEE 1800) is a superset of Verilog (IEEE 1364). Everything valid in Verilog is valid in SystemVerilog. This cheat sheet focuses on what SystemVerilog adds: improved data types, explicit always blocks, interfaces, packages, OOP for verification, constrained randomization, assertions, and functional coverage.


Data Types

Two-State vs Four-State

TypeWidthValuesSigned?Use
logicUser-defined0, 1, x, zUnsignedReplaces both wire and reg
regUser-defined0, 1, x, zUnsignedLegacy — use logic instead
integer320, 1, x, zSignedLegacy — use int instead
bitUser-defined0, 1UnsignedTwo-state for testbench (faster simulation)
byte80, 1SignedSmall signed integers
shortint160, 1Signed16-bit signed
int320, 1SignedLoop variables, counters
longint640, 1Signed64-bit signed
real64float—Floating-point (not synthesizable)
shortreal32float—32-bit float (not synthesizable)
stringDynamicchars—Text manipulation (not synthesizable)

logic is the single most important SystemVerilog improvement. It replaces both wire and reg — you no longer need to think about which to use. Use logic everywhere except for signals with multiple drivers (which still need wire).


logic [7:0]  data;       // replaces both wire and reg
logic        clk, rst_n;
bit   [31:0] tb_counter; // two-state — faster in simulation

byte   b = 8'hFF;        // 8-bit signed
int    i = -42;          // 32-bit signed
longint l;               // 64-bit signed
string s = "hello";      // dynamic string

Enumerations


// Basic enum (default type: int)
typedef enum {IDLE, RUN, DONE, ERROR} state_t;
state_t state, next_state;

// Explicit encoding
typedef enum logic [2:0] {
  S_IDLE  = 3'b001,
  S_RUN   = 3'b010,
  S_DONE  = 3'b100
} state_e;

// Enum methods
state.name();        // returns string: "IDLE"
state.next();        // next value in enumeration
state.prev();        // previous value
state.num();         // number of values in enum
state.first();       // first value
state.last();        // last value

For synthesizable FSMs, always specify the base type explicitly (e.g., enum logic [2:0]). This controls the encoding width and avoids synthesis warnings.

Structs & Unions


// Packed struct — maps to a contiguous bit vector (synthesizable)
typedef struct packed {
  logic [3:0]  opcode;
  logic [11:0] address;
  logic [7:0]  data;
} instruction_t;                    // total: 24 bits

instruction_t instr;
assign instr.opcode = 4'hA;
assign instr.address = 12'h100;
logic [23:0] raw = instr;           // can cast to plain vector

// Unpacked struct — fields may have gaps (for testbench modeling)
typedef struct {
  int    id;
  string name;
  real   score;
} student_t;

// Packed union — all members share same bits
typedef union packed {
  logic [31:0]          word;
  logic [3:0][7:0]      bytes;     // 4 bytes
  struct packed {
    logic [15:0] upper;
    logic [15:0] lower;
  } halves;
} word_u;

word_u w;
w.word = 32'hDEAD_BEEF;
// w.bytes[3] == 8'hDE, w.halves.lower == 16'hBEEF

Use packed structs/unions for synthesizable RTL. Unpacked structs are for verification only — they cannot map to a bit vector.

typedef


typedef logic [7:0]  byte_t;
typedef logic [31:0] word_t;
typedef logic [15:0] addr_t;

// Parameterized types (via macros or packages)
typedef struct packed {
  addr_t addr;
  word_t data;
  logic  valid;
} bus_req_t;

Arrays — Fixed, Dynamic, Associative, Queues


// Packed array — contiguous bits (synthesizable)
logic [3:0][7:0] packed_arr;       // 32 bits: 4 bytes packed together

// Unpacked fixed-size array (synthesizable)
logic [7:0] mem [0:255];           // 256 entries of 8 bits
logic [7:0] rom [256];             // same thing — shorthand

// Multidimensional
logic [7:0] matrix [4][4];

// Dynamic array (not synthesizable — verification)
int dyn[];
initial begin
  dyn = new[10];                   // allocate 10 elements
  dyn = new[20](dyn);              // resize, preserve contents
  dyn.delete();                    // free
end

// Associative array (not synthesizable — verification)
int lookup [string];               // string-indexed
int sparse [int];                  // integer-indexed
initial begin
  lookup["alpha"] = 1;
  lookup["beta"]  = 2;
  if (lookup.exists("alpha")) $display("found");
  lookup.delete("beta");
end

// Queue (not synthesizable — verification)
int q [$];                         // unbounded queue
int bq [$:255];                    // bounded queue (max 256 entries)
initial begin
  int val, sz;
  q.push_back(10);                 // append
  q.push_front(5);                 // prepend
  val = q.pop_front();             // remove & return first
  q.insert(1, 7);                  // insert at index 1
  q.delete(0);                     // delete index 0
  sz = q.size();                   // current size
end

Array Manipulation Methods


int arr[] = '{3, 1, 4, 1, 5, 9};

// Locator methods — return queues
int q[$];
q = arr.find with (item > 3);          // {4, 5, 9}
q = arr.find_index with (item == 1);   // {1, 3}
q = arr.find_first with (item > 4);    // {5}
q = arr.find_last with (item > 4);     // {9}
q = arr.min();                         // {1}
q = arr.max();                         // {9}
q = arr.unique();                      // {3, 1, 4, 5, 9}

// Reduction methods
int s = arr.sum();                     // 23
int p = arr.product();                 // 540

// Ordering
arr.sort();                            // ascending
arr.rsort();                           // descending
arr.reverse();                         // reverse order
arr.shuffle();                         // randomize order

Strings


string s = "Hello";
int len  = s.len();              // 5
s = {s, " World"};              // concatenation: "Hello World"
string up = s.toupper();        // "HELLO WORLD"
string lo = s.tolower();        // "hello world"
byte c = s.getc(0);             // 'H' (ASCII 72)
s.putc(0, "h");                 // "hello World"
string sub = s.substr(0, 4);   // "hello"
int idx = s.atoi();             // string to integer
s.itoa(42);                     // integer to string

Casting & Type Conversions


// Static cast — compile-time check
int i = int'(3.14);                       // real → int: 3
logic [7:0] b = 8'(some_wider_signal);    // truncate
logic signed [7:0] sb = signed'(b);       // unsigned → signed

// Cast between enum and int
state_e s = state_e'(3);                  // int → enum

// $cast — dynamic cast with runtime check (returns 1 on success)
if (!$cast(child_handle, base_handle))
  $error("Cast failed");

// Type query
$typename(variable)      // returns type name as string (for debug)
$bits(type_or_expr)      // number of bits

Packages


// Definition
package bus_pkg;
  typedef enum logic [1:0] {READ, WRITE, IDLE} cmd_e;
  typedef struct packed {
    cmd_e        cmd;
    logic [15:0] addr;
    logic [31:0] data;
  } bus_req_t;

  parameter int BUS_WIDTH = 32;

  function automatic logic [3:0] encode(input logic [15:0] val);
    return val[3:0] ^ val[7:4];
  endfunction
endpackage

// Usage — wildcard import (most common)
import bus_pkg::*;

// Usage — explicit import
import bus_pkg::bus_req_t;
import bus_pkg::BUS_WIDTH;

// Usage — fully qualified (no import needed)
bus_pkg::cmd_e my_cmd;

Use packages for shared types, parameters, and utility functions. Wildcard import (::*) is convenient but explicit imports are safer in large projects to avoid name collisions.


Module Declaration — SystemVerilog Style


module my_module
  import bus_pkg::*;                   // package import before ports
#(
  parameter int WIDTH = 8,
  parameter int DEPTH = 16,
  parameter type DATA_T = logic [7:0]  // parameterized type
)(
  input  logic               clk,
  input  logic               rst_n,
  input  DATA_T              data_in,
  output DATA_T              data_out,
  output logic               valid
);
  localparam int ADDR_W = $clog2(DEPTH);
  // ...
endmodule

Implicit Port Connections


// .* — connect all ports by matching name (SystemVerilog)
my_module u_mod (
  .clk,                    // .name is shorthand for .name(name)
  .rst_n,
  .data_in  (fifo_data),   // explicit when names differ
  .data_out,
  .valid
);

// Or even shorter with .*
my_module u_mod (
  .*,                      // connect all matching names automatically
  .data_in  (fifo_data)   // override specific ports
);

.* is powerful but dangerous — it silently connects signals you may not intend. Use with caution in large designs. Explicit connections are safer for RTL; .* is fine in testbenches.


Always Blocks — Explicit Intent

BlockPurposeAssignmentSensitivity
always_combCombinational logic= (blocking)Automatic (all reads)
always_ffSequential logic (flip-flops)<= (non-blocking)Explicit edge list
always_latchIntentional latches= (blocking)Automatic

// Combinational — replaces always @(*)
always_comb begin
  y = a & b;                  // tool warns if latch is inferred
  case (sel)
    2'b00:   out = in_a;
    2'b01:   out = in_b;
    default: out = '0;
  endcase
end

// Sequential — synchronous reset
always_ff @(posedge clk) begin
  if (!rst_n)  q <= '0;
  else         q <= d;
end

// Sequential — asynchronous reset
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)  q <= '0;
  else         q <= d;
end

// Intentional latch (rare — makes intent clear)
always_latch begin
  if (enable) q = d;
  // no else — intentionally latching
end

Always use always_comb, always_ff, and always_latch instead of the old always. Tools check the intent of each block: they should warn when an always_comb infers a latch or when an always_ff does not describe sequential logic. This catches bugs at compile time.


unique, priority, and Enhanced case/if


// unique — asserts all cases are mutually exclusive (no overlap)
// and that one case always matches (full)
always_comb begin
  unique case (sel)
    2'b00: y = a;
    2'b01: y = b;
    2'b10: y = c;
    2'b11: y = d;
  endcase
end

// priority — asserts at least one case matches (full)
// but allows overlap (first match wins)
always_comb begin
  priority case (1'b1)
    req[3]: grant = 4'b1000;
    req[2]: grant = 4'b0100;
    req[1]: grant = 4'b0010;
    req[0]: grant = 4'b0001;
    default: grant = 4'b0000;
  endcase
end

// unique if / priority if
always_comb begin
  unique if (sel == 2'b00)   y = a;
  else if   (sel == 2'b01)   y = b;
  else if   (sel == 2'b10)   y = c;
  else                       y = d;
end

always_comb begin
  priority if (req[3])  grant = 4'b1000;
  else if     (req[2])  grant = 4'b0100;
  else if     (req[1])  grant = 4'b0010;
  else if     (req[0])  grant = 4'b0001;
  else                  grant = 4'b0000;
end
KeywordFull?Parallel?Simulation Check
uniqueYesYesWarns if no match or multiple matches
unique0NoYesWarns only on multiple matches
priorityYesNoWarns if no match
(none)NoNoNo runtime checks

Use unique case for one-hot muxes and decoders. Use priority case or priority if for priority encoders. These replace the unreliable Verilog full_case/parallel_case pragmas with proper language semantics and runtime checks.


Interfaces


// Interface definition
interface axi_lite_if #(
  parameter int ADDR_W = 32,
  parameter int DATA_W = 32
)(
  input logic clk,
  input logic rst_n
);
  logic [ADDR_W-1:0] awaddr;
  logic               awvalid;
  logic               awready;
  logic [DATA_W-1:0] wdata;
  logic               wvalid;
  logic               wready;
  logic [1:0]         bresp;
  logic               bvalid;
  logic               bready;

  // Modport — defines port directions from each perspective
  modport master (
    input  clk, rst_n,
    output awaddr, awvalid, wdata, wvalid, bready,
    input  awready, wready, bresp, bvalid
  );

  modport slave (
    input  clk, rst_n,
    input  awaddr, awvalid, wdata, wvalid, bready,
    output awready, wready, bresp, bvalid
  );

  modport monitor (
    input clk, rst_n,
    input awaddr, awvalid, awready,
          wdata, wvalid, wready,
          bresp, bvalid, bready
  );
endinterface

// Module using interface
module axi_slave (
  axi_lite_if.slave bus        // interface with modport
);
  always_ff @(posedge bus.clk or negedge bus.rst_n) begin
    if (!bus.rst_n) begin
      bus.awready <= 1'b0;
    end else begin
      bus.awready <= bus.awvalid & ~bus.awready;
    end
  end
endmodule

// Top-level instantiation
module top;
  logic clk, rst_n;

  axi_lite_if #(.ADDR_W(16)) u_bus (.clk, .rst_n);

  axi_master u_master (.bus(u_bus.master));
  axi_slave  u_slave  (.bus(u_bus.slave));
endmodule

Interfaces bundle related signals into a single port, reducing port list verbosity and connection errors. Use modport to enforce correct signal directions for each connected module.


Clocking Blocks


// Clocking block — defines timing for testbench signal access
interface bus_if (input logic clk);
  logic [7:0] data;
  logic       valid;
  logic       ready;

  clocking driver_cb @(posedge clk);
    default input #1step output #0;    // sample before edge, drive at edge
    output data;
    output valid;
    input  ready;
  endclocking

  clocking monitor_cb @(posedge clk);
    default input #1step;
    input data;
    input valid;
    input ready;
  endclocking

  modport driver  (clocking driver_cb);
  modport monitor (clocking monitor_cb);
endinterface

// Usage in testbench
task automatic drive(virtual bus_if.driver drv);
  drv.driver_cb.data  <= 8'hAB;
  drv.driver_cb.valid <= 1'b1;
  @(drv.driver_cb);                  // wait one clock cycle
  wait (drv.driver_cb.ready == 1);
  drv.driver_cb.valid <= 1'b0;
endtask

Clocking blocks eliminate timing races in testbenches by defining exact sample/drive times relative to the clock edge. Always use them in UVM drivers and monitors.


Tasks & Functions — Enhanced


// Functions in a module are static by default - add "automatic" for per-call storage
function automatic int max(int a, int b);
  return (a > b) ? a : b;          // 'return' statement (new)
endfunction

// void function — no return value
function void print_hex(logic [31:0] val);
  $display("0x%08h", val);
endfunction

// Default argument values
function automatic int clamp(int val, int lo = 0, int hi = 255);
  if (val < lo) return lo;
  if (val > hi) return hi;
  return val;
endfunction
int x = clamp(300);           // uses defaults: lo=0, hi=255 → returns 255
int y = clamp(50, .hi(100));  // named argument: lo=0, hi=100

// ref arguments — pass by reference (like C++ &)
function automatic void swap(ref int a, ref int b);
  int temp = a;
  a = b;
  b = temp;
endfunction

// Tasks - no return value; results go through output, inout or ref arguments
task automatic drive_burst(
  ref logic [7:0] bus,
  input int count,
  input logic [7:0] data[]
);
  for (int i = 0; i < count; i++) begin
    @(posedge clk);
    bus = data[i];                 // a ref argument can't be written with <=
  end
endtask

Function vs Task (SystemVerilog Updates)

FeatureFunctionTask
Timing controls (#, @)NoYes
Return valueSingle value or voidNone - results go through output, inout or ref arguments
Can call tasksNoYes
ref argumentsYesYes
Default argumentsYesYes
SynthesizableYes (combinational)Only if no timing

Processes & Fork/Join


// fork...join — all threads must complete before continuing
fork
  begin : thread_a
    #10 $display("A done");
  end
  begin : thread_b
    #20 $display("B done");
  end
join  // waits for both threads (resumes at t=20)

// fork...join_any — continue when ANY thread completes
fork
  begin #10 $display("fast"); end
  begin #50 $display("slow"); end
join_any  // resumes at t=10 (slow thread still running)

// fork...join_none — launch threads and continue immediately
fork
  begin #10 $display("background"); end
join_none  // continues immediately, thread runs in background

// Controlling threads
disable fork;              // kill all child threads of current process

// wait fork — wait for all forked threads to complete
fork
  task_a();
  task_b();
join_none
// ... do other work ...
wait fork;                 // block until task_a and task_b both finish

disable fork kills ALL child threads, including those from outer scopes. To limit scope, wrap the fork in a named begin...end block and disable that block instead.


Classes (OOP for Verification)


class Transaction;
  // Properties
  rand  bit [7:0]  addr;        // randomizable
  rand  bit [31:0] data;
  randc bit [3:0]  id;          // cyclic random — covers all values before repeating
  bit              is_write;
  static int       count = 0;   // shared across all instances

  // Constraints
  constraint addr_range { addr inside {[8'h00:8'h7F]}; }
  constraint data_align { data[1:0] == 2'b00; }

  // Constructor
  function new(bit is_write = 0);
    this.is_write = is_write;
    count++;
  endfunction

  // Methods
  function void display();
    $display("Txn id=%0d addr=0x%02h data=0x%08h wr=%0b",
             id, addr, data, is_write);
  endfunction

  // Virtual method — can be overridden by subclass
  virtual function bit is_valid();
    return (addr != '0);
  endfunction

  // Copy
  function Transaction copy();
    Transaction t = new();
    t.addr     = this.addr;
    t.data     = this.data;
    t.id       = this.id;
    t.is_write = this.is_write;
    return t;
  endfunction
endclass

// Inheritance
class WriteTransaction extends Transaction;
  rand bit [3:0] byte_enable;

  constraint be_valid { $countones(byte_enable) inside {1, 2, 4}; }

  function new();
    super.new(1);              // call parent constructor
  endfunction

  // Override virtual method
  function bit is_valid();
    return (super.is_valid() && byte_enable != 0);
  endfunction
endclass

// Usage
Transaction t = new();
t.randomize();                 // randomize with constraints
t.display();

WriteTransaction wt = new();
wt.randomize() with { addr < 8'h40; };  // inline constraint

Polymorphism


// Virtual methods enable polymorphism
Transaction t;
WriteTransaction wt = new();
t = wt;                        // parent handle pointing to child object
t.is_valid();                  // calls WriteTransaction::is_valid() (virtual dispatch)

// $cast for downcasting
WriteTransaction wt2;
if ($cast(wt2, t))             // runtime check: is t actually a WriteTransaction?
  wt2.byte_enable = 4'hF;

Parameterized Classes


class Stack #(type T = int, int DEPTH = 32);
  T data[$:DEPTH-1];

  function void push(T val);
    if (data.size() < DEPTH) data.push_back(val);
  endfunction

  function T pop();
    return data.pop_front();
  endfunction

  function int size();
    return data.size();
  endfunction
endclass

Stack #(bit [7:0], 64) byte_stack = new();
Stack #(Transaction)   txn_stack  = new();

Constrained Randomization


class Packet;
  rand  bit [7:0]  length;
  rand  bit [15:0] addr;
  rand  bit [7:0]  payload[];
  randc bit [2:0]  channel;      // cyclic — all values visited before repeat

  // Simple constraints
  constraint len_range   { length inside {[1:64]}; }
  constraint addr_align  { addr[1:0] == 2'b00; }      // 4-byte aligned
  constraint payload_sz  { payload.size() == length; }

  // Weighted distribution
  constraint len_dist {
    length dist {
      [1:8]   := 50,     // equal weight per value in range
      [9:32]  :/ 30,     // weight divided among values in range
      [33:64] := 20
    };
  }

  // Implication
  constraint wr_addr {
    (channel == 0) -> addr inside {[16'h0000:16'h0FFF]};
  }

  // Conditional constraint
  constraint cond_c {
    if (length > 32) addr > 16'h1000;
    else             addr < 16'h1000;
  }

  // Foreach on arrays
  constraint ordered {
    foreach (payload[i])
      if (i > 0) payload[i] >= payload[i-1];
  }

  // Solve order — control randomization order
  constraint order { solve length before addr; }
endclass

// Randomization calls
Packet p = new();
assert(p.randomize());                    // basic randomize
assert(p.randomize() with {              // inline constraints (added on top)
  length < 16;
  addr == 16'h0100;
});

// Constraint control
p.constraint_mode(0);                     // disable ALL constraints
p.len_range.constraint_mode(0);           // disable specific constraint
p.rand_mode(0);                           // disable ALL randomization
p.addr.rand_mode(0);                      // disable specific field randomization

// Pre/post randomize callbacks
class SmartPacket extends Packet;
  function void pre_randomize();
    // called before randomize()
  endfunction

  function void post_randomize();
    // called after randomize() — compute CRC, etc.
  endfunction
endclass

Assertions (SVA)

Immediate Assertions


// Simple immediate assertion — checked procedurally
always_comb begin
  assert (state != ILLEGAL_STATE) else $error("Illegal state reached");
end

// With action blocks
always_ff @(posedge clk) begin
  assert (fifo_count <= DEPTH)
    $display("FIFO OK, count=%0d", fifo_count);   // pass action
  else
    $fatal(1, "FIFO overflow! count=%0d", fifo_count); // fail action
end

// Deferred assertion — checked at end of time step (avoids glitches)
always_comb begin
  assert #0 (onehot(grant)) else $error("Grant not one-hot");
end

Concurrent Assertions


// Property — a reusable temporal expression
property p_req_ack;
  @(posedge clk) disable iff (!rst_n)
  req |-> ##[1:3] ack;              // if req, ack must come within 1-3 cycles
endproperty

assert property (p_req_ack)
  else $error("Req not acknowledged within 3 cycles");

// Sequence — a pattern of events over time
sequence s_handshake;
  req ##1 ack ##1 !req ##1 !ack;    // four-cycle handshake
endsequence

property p_handshake;
  @(posedge clk) disable iff (!rst_n)
  req |-> s_handshake;
endproperty

assert property (p_handshake);

SVA Operators

OperatorMeaningExample
##NDelay N cyclesa ##2 b — a, then b 2 cycles later
##[M:N]Delay rangea ##[1:3] b — b within 1-3 cycles
|-> Overlapping implicationa |-> b — if a, b must hold same cycle
|=>Non-overlapping implicationa |=> b — if a, b must hold next cycle
[*N]Consecutive repetitiona[*3] — a for 3 consecutive cycles
[*M:N]Repetition rangea[*1:3] — a for 1 to 3 cycles
[->N]Goto repetitiona[->3] — 3rd occurrence of a
[=N]Non-consecutive repetitiona[=3] — a occurs 3 times (not necessarily consecutive)
notProperty negationnot (a |-> b)
$rose()Signal rose this cycle$rose(req)
$fell()Signal fell this cycle$fell(ack)
$stable()Signal unchanged$stable(data)
$past()Value in previous cycle$past(data, 2) — value 2 cycles ago
$onehot()Exactly one bit set$onehot(grant)
$onehot0()Zero or one bit set$onehot0(req)
$countones()Count set bits$countones(mask)

Common Assertion Patterns


// Request must be acknowledged
assert property (@(posedge clk) disable iff (!rst_n)
  req |-> ##[1:5] ack
);

// Signal must be stable while valid
assert property (@(posedge clk) disable iff (!rst_n)
  valid |-> $stable(data)
);

// Two signals are mutually exclusive
assert property (@(posedge clk) disable iff (!rst_n)
  not (signal_a && signal_b)
);

// Data must change after write
assert property (@(posedge clk) disable iff (!rst_n)
  (wr_en && addr == TARGET) |=> (rdata == $past(wdata))
);

// No back-to-back requests
assert property (@(posedge clk) disable iff (!rst_n)
  req |=> !req
);

// Cover — check that a scenario CAN occur (not that it must)
cover property (@(posedge clk) req ##1 ack);

// Assume — constrain inputs in formal verification
assume property (@(posedge clk) $onehot0(req));

Functional Coverage


// Covergroup — tracks value coverage during simulation
covergroup cg_bus @(posedge clk);
  option.per_instance = 1;

  // Coverpoint — which values were seen
  cp_addr: coverpoint addr {
    bins low    = {[0:63]};
    bins mid    = {[64:191]};
    bins high   = {[192:255]};
    bins zero   = {0};
    illegal_bins reserved = {[240:255]};   // error if hit
    ignore_bins unused    = {[200:239]};   // don't count
  }

  cp_cmd: coverpoint cmd {
    bins read  = {READ};
    bins write = {WRITE};
  }

  cp_len: coverpoint length {
    bins short_pkt  = {[1:8]};
    bins medium_pkt = {[9:32]};
    bins long_pkt   = {[33:64]};
    bins powers[]   = {1, 2, 4, 8, 16, 32, 64}; // one bin per value
  }

  // Cross coverage — combinations of coverpoints
  addr_x_cmd: cross cp_addr, cp_cmd;

  // Transition coverage
  cp_state: coverpoint state {
    bins idle_to_run  = (IDLE => RUN);
    bins run_to_done  = (RUN  => DONE);
    bins any_to_idle  = (RUN, DONE => IDLE);
  }
endgroup

// Instantiate and sample
cg_bus bus_cov = new();

// Explicit sampling (alternative to automatic @(posedge clk))
covergroup cg_txn;
  coverpoint txn.addr;
  coverpoint txn.data;
endgroup

cg_txn txn_cov = new();
// ... later ...
txn_cov.sample();              // manually trigger sampling

// Query coverage
$display("Coverage: %.1f%%", bus_cov.get_coverage());

Processes & Synchronization

Events


event done;
event error;

// Trigger
-> done;                    // trigger event (non-blocking)
->> done;                   // non-blocking trigger (for use in always_ff)

// Wait
@(done);                    // wait for trigger
wait(done.triggered);       // level-sensitive — doesn't miss same-timestep trigger

Mailboxes


// Typed mailbox — thread-safe FIFO
mailbox #(Transaction) mbx = new(16);   // bounded: max 16 entries
mailbox #(Transaction) ubx = new();     // unbounded

// Producer
Transaction t = new();
t.randomize();
mbx.put(t);                   // blocking if full
mbx.try_put(t);               // non-blocking, returns 0 if full

// Consumer
Transaction rx;
mbx.get(rx);                  // blocking if empty
mbx.try_get(rx);              // non-blocking, returns 0 if empty
mbx.peek(rx);                 // get without removing

int n = mbx.num();            // number of entries

Semaphores


semaphore sem = new(1);        // 1 key = mutex

// Acquire
sem.get(1);                    // blocking — wait for key
sem.try_get(1);                // non-blocking, returns 0 if unavailable

// Release
sem.put(1);                    // return key

// Example: mutual exclusion
task automatic critical_section();
  sem.get(1);
  // ... exclusive access ...
  sem.put(1);
endtask

Virtual Interfaces


// Virtual interface — a handle to an interface instance
// Allows class-based testbenches to drive/sample physical signals

class Driver;
  virtual axi_lite_if vif;     // virtual interface handle

  function new(virtual axi_lite_if vif);
    this.vif = vif;
  endfunction

  task run();
    forever begin
      @(posedge vif.clk);
      vif.awaddr  <= txn.addr;
      vif.awvalid <= 1'b1;
      @(posedge vif.clk iff vif.awready);
      vif.awvalid <= 1'b0;
    end
  endtask
endclass

// In testbench top
module tb;
  logic clk, rst_n;
  axi_lite_if u_if (.clk, .rst_n);
  axi_slave   dut  (.bus(u_if.slave));

  Driver drv = new(u_if);     // pass interface to class
  initial drv.run();
endmodule

Classes cannot have ports, so virtual interface is the bridge between class-based verification (OOP) and the signal-level world of modules. Every UVM component uses virtual interfaces to access DUT signals.


Program Block


// program — separates testbench from DUT timing
// Executes in the "reactive" region (after all design updates)
program automatic test (
  input logic clk,
  bus_if.driver drv_if
);
  initial begin
    repeat (10) @(posedge clk);
    drv_if.driver_cb.data  <= 8'hAB;
    drv_if.driver_cb.valid <= 1'b1;
    @(drv_if.driver_cb);
    drv_if.driver_cb.valid <= 1'b0;
    $display("Test complete");
  end
endprogram

The program block avoids race conditions by scheduling testbench code in the Reactive region, after all RTL always blocks have executed. In UVM-based flows, program blocks are less common since UVM handles phasing internally.


Generate Blocks — SystemVerilog Style


// generate/endgenerate keywords are optional in SystemVerilog
// Loop generate
for (genvar g = 0; g < N; g++) begin : stage
  assign pipe[g+1] = pipe[g] & enable[g];
end

// Conditional generate
if (USE_PARITY) begin : gen_par
  parity_checker u_par (.data(bus), .par(par_bit));
end else begin : gen_nopar
  assign par_ok = 1'b1;
end

// Case generate
case (IMPL)
  "FAST":    fast_adder  u_add (.*);
  "SMALL":   small_adder u_add (.*);
  default:   basic_adder u_add (.*);
endcase

Loops — Enhanced


// C-style for loop (declare loop variable inline)
always_comb begin
  parity = 1'b0;
  for (int i = 0; i < 8; i++) begin
    parity ^= data[i];
  end
end

// foreach — iterate over arrays
int arr[4] = '{10, 20, 30, 40};
foreach (arr[i]) begin
  $display("arr[%0d] = %0d", i, arr[i]);
end

// foreach on multidimensional arrays
int matrix[3][4];
foreach (matrix[i, j]) begin
  matrix[i][j] = i * 4 + j;
end

// do...while
int count = 0;
do begin
  // body executes at least once
  count++;
end while (count < 10);

// break and continue
for (int i = 0; i < 100; i++) begin
  if (skip_condition) continue;   // skip rest of this iteration
  if (stop_condition) break;      // exit loop entirely
  // ...
end

System Tasks & Functions — SystemVerilog Additions

Severity System Tasks


$info("Informational message");            // severity: info
$warning("Something unusual: %0d", val);   // severity: warning
$error("Protocol violation at %0t", $time); // severity: error
$fatal(1, "Unrecoverable: FIFO overflow"); // severity: fatal (stops simulation)
// $fatal first argument sets the diagnostic level (0 prints nothing, 1 time and location, 2 adds statistics); the simulation always finishes

Array Query Functions


int arr[4][8];

$dimensions(arr)       // 2 (number of dimensions)
$left(arr, 1)          // 0 (left bound of dimension 1)
$right(arr, 1)         // 3 (right bound of dimension 1)
$low(arr, 1)           // 0
$high(arr, 1)          // 3
$size(arr, 1)          // 4 (size of dimension 1)
$increment(arr, 1)     // 1 or -1 depending on range direction
$bits(arr)             // total bits

Type & Conversion


$typename(var)          // returns type name as string
$itor(int_val)          // integer to real
$rtoi(real_val)         // real to integer
$signed(val)            // cast to signed
$unsigned(val)          // cast to unsigned
$bitstoreal(bits)       // bit pattern to real
$realtobits(r)          // real to bit pattern

Direct Programming Interface (DPI)


// Import C function into SystemVerilog
import "DPI-C" function int c_sqrt(input int val);
import "DPI-C" function void c_print(input string msg);
import "DPI-C" context function void c_callback();

// Export SystemVerilog function to C
export "DPI-C" function sv_get_data;

function int sv_get_data(input int addr);
  return mem[addr];
endfunction

// Usage
initial begin
  int result = c_sqrt(144);    // calls C function
  $display("sqrt(144) = %0d", result);
end

DPI enables calling C/C++ functions from SystemVerilog and vice versa. Use it for complex math, file parsing, or connecting to external models. The context keyword allows the C function to call back into SystemVerilog.


Compiler Directives — Additions


// Same as Verilog, plus:
`begin_keywords "1800-2012"     // restrict to specific SV version keywords
`end_keywords

`__FILE__                       // current filename (string)
`__LINE__                       // current line number (integer)

`pragma protect begin           // IP encryption
`pragma protect end

Common RTL Patterns — SystemVerilog Style

FSM with Enum


typedef enum logic [1:0] {
  IDLE = 2'b00,
  RUN  = 2'b01,
  DONE = 2'b10
} state_e;

state_e state, next_state;

always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) state <= IDLE;
  else        state <= next_state;
end

always_comb begin
  next_state = state;
  busy  = 1'b0;
  valid = 1'b0;
  unique case (state)
    IDLE: if (start) next_state = RUN;
    RUN:  begin
      busy = 1'b1;
      if (done) next_state = DONE;
    end
    DONE: begin
      valid = 1'b1;
      next_state = IDLE;
    end
    default: next_state = IDLE;
  endcase
end

Parameterized FIFO


module sync_fifo #(
  parameter type DATA_T = logic [7:0],
  parameter int  DEPTH  = 16
)(
  input  logic  clk,
  input  logic  rst_n,
  input  logic  wr_en,
  input  DATA_T wr_data,
  input  logic  rd_en,
  output DATA_T rd_data,
  output logic  full,
  output logic  empty
);
  localparam int ADDR_W = $clog2(DEPTH);

  DATA_T mem [DEPTH];
  logic [ADDR_W:0] wr_ptr, rd_ptr;       // extra bit for full/empty

  assign full  = (wr_ptr[ADDR_W] != rd_ptr[ADDR_W]) &&
                 (wr_ptr[ADDR_W-1:0] == rd_ptr[ADDR_W-1:0]);
  assign empty = (wr_ptr == rd_ptr);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_ptr <= '0;
      rd_ptr <= '0;
    end else begin
      if (wr_en && !full) begin
        mem[wr_ptr[ADDR_W-1:0]] <= wr_data;
        wr_ptr <= wr_ptr + 1'b1;
      end
      if (rd_en && !empty) begin
        rd_ptr <= rd_ptr + 1'b1;
      end
    end
  end

  assign rd_data = mem[rd_ptr[ADDR_W-1:0]];
endmodule

AXI-Style Handshake


// Valid/ready handshake — transfer occurs when both asserted
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    valid <= 1'b0;
    data  <= '0;
  end else if (valid && ready) begin
    // Transfer accepted — load next
    valid <= next_valid;
    data  <= next_data;
  end else if (!valid) begin
    // Load new data when idle
    valid <= next_valid;
    data  <= next_data;
  end
end

// Assertion to verify protocol
assert property (@(posedge clk) disable iff (!rst_n)
  valid && !ready |=> valid && $stable(data)
) else $error("Valid data changed without ready");

Packed Struct Bus


package bus_pkg;
  typedef enum logic [1:0] {
    CMD_READ  = 2'b00,
    CMD_WRITE = 2'b01,
    CMD_NOP   = 2'b10
  } cmd_e;

  typedef struct packed {
    cmd_e         cmd;
    logic [15:0]  addr;
    logic [31:0]  data;
    logic [3:0]   strobe;
  } bus_req_t;

  typedef struct packed {
    logic [31:0]  data;
    logic [1:0]   resp;
    logic         valid;
  } bus_resp_t;
endpackage

module bus_master
  import bus_pkg::*;
(
  output bus_req_t  req,
  input  bus_resp_t resp
);
  always_comb begin
    req.cmd    = CMD_WRITE;
    req.addr   = 16'h1000;
    req.data   = 32'hDEAD_BEEF;
    req.strobe = 4'hF;
  end
endmodule

Edge Detector (SystemVerilog Style)


logic sig_q;
always_ff @(posedge clk) sig_q <= sig;

logic rising;  assign rising  = sig & ~sig_q;
logic falling; assign falling = ~sig & sig_q;

// Or use $rose/$fell in assertions and procedural code
always_ff @(posedge clk)
  if ($rose(sig)) count <= count + 1;

UVM Essentials (Quick Reference)

SystemVerilog classes power the Universal Verification Methodology. Here are the key building blocks.

UVM Component Hierarchy

ClassPurpose
uvm_testTop-level test — configures the environment
uvm_envContains agents, scoreboards, coverage
uvm_agentContains driver, monitor, sequencer
uvm_driverDrives stimulus onto DUT interface
uvm_monitorObserves DUT interface passively
uvm_sequencerRoutes sequence items to driver
uvm_sequenceGenerates sequence items (stimulus)
uvm_sequence_itemTransaction-level data object
uvm_scoreboardChecks DUT output against expected

Minimal UVM Skeleton


// Sequence item
class my_txn extends uvm_sequence_item;
  `uvm_object_utils(my_txn)

  rand bit [7:0] addr;
  rand bit [31:0] data;

  function new(string name = "my_txn");
    super.new(name);
  endfunction
endclass

// Sequence
class my_seq extends uvm_sequence #(my_txn);
  `uvm_object_utils(my_seq)

  function new(string name = "my_seq");
    super.new(name);
  endfunction

  task body();
    my_txn txn;
    repeat (10) begin
      txn = my_txn::type_id::create("txn");
      start_item(txn);
      assert(txn.randomize());
      finish_item(txn);
    end
  endtask
endclass

// Driver
class my_driver extends uvm_driver #(my_txn);
  `uvm_component_utils(my_driver)
  virtual bus_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      // drive signals via vif
      @(posedge vif.clk);
      vif.addr <= req.addr;
      vif.data <= req.data;
      @(posedge vif.clk);
      seq_item_port.item_done();
    end
  endtask
endclass

Synthesis Golden Rules — SystemVerilog Edition

  • Use logic instead of wire/reg — except for multi-driver nets
  • Use always_comb, always_ff, always_latch — never plain always
  • Use = in always_comb, <= in always_ff — the convention that lint tools check
  • Use unique case or priority case — replaces full_case/parallel_case
  • Use typedef enum logic [N:0] for FSM states — explicit encoding width
  • Use packed structs for bus bundles — cleaner than raw vectors
  • Use packages for shared types and parameters — avoid `defines
  • Use interfaces for complex bus protocols — reduce port clutter
  • Use $clog2() for address widths
  • Use '0, '1, 'x, 'z for context-width fill
  • `default_nettype none at the top of every file
  • One module per file, filename matches module name

What Is Synthesizable?

FeatureSynthesizable?Notes
logic, wire, packed structs/unionsYesCore RTL types
enum, typedefYesUse for FSMs, type clarity
always_comb, always_ff, always_latchYesPreferred over always
unique/priority case/ifYesSynthesis directives + runtime checks
Interfaces & modportsYesMajor synthesis tools support
PackagesYesTypes, parameters, functions
Generate blocksYesParameterized hardware
Inline variable declarations (for (int i...))YesC-style loops
bit, int, byte, shortintPartialSome tools support; prefer logic in RTL
Assertions (assert property)IgnoredStripped by synthesis, valuable for formal
Classes, dynamic arrays, queues, stringsNoVerification only
Randomization, coverage, mailboxes, semaphoresNoVerification only
DPI, program blockNoSimulation/verification only

Common Pitfalls

MistakeResultFix
Using int/bit in RTL portsTwo-state: no x-propagationUse logic for RTL signals
Unpacked struct on portTool-specific behaviorUse packed structs for RTL ports
Missing unique/priorityNo synthesis/sim checksAnnotate every case statement
logic with multiple driversCompile errorUse wire for tri-state/multi-driver
Enum without explicit widthDefaults to int (32-bit)Use enum logic [N:0]
Blocking in always_ffLegal, but flagged by lint and prone to race conditionsUse <= non-blocking
Using always @(*) in SVNo latch checkingUse always_comb
Forgetting disable iff in assertionsFalse failures during resetAlways add disable iff (!rst_n)
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5