SystemVerilog

SystemVerilog

One language covering three jobs: describing hardware that synthesizes into gates, specifying properties that formal and simulation tools check, and building the object-oriented testbenches that verify it all.

!!! info "Framing for this handbook"

SystemVerilog is often described as "Verilog plus verification features," which undersells it. It is a **superset of Verilog-2005** that merged three previously separate worlds — the Verilog HDL, the Vera/OpenVera verification language, and assertion languages like PSL — into a single IEEE standard (**IEEE 1800**).

For an ASIC designer, the important consequence is that only a well-defined **synthesizable subset** maps to hardware; everything else exists to test that hardware. Knowing exactly where that boundary sits is the single most valuable thing on this page.

1. What SystemVerilog Is

!!! abstract "Core idea"

SystemVerilog is a **hardware description and verification language (HDVL)**. The same file format that describes a flop can also declare a class, randomize a transaction, and assert a protocol rule. Tools decide what to do with each construct: a synthesis tool ignores or errors on non-synthesizable code; a simulator runs all of it.

Lineage

Verilog-1995  (IEEE 1364-1995)
      │
Verilog-2001  (IEEE 1364-2001)   ANSI ports, generate, signed types
      │
Verilog-2005  (IEEE 1364-2005)
      │
      ├── + Vera / OpenVera        (classes, constrained random, coverage)
      ├── + assertion concepts     (PSL / Sugar lineage → SVA)
      ├── + C-like data types      (int, struct, enum, typedef)
      ▼
SystemVerilog 3.1a  (Accellera, 2004)
      │
IEEE 1800-2005      first IEEE SystemVerilog standard
IEEE 1800-2009      Verilog (1364) folded IN — one unified standard
IEEE 1800-2012 / -2017 / -2023    incremental refinement

Since 2009 there is no separate Verilog standard. IEEE 1800 is Verilog. When someone says "we write Verilog," in a modern ASIC flow they almost always mean the SystemVerilog RTL subset.

The problems it was built to solve

  • Intent was invisible


    In Verilog, always @(*) could accidentally infer a latch, and always @(posedge clk) could accidentally infer combinational logic. Nothing in the code stated what you meant.

    Fix: always_comb, always_ff, always_latch — tools now check intent against inference.

  • Port lists didn't scale


    A 200-signal bus between two blocks meant 200 port declarations in every module, repeated in every instantiation, edited in five places on every change.

    Fix: interface and modport bundle signals and direction once.

  • Testbenches were toys


    Verilog had no classes, no randomization, no functional coverage. Real verification lived in a separate language (Vera, e) with a fragile inter-language bridge.

    Fix: OOP, constrained-random stimulus, covergroups, mailboxes — all native. UVM is a library on top of this.

  • No way to state a rule


    "Grant must follow request within 4 cycles" lived in a spec document, not in the code, and drifted from reality.

    Fix: SystemVerilog Assertions — temporal properties that both simulation and formal tools consume.


2. The Language in Layers

┌──────────────────────────────────────────────────────────────────────┐
│                      SystemVerilog (IEEE 1800)                       │
├───────────────────┬───────────────────┬──────────────────────────────┤
│  DESIGN LAYER     │  ASSERTION LAYER  │   VERIFICATION LAYER         │
│  (synthesizable)  │  (SVA)            │   (simulation only)          │
├───────────────────┼───────────────────┼──────────────────────────────┤
│  module           │  assert property  │   class / extends            │
│  always_ff        │  sequence         │   rand / randc / constraint  │
│  always_comb      │  property         │   covergroup / coverpoint    │
│  logic / bit      │  cover property   │   mailbox / semaphore        │
│  struct / enum    │  assume property  │   program / clocking block   │
│  interface        │  |-> |=> ##N      │   fork...join_any/none       │
│  generate / for   │  $rose $stable    │   virtual interface handle   │
│  parameter        │  disable iff      │   DPI-C imports              │
├───────────────────┴───────────────────┴──────────────────────────────┤
│  Consumed by:                                                        │
│    Synthesis  ──► design layer only (errors/ignores the rest)        │
│    Simulation ──► all three layers                                   │
│    Formal     ──► design layer + assertion layer                     │
│    Lint/CDC   ──► design layer, plus assertions as extra intent      │
└──────────────────────────────────────────────────────────────────────┘

!!! warning "The one rule that matters"

A construct being *legal SystemVerilog* tells you nothing about whether it becomes hardware. `#10ns`, `initial`, `class`, `real`, dynamic arrays, and `while` loops with data-dependent bounds all simulate fine and all fail (or silently mislead) in synthesis.

Keep design files and verification files in separate directories and separate filelists. It's the cheapest guardrail you can put in a flow.

3. Data Types

4-state vs. 2-state

A 4-state type holds 0, 1, X (unknown/conflict), and Z (high-impedance). A 2-state type holds only 0 and 1.

Type States Width Signed? Use for
logic 4 1 bit (or packed) unsigned Default choice for all RTL signals. Replaces both reg and wire in most places.
wire 4 1 bit (or packed) unsigned Only when you actually need a net with multiple drivers / resolution (tri-state, bidirectional pads).
bit 2 1 bit (or packed) unsigned Testbench counters, models, anything where X-propagation would just be noise.
byte 2 8 signed TB scoreboards, DPI interop.
shortint / int / longint 2 16 / 32 / 64 signed Loop indices, TB math. int unsigned for unsigned.
integer 4 32 signed Legacy Verilog. Prefer int.
real / shortreal — float signed Analog models, timing math. Never synthesizable.
string — dynamic — Messages, TB only.

!!! tip "Why RTL wants 4-state"

X-propagation is a debug feature, not a nuisance. An uninitialized flop or an unreached case arm shows up as `X` in waveforms and poisons downstream logic loudly. If you model RTL in `bit`, that same bug reads as a clean `0` and hides until silicon.

**Use `logic` in design, `bit` in testbenches.**

logic vs. reg vs. wire

This trips up everyone coming from Verilog. The old reg/wire split was never about storage — it was about how the value gets assigned.

VERILOG (two type systems, procedural vs continuous)
  reg  q;    // assigned inside always/initial (procedural)
  wire y;    // assigned by assign or module output (continuous)
  → "reg" implies memory to newcomers. It doesn't. reg can be pure combo.

SYSTEMVERILOG
  logic q;   // assign it EITHER way — procedurally OR continuously.
             // A single-driver variable. The compiler enforces
             // single-driver, which catches real bugs.

  Still need `wire` when:
    - multiple drivers must be resolved (tri-state bus, inout pad)
    - you need a net type with a strength/resolution function

  Rule of thumb:  logic everywhere; wire only for multi-driver nets.

Packed vs. unpacked arrays

The dimension position relative to the identifier decides everything.

// PACKED — dimensions BEFORE the name. A contiguous vector of bits.
logic [31:0] word;              // one 32-bit value; can do arithmetic on it
logic [3:0][7:0] word_bytes;    // same 32 bits, addressable as 4 bytes
word_bytes[2]                   // → bits [23:16]. Legal, free in hardware.

// UNPACKED — dimensions AFTER the name. A collection of separate objects.
logic [31:0] mem [0:1023];      // 1024 separate 32-bit words → infers a RAM
logic [31:0] mem [1024];        // same thing; [size] means [0:size-1]

// The distinction in one line:
//   packed   = one number you can slice, add, shift, compare
//   unpacked = many numbers you can index, iterate, and infer memory from

// Combine them freely:
logic [3:0][7:0] fifo [16];     // 16 entries, each 4 bytes / 32 bits

!!! warning

You cannot do arithmetic on an unpacked array as a whole, and you cannot pass it through a port that expects a packed vector. When a design won't compile with a cryptic width error, a packed/unpacked mismatch is the usual culprit.

User-defined types

=== "enum"

Strongly-typed named constants. Synthesizable, and the single biggest readability win in FSM code.

```systemverilog
typedef enum logic [2:0] {
    IDLE   = 3'b001,   // explicit encoding — one-hot here
    FETCH  = 3'b010,
    EXEC   = 3'b100
} state_e;

state_e state, next_state;   // waveforms show IDLE/FETCH/EXEC, not 3'b001

// You get for free:
state.name()      // → "FETCH" as a string, great for $display
state.next()      // → next enum member
// And the compiler rejects: state = 3'b111;  (not a valid member)
```

Leave encodings off (`{IDLE, FETCH, EXEC}`) and the synthesis tool picks binary / gray / one-hot for you based on your constraints — usually the right call.

=== "struct"

Bundle related signals into one object. `packed` structs are just a vector with named fields — fully synthesizable and portable through ports.

```systemverilog
typedef struct packed {
    logic        valid;
    logic [3:0]  id;
    logic [31:0] addr;
    logic [63:0] data;
} req_t;                       // exactly 101 bits, MSB = valid

req_t req;
req.addr = 32'hDEAD_BEEF;      // field access
some_fifo_din = req;           // or treat the whole thing as 101 bits

// UNPACKED struct — fields may be padded/reordered, NOT a bit vector.
// Fine in a testbench, avoid in synthesizable ports.
typedef struct { int a; string s; } tb_item_t;
```

!!! tip

    Packed structs are how you make a wide bus survive refactoring. Add a field to `req_t` and every producer, consumer, and FIFO width follows automatically — no 40-file edit.

=== "typedef / package"

Put shared types, parameters, and functions in a `package` and import it. This is the standard way to keep type definitions consistent across a hierarchy.

```systemverilog
package soc_pkg;
    parameter int ADDR_W = 32;
    parameter int DATA_W = 64;

    typedef enum logic [1:0] { OKAY, EXOKAY, SLVERR, DECERR } resp_e;

    typedef struct packed {
        logic [ADDR_W-1:0] addr;
        logic [DATA_W-1:0] data;
    } beat_t;

    function automatic int clog2_ceil(int n);
        return $clog2(n);
    endfunction
endpackage

// In a module:
import soc_pkg::*;              // bring everything in
// or, more disciplined:
import soc_pkg::beat_t;
import soc_pkg::resp_e;

module node import soc_pkg::*; (  // import in the header so ports can use the types
    input  beat_t in_beat,
    output resp_e resp
);
```

4. Procedural Blocks & RTL Modeling

!!! abstract "The golden rules"

- Sequential logic → **`always_ff`** with nonblocking (`<=`)
- Combinational logic → **`always_comb`** with blocking (`=`)
- Never mix the two assignment styles in one block
- Never drive the same signal from two blocks

These rules eliminate most simulation/synthesis mismatches before they exist.

The three intent-declaring blocks

// ── SEQUENTIAL ────────────────────────────────────────────────
// Tool CHECKS that this infers flops. Nonblocking assignments.
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)        count <= '0;        // async active-low reset
    else if (clear)    count <= '0;
    else if (incr)     count <= count + 1'b1;
end
// No `else` needed — the flop simply holds. That's not a latch.

// ── COMBINATIONAL ─────────────────────────────────────────────
// Sensitivity list is inferred AND the tool errors if a latch is inferred.
always_comb begin
    next_state = state;          // ← default assignment: kills latch inference
    grant      = '0;
    unique case (state)
        IDLE : if (req) next_state = FETCH;
        FETCH: begin grant = 1'b1; next_state = EXEC; end
        EXEC : next_state = IDLE;
        default: next_state = IDLE;   // always have a default
    endcase
end

// ── LATCH (rare, and you should mean it) ──────────────────────
always_latch begin
    if (enable) q <= d;          // no else → intentional latch
end
Block Sensitivity Tool check Assignment
always_ff Explicit (@(posedge clk ...)) Must infer flip-flops Nonblocking <=
always_comb Inferred from all RHS reads Must be latch-free, no incomplete assignment Blocking =
always_latch Inferred Must infer latches Nonblocking <=
always (legacy) Whatever you wrote None. Silently infers anything. Whatever you wrote
always @(*) Inferred None — will happily create a latch Blocking

!!! danger "Ban plain always in new RTL"

The entire point of `always_comb` / `always_ff` is that the tool now verifies your intent. A plain `always @(*)` that misses a signal in the sensitivity list simulates one way and synthesizes another — the classic, expensive, silicon-respin-grade bug that these keywords exist to make impossible.

Blocking vs. nonblocking — why it actually matters

GOAL: a 2-stage shift register.   d ──▶[FF a]──▶[FF b]──▶ q

CORRECT (nonblocking — both flops sample the OLD values):
  always_ff @(posedge clk) begin
      a <= d;
      b <= a;      // ← 'a' here is the value BEFORE this clock edge
  end
  Result:  two flops in series.  ✓

WRONG (blocking — executes like software, top to bottom):
  always_ff @(posedge clk) begin
      a = d;
      b = a;       // ← 'a' was ALREADY updated to d on the line above
  end
  Result:  ONE flop. 'd' races straight through to b.  ✗

WHY: nonblocking assignments evaluate all RHS first, then update all LHS
     at the end of the time step. That is exactly what real flops do —
     every flop in a design samples its input simultaneously.

!!! tip

Say it as a physical fact, not a coding style: **flops sample simultaneously, so model them with an assignment that updates simultaneously.** Combinational logic settles in order, so model it with an assignment that executes in order. The rule follows from the hardware.

unique, unique0, and priority

Qualifiers on case and if that state your intent to both the simulator (runtime violation reports) and synthesis (optimization freedom).

Qualifier You are asserting Synthesis effect Sim effect if violated
unique case Exactly one arm matches, always Parallel mux, no priority chain Runtime warning if 0 or 2+ arms match
unique0 case At most one arm matches (zero is OK) Parallel mux Warning only if 2+ match
priority case At least one matches; first match wins Priority encoder chain Warning if no arm matches
(none) Nothing Priority chain (safe default) Silent

!!! warning "These are promises, not requests"

If you write `unique case` and two arms can genuinely overlap, synthesis builds a parallel mux that produces garbage for that input, while simulation may only print a warning you filtered out.

The old `// synopsys full_case parallel_case` pragmas were the same trap with no simulation check at all — `unique`/`priority` are strictly better because the simulator polices the claim. Still: only claim what you can prove.

5. Design Hierarchy & Reuse

Parameterization

module sync_fifo #(
    parameter int  DEPTH = 16,
    parameter type T     = logic [31:0],   // type parameters! very useful
    localparam int PTR_W = $clog2(DEPTH)   // derived — caller cannot override
) (
    input  logic clk, rst_n,
    input  logic push, pop,
    input  T     din,
    output T     dout,
    output logic full, empty
);
    T mem [DEPTH];
    logic [PTR_W:0] wptr, rptr;   // extra MSB for full/empty disambiguation
    // ...
endmodule

// Instantiation — always use named ports and named parameters.
sync_fifo #(
    .DEPTH (64),
    .T     (beat_t)              // pass a struct type straight in
) u_req_fifo (
    .clk   (clk),
    .rst_n (rst_n),
    .push  (req_valid && req_ready),
    .pop   (fifo_pop),
    .din   (req_beat),
    .dout  (fifo_beat),
    .full  (fifo_full),
    .empty (fifo_empty)
);

!!! tip "localparam vs. parameter"

**`parameter`** = knob the instantiator turns. **`localparam`** = value derived from those knobs.

Making `PTR_W` a `parameter` invites someone to override it inconsistently with `DEPTH`; making it a `localparam` makes that impossible.

Generate — structural elaboration

generate constructs are evaluated at elaboration time, before simulation or synthesis begins. They build structure; they are not runtime loops.

// generate-for: replicate hardware
genvar i;
generate
    for (i = 0; i < NUM_LANES; i++) begin : g_lane   // ← ALWAYS name the block
        lane_top #(.LANE_ID(i)) u_lane (
            .clk  (clk),
            .din  (din [i]),
            .dout (dout[i])
        );
    end
endgenerate
// Hierarchy becomes: top.g_lane[0].u_lane, top.g_lane[1].u_lane, ...
// Unnamed blocks get tool-generated names → your SDC, UPF, and
// waveform scripts break on the next tool version. Name every block.

// generate-if: compile-time configuration
generate
    if (USE_ECC) begin : g_ecc
        ecc_encoder u_enc (.*);
    end else begin : g_no_ecc
        assign data_out = data_in;
    end
endgenerate

!!! warning

A `for` loop inside `always_comb` is *also* unrolled at elaboration — it is not a runtime loop either. The bounds must be compile-time constant. If you find yourself wanting a data-dependent loop count in RTL, what you actually want is an FSM.

6. Interfaces

An interface is a named bundle of signals — plus, optionally, the tasks, functions, assertions, and coverage that go with the protocol. A modport declares which direction each signal takes from a given side's point of view.

interface axi_lite_if #(parameter int AW = 32, DW = 32) (input logic clk, rst_n);

    // write address channel
    logic [AW-1:0]   awaddr;
    logic            awvalid, awready;
    // write data channel
    logic [DW-1:0]   wdata;
    logic [DW/8-1:0] wstrb;
    logic            wvalid, wready;
    // write response
    logic [1:0]      bresp;
    logic            bvalid, bready;

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

    modport slave (              // same signals, mirrored directions
        input  awaddr, awvalid, wdata, wstrb, wvalid, bready,
        output awready, wready, bresp, bvalid,
        input  clk, rst_n
    );

    // Protocol checking lives WITH the protocol — every instance gets it free.
    property p_awvalid_stable;
        @(posedge clk) disable iff (!rst_n)
        awvalid && !awready |=> awvalid && $stable(awaddr);
    endproperty
    a_awvalid_stable: assert property (p_awvalid_stable)
        else $error("AW channel: valid dropped or addr changed before ready");

endinterface

// ── Usage ──────────────────────────────────────────────────────
module cpu   (axi_lite_if.master bus, ...);   // one port, whole protocol
module sram  (axi_lite_if.slave  bus, ...);

module top;
    axi_lite_if #(.AW(32)) bus (.clk(clk), .rst_n(rst_n));
    cpu  u_cpu  (.bus(bus));
    sram u_sram (.bus(bus));
endmodule
  • What you gain


    • Add a signal to the protocol → edit one file, not every module and instantiation
    • Direction errors caught at compile time by the modport
    • Assertions and coverage live next to the signals they describe
    • Testbench gets a virtual interface handle — the standard UVM driver/monitor connection
  • What to watch for


    • Synthesis support for interfaces is good but not universal — check your tool and your IP vendors before committing
    • Interfaces at the chip top-level pins are usually a bad idea; flatten to plain ports at the boundary
    • Some flows (formal, some lint, some legacy IP) prefer flat ports — many teams use interfaces internally and flatten at block boundaries

7. The Synthesizable Subset

!!! abstract "Working definition"

Synthesizable code describes a **static, finite structure of gates and flops**. Anything that implies unbounded time, dynamic memory allocation, or a value that can't be resolved at elaboration is not synthesizable — regardless of whether it simulates beautifully.
Category Synthesizable Not synthesizable
Types logic, bit, wire, packed arrays/structs/unions, enum, int (as constants/loop vars) real, string, class, dynamic arrays, queues, associative arrays, ref args
Procedural always_ff, always_comb, always_latch, assign, function, task (no timing) initial, final, fork/join, #delay, wait, @ inside a task
Control flow if/else, case/casez, for with constant bounds, generate while/forever with data-dependent bounds, casex (legal but banned by most style guides)
Structure module, interface (tool-dependent), package, parameter, hierarchical instantiation program, clocking blocks, covergroup, constraint, randomize()
System tasks $clog2, $bits, $size, $signed/$unsigned, $countones, $onehot $display, $finish, $random, $time, $fopen (ignored or errored)
Assertions Ignored by synthesis (harmless), consumed by formal —

!!! danger "casex is a trap"

It treats `X` in the *case expression* as a wildcard, so an `X` arriving on a real signal can match an arm it should never have matched — and simulation will happily proceed down the wrong branch while the gates do something else.

Use `casez` (only `?`/`Z` is a wildcard) or, better, `case ... inside` with explicit wildcards. Most ASIC style guides ban `casex` outright.

The inference cheat sheet

WHAT YOU WRITE                          WHAT YOU GET
─────────────────────────────────────────────────────────────────
always_comb + every output assigned
  on every path                    ──►  pure combinational cloud

always_comb + a path that DOESN'T
  assign an output                 ──►  LATCH (tool will error — good)

always_ff @(posedge clk)           ──►  D flip-flop, no reset

always_ff @(posedge clk)
  if (!rst_n) q <= 0;              ──►  D flop with SYNCHRONOUS reset

always_ff @(posedge clk or
            negedge rst_n)
  if (!rst_n) q <= 0;              ──►  D flop with ASYNCHRONOUS reset

logic [31:0] mem [1024];
 + one write port, one read port   ──►  RAM macro (or flops, if small
                                        and the tool can't map it)

assign y = sel ? a : b;            ──►  2:1 mux

if / else if / else chain          ──►  priority mux chain
unique case                        ──►  parallel mux

for (i=0;i<N;i++) sum += x[i];
  inside always_comb               ──►  N-input adder tree, UNROLLED
                                        (not a loop — N adders of area)

8. Assertions (SVA)

SystemVerilog Assertions let you write a temporal rule — something that must hold over time — as code. Simulation checks it on every run; formal tools try to prove it for all inputs. Both consume the same source.

Immediate vs. concurrent

// IMMEDIATE — a plain procedural statement, checked when execution reaches it.
always_comb begin
    a_onehot: assert ($onehot0(grant))
        else $error("Arbiter granted more than one requester");
end

// CONCURRENT — clocked, evaluated across time. This is the interesting one.
property p_req_gets_grant;
    @(posedge clk) disable iff (!rst_n)     // ← never omit disable iff
    req |-> ##[1:4] grant;
endproperty

a_req_gets_grant: assert property (p_req_gets_grant)
    else $error("req not granted within 4 cycles");

c_req_gets_grant: cover property (p_req_gets_grant);  // did we ever exercise it?
Operator Name Meaning
|-> Overlapping implication If LHS holds, RHS must hold starting the same cycle.
|=> Non-overlapping implication If LHS holds, RHS must hold starting the next cycle.
##N Cycle delay Exactly N clock cycles later.
##[M:N] Delay range Between M and N cycles later. ##[1:$] = eventually.
$rose / $fell Edge Signal transitioned 0→1 / 1→0 since the last clock.
$stable / $changed Value sampling Value did / did not change since the last clock.
$past(sig, N) History Value of sig N clocks ago.
throughout, within Sequence composition Constrain one sequence relative to another.
disable iff (x) Reset guard Abort evaluation while x is true. Always guard with reset.

assert vs. assume vs. cover

  • assert


    "This must be true." A failure is a bug in the DUT. Simulation reports it; formal tries to disprove it.

  • assume


    "The environment guarantees this." Constrains the formal tool's input space. In simulation, typically treated as an assert on the stimulus.

    Over-assuming is how formal proofs become meaningless — you proved the design correct in a world that can't happen.

  • cover


    "Did we ever see this?" No pass/fail — it reports whether the scenario was reached.

    Essential: an assertion that never fires because the scenario never occurred proves nothing.

!!! tip "Where to put them"

Inline in the RTL for internal invariants (one-hot state, FIFO never overflows, counter never wraps unexpectedly), and inside the `interface` for protocol rules — that way every instantiation of the protocol is checked, in every testbench, forever, with no extra work.

9. The Verification Half

You won't write this in RTL, but you'll read it in bug reports, review it in DV code, and use it when you write your own block-level testbench. Here's the working vocabulary.

class / extends
: Full OOP: inheritance, virtual methods, polymorphism, handles (references, not values). Objects are dynamically allocated with new() and garbage-collected. This is the foundation UVM is built on.

rand / randc / constraint
: Constrained-random stimulus. Declare a class member rand, write constraint c { addr inside {[0:1023]}; len dist {1:=70, [2:8]:=30}; }, then call obj.randomize(). A SAT-based solver picks legal values. This is how you cover a state space no directed test could reach.

covergroup / coverpoint / cross
: Functional coverage — the measurement of what your random tests actually hit. Distinct from code coverage (lines/toggles/branches). Coverage closure is the exit criterion for most verification plans.

mailbox / semaphore / event
: Inter-process communication between concurrent testbench threads (driver, monitor, scoreboard).

fork ... join / join_any / join_none
: Spawn concurrent threads. join waits for all, join_any for the first, join_none for none (fire and forget).

clocking block
: Declares sampling and driving skew relative to a clock edge, so the testbench can't race the DUT. Solves the classic "did my driver's value land before or after the flop sampled?" problem.

program block
: A testbench container that executes in the Reactive region — after all design events settle. Largely superseded by clocking blocks + UVM conventions, but you'll see it in older code.

virtual interface
: A handle to a physical interface instance, usable inside a class. This is the bridge between the static design world and the dynamic class-based TB world.

DPI-C
: Direct Programming Interface. Call C/C++ from SystemVerilog and vice versa. Used for reference models, ISS co-simulation, and reusing existing C models.

!!! info "UVM is not part of SystemVerilog"

The Universal Verification Methodology is a *class library* (IEEE 1800.2) written in SystemVerilog, providing a standard structure for agents, sequences, drivers, monitors, and scoreboards. It's convention on top of the language, not the language itself. Worth its own handbook page.

10. Scheduling & Race Avoidance

Every simulation time step is divided into ordered regions. Understanding the ordering is what turns "nonblocking assignments are a rule I follow" into "nonblocking assignments are obviously correct."

Within ONE simulation time step (simplified from the 17-region LRM model):

  ┌─ PREPONED ────────── sample values for concurrent assertions
  │                      (BEFORE anything changes — this is why SVA
  │                       never sees a race with the RTL it's checking)
  │
  ├─ ACTIVE ──────────── blocking assignments (=), continuous assigns,
  │                      always_comb bodies, RHS of nonblocking evaluated
  │
  ├─ INACTIVE ────────── #0 delays (avoid these — they're a smell)
  │
  ├─ NBA ─────────────── nonblocking assignments (<=) UPDATE their LHS
  │                      ← all flops in the design land here, together
  │
  ├─ OBSERVED ────────── concurrent assertions evaluate
  │
  ├─ REACTIVE ────────── program blocks / testbench code runs
  │                      (so the TB sees settled design values)
  │
  └─ POSTPONED ───────── $strobe, $monitor, coverage sampling


THE RACE THAT NONBLOCKING KILLS:

   always_ff @(posedge clk) a <= d;     // both blocks trigger on the
   always_ff @(posedge clk) b <= a;     // same edge — but in WHAT ORDER?

   The LRM does NOT define which always block runs first.
   With BLOCKING assignments, the answer decides your circuit. Nondeterministic.

   With NONBLOCKING: both RHS are read in ACTIVE (old values),
   both LHS are written in NBA. Order of the blocks is irrelevant.
   Deterministic, and it matches what the gates do.  ✓

!!! tip

The PREPONED region is why you can drop assertions into RTL without worrying about them racing the logic they observe — they sample the values as they existed *before* any of this time step's activity. This is also why `$past()` is trustworthy.

11. Pitfalls & Lint Rules

  • Unintentional latch


    An always_comb path that doesn't assign an output on every branch infers a latch — a level-sensitive element with no defined timing arc, a nightmare for STA.

    Fix: assign a default to every output at the top of the block, then override. always_comb makes the tool error on this; plain always @(*) does not. That's the whole argument for the keyword.

  • Blocking in sequential logic


    Using = inside always_ff creates order dependence between always blocks. May simulate correctly, may synthesize to something else, may differ between simulators.

    Fix: <= in always_ff, = in always_comb, never mixed. Every lint tool checks this. Never waive it.

  • Width mismatch / silent truncation


    logic [7:0] a = b + c; where b, c are 8-bit silently drops the carry. Verilog's implicit width rules are permissive and will not warn you by default.

    Fix: turn width warnings into errors in your lint policy. Size literals explicitly (8'd1, not 1). Use '0/'1 for context-sized fill.

  • Incomplete sensitivity list


    Legacy: always @(a or b) when the block also reads c. Simulation misses the update; synthesis builds the full combinational cloud. Classic sim/synth mismatch.

    Fix: always_comb. It derives the sensitivity list, and it includes signals read by functions the block calls — which @(*) does not.

  • Reset style mixing


    Some flops async-reset, some sync-reset, some not reset at all, with no documented policy. Reset release becomes a metastability hazard and STA becomes guesswork.

    Fix: pick one house style (commonly: async assert, synchronous de-assert, via a reset synchronizer). Document it. Lint for deviations.

  • Unsynchronized clock crossings


    SystemVerilog will not stop you from sampling a signal from another clock domain with a single flop. Silicon will.

    Fix: a dedicated CDC tool — not lint, not simulation, since zero-delay models cannot see metastability. Use standard synchronizer cells and mark them.

  • Multiple drivers


    Two always blocks assigning the same signal. With logic this is a compile error (single-driver rule) — with wire it silently resolves to X.

    Fix: use logic, not wire, so the compiler catches it. One signal, one driver, always.

  • `include and macro soup


    Verilog macros (`define) are global, textual, and have no scope. A macro collision across two IP blocks is a miserable debug.

    Fix: use package + parameter + typedef instead of macros wherever possible. Scoped, typed, and visible in the debugger.

A starter house style

  • Types: logic in design, bit in TB. wire only for multi-driver nets.
  • Blocks: always_ff / always_comb / always_latch only. Plain always is a lint error.
  • Assignments: <= in always_ff, = in always_comb. Never mixed in one block.
  • Case: always a default. Use unique/priority when you can prove the claim. casex banned.
  • Ports: named connection only (.clk(clk)). Positional connection is a lint error. .* is a team-by-team judgment call.
  • Names: active-low suffix _n, clock clk_*, reset rst_n, types _t, enums _e, generate blocks g_*, instances u_*.
  • Constants: localparam for derived values, parameter only for real knobs. No bare magic numbers.
  • Files: one module per file, filename matches module name. Design and TB in separate trees.

12. Study Checklist

Foundations

  • [ ] Explain what IEEE 1800 unified and why Verilog no longer has a separate standard
  • [ ] Name the three layers (design / assertion / verification) and which tools consume each
  • [ ] State the working definition of the synthesizable subset

Data types

  • [ ] Explain 4-state vs. 2-state and why RTL should use logic, not bit
  • [ ] Explain what logic replaced and when you still need wire
  • [ ] Distinguish packed vs. unpacked arrays and know which infers memory
  • [ ] Write a packed struct and a typedef'd enum for an FSM state
  • [ ] Use a package to share types and parameters across a hierarchy

RTL modeling

  • [ ] State what each of always_ff, always_comb, always_latch checks
  • [ ] Explain blocking vs. nonblocking using the 2-stage shift register example
  • [ ] Explain how a default assignment at the top of always_comb prevents a latch
  • [ ] Contrast unique, unique0, priority and their synthesis consequences
  • [ ] Explain why casex is dangerous and what to use instead
  • [ ] Write a 3-block FSM (state reg / next-state comb / output comb)

Structure & reuse

  • [ ] Explain parameter vs. localparam and when each is correct
  • [ ] Use a type parameter to make a generic FIFO
  • [ ] Explain that generate and RTL for loops are elaboration-time, not runtime
  • [ ] Explain why every generate block must be named
  • [ ] Write an interface with mirrored modports and explain the tradeoffs of using them

Assertions & scheduling

  • [ ] Distinguish immediate from concurrent assertions
  • [ ] Explain |-> vs. |=>, and ##N vs. ##[M:N]
  • [ ] Explain why disable iff (!rst_n) belongs on essentially every property
  • [ ] Distinguish assert / assume / cover and explain how over-assuming invalidates a formal proof
  • [ ] Name the event regions in order and explain why NBA makes flop order irrelevant

Pitfalls

  • [ ] List the classic sim/synth mismatch sources and how the SV keywords eliminate each
  • [ ] Explain why width truncation is silent by default and how to catch it
  • [ ] Explain why simulation cannot find CDC bugs and what tool must
  • [ ] Recite your team's house style from memory (or write one)

References

  • IEEE 1800-2023 — SystemVerilog LRM
  • IEEE 1800.2 — UVM
  • Sutherland & Mills — Standard Gotchas: Subtleties in the Verilog and SystemVerilog Standards
  • Cummings — nonblocking assignment papers (SNUG)
Contact

Have an Idea?
Let's Build It.

Whether you're a startup with a hardware concept, an established company needing deep design help, or an enthusiast with a wild project — we want to hear about it.

LocationBoulder, Colorado