SystemVerilog

SV Data
Types

From 4-state logic to packed arrays — why SystemVerilog's type system exists, how it prevents simulation-synthesis mismatch, and when X masking silently hides bugs.

1. The 4-State Logic System

Standard programming languages use only two states: true and false, 0 and 1. Hardware simulation requires four because wires in real silicon are not always cleanly driven to VDD or GND.

0 — Logic Zero

Driven low — wire connected to GND or output of a gate whose output is low. The nominal "off" state.

1 — Logic One

Driven high — wire connected to VDD or output of a gate whose output is high. The nominal "on" state.

X — Unknown

Uninitialized, multiple conflicting drivers, or result of an illegal operation. X propagates through gates to expose potential bugs.

Z — High-Z

Undriven (floating) wire, or tri-state buffer in disabled state. A wire left floating in real hardware is a bug.

The simulator propagates X through logic. If AND(X, 0) = 0 (X can't change the result) but AND(X, 1) = X (result is unknown), the X propagation makes bugs visible at the output where behavior is actually wrong — not just at the uninitialized register.

Simulation ≠ Synthesis for X: A synthesizer treats X as "don't care" — it may assign 0 or 1. A wire that is X in simulation may be deterministically 0 or 1 in real silicon, hiding the bug. Never assume your design is correct because it passes with X-producing initial conditions; always ensure resets drive every register to a known state.

2. logic vs bit vs reg vs wire

This is the most common interview distinction in SystemVerilog. Understanding when to use each type prevents subtle bugs.

TypeStatesDriversUse Case
logic4 (0,1,X,Z)Single driver onlyRTL ports, internal signals — replaces reg and wire for single-driver nets
bit2 (0,1)Single driverTestbench variables, transaction fields — faster simulation, no X/Z overhead
reg4 (legacy)Procedural onlyVerilog-2001 holdover — use logic instead in SV
wire4Multiple drivers OKNets with multiple drivers (tri-state buses, module interconnects with wired-OR)

Rule of thumb: Use logic for everything in RTL. Use wire only when you intentionally have multiple drivers. Use bit in testbench code and class-based UVM components for simulation performance. Never use reg in new SystemVerilog code.

The silent X-to-0 conversion in bit

The most dangerous property of bit is that assigning X or Z silently resolves to 0 with no warning in most simulators. This matters when crossing from an RTL logic signal into a testbench bit variable — an uninitialized X in RTL appears as a clean 0 in the testbench, masking the initialization bug entirely.

logic my_logic;         // starts as X (uninitialized)
bit   my_bit;            // starts as 0 (always)

my_bit = my_logic;      // X silently converted to 0 — BUG HIDDEN

if (my_bit === 1'b0)    // passes even though logic was X
  $display("looks fine"); // WRONG — bug masked by bit type

3. Integer Data Types

SystemVerilog adds C-style fixed-width integers for verification code. Unlike logic vectors, these are always 2-state (no X/Z) and have defined signed/unsigned behavior.

TypeWidthSigned?RangeCommon Use
byte8 bitsYes (signed)−128 to 127Short protocol fields, array indices
shortint16 bitsYes−32768 to 32767Counters, offsets
int32 bitsYes−2^31 to 2^31−1Loop variables, random seeds
longint64 bitsYes−2^63 to 2^63−1Timestamps, large counters
byte unsigned8 bitsNo0 to 255Byte data, AXI data beats
int unsigned32 bitsNo0 to 2^32−1Memory addresses, packet lengths
time64 bitsNo (unsigned)0 to 2^64−1Simulation timestamps ($time)

Signed vs Unsigned wrap-around

Signed arithmetic wraps at the MSB boundary. Adding 1 to a byte value of 127 (binary 0111_1111) sets the sign bit, giving −128 — not 128. This wraps back into the signed range instead of overflowing. For a byte unsigned (or bit [7:0]), 255 + 1 = 0 with an implicit carry that is simply discarded.

byte          s = 127;      // signed: max positive value
byte unsigned u = 255;      // unsigned: max value

s = s + 1;  // s = -128  (sign bit wraps — Overflow!)
u = u + 1;  // u = 0     (carry discarded — Wrap-around)

// Detecting overflow
int wide = s + 1;          // promote before overflow: wide = 128
if (wide > 127) $display("overflow detected");

4. Packed vs Unpacked Arrays

Arrays in SystemVerilog come in two fundamentally different forms that affect how memory is laid out and what operations are valid.

Packed arrays

Dimensions declared before the variable name. All bits are contiguous in memory — the entire array is a single vector that you can part-select, slice, and apply bitwise operations to as if it were one big integer.

logic [31:0] data;           // 32-bit packed vector
logic [3:0][7:0] bytes;      // 4 bytes packed into 32 bits

data[15:8] = 8'hAB;          // valid: part-select on packed
bytes[2]   = 8'hFF;          // valid: access byte 2
bytes[2][3] = 1'b1;          // valid: bit 3 of byte 2

if (data == 32'hDEAD_BEEF)  // compare entire 32-bit word
  ...

Unpacked arrays

Dimensions declared after the variable name, like C arrays. Each element is a separate storage location. Bit-slicing across element boundaries is not allowed, but you can assign entire arrays element-by-element or use foreach loops.

logic mem [0:255];           // 256 separate 1-bit logic values
int   queue[1024];           // 1024 separate 32-bit integers
byte  pkt [0:63];           // 64 separate signed bytes

pkt[0] = 8'hFF;              // valid: element access
// pkt[0][3] = 1'b1;         // INVALID: cannot bit-select element of byte array

foreach (queue[i]) queue[i] = i * 4; // initialize each element

5. Special Types: enum, struct, union

SystemVerilog adds abstract data types that make RTL and testbench code self-documenting and catch illegal value assignments in simulation.

enum — Named States

Enumerations associate symbolic names with integer values. The simulator can warn when a variable holds an out-of-range value. Essential for FSM state encoding.

typedef enum logic [1:0] {
  IDLE = 2'b00,
  BUSY = 2'b01,
  DONE = 2'b10,
  ERR  = 2'b11
} state_t;

state_t cur_state, nxt_state;

always_ff @(posedge clk)
  cur_state <= nxt_state;

always_comb
  unique case (cur_state)
    IDLE: nxt_state = wr_en ? BUSY : IDLE;
    BUSY: nxt_state = done  ? DONE : BUSY;
    default: nxt_state = ERR;
  endcase

struct — Grouped Fields

Structures bundle multiple signals into a named record, ideal for AXI channel signals or packet headers.

typedef struct packed {
  logic [31:0] addr;
  logic [3:0]  strb;
  logic        valid;
  logic        ready;
} axi_aw_t;

axi_aw_t aw_chan;
aw_chan.addr  = 32'hC000_0000;
aw_chan.valid = 1;
aw_chan.strb  = 4'hF;

6. Type Casting and Conversion

SystemVerilog provides explicit cast operators to convert between types. Implicit conversion can hide bugs — use explicit casts when crossing type boundaries.

int   i_val  = -1;
logic [31:0] u_val;

// Implicit: -1 (32'hFFFFFFFF) assigned to logic — works, but intent unclear
u_val = i_val;

// Explicit cast — intent is clear
u_val = logic[31:0]'(i_val);     // = 32'hFFFFFFFF

// Static cast: change width
logic [7:0] byte_val = 8'(i_val); // truncate to 8 bits = 8'hFF

// $cast: for enum — required when assigning from int to enum
state_t s;
if (!$cast(s, 2)) $error("invalid enum value");

7. RTL Best Practices

Interactive Simulation Lab
Drive 4-state values into logic and bit registers to see how each type responds. Then explore signed/unsigned integer interpretation and overflow wrap-around.

4-State Logic Visualizer

Select a value to shift into both registers:

logic [3:0] — 4-state, preserves X/Z
bit [3:0] — 2-state, X/Z → 0
> Initial: logic=X, bit=0

Integer Overflow Lab

0127255
01100100
100
MSB highlighted in signed mode

Frequently Asked Questions

logic is 4-state: it can hold 0, 1, X, or Z. bit is 2-state: only 0 or 1. Assigning X or Z to a bit silently resolves to 0 — no warning, no error. Use logic for RTL (you want X-propagation to expose bugs). Use bit in testbenches where speed matters and X is not meaningful.
In simulation, X means unknown — the simulator propagates it through gates so you can see where incorrect logic might be used. In synthesized hardware, X is treated as don't-care by the synthesizer, which assigns 0 or 1. So a bug that produces X in simulation may produce a deterministic (and wrong) 0 or 1 in silicon, making it hard to reproduce in hardware.
Packed arrays (logic [7:0] a) store all bits contiguously — you can bit-select, part-select, and apply bitwise operators across the whole vector. Unpacked arrays (logic a [7:0]) are like C arrays — separate storage elements that cannot be part-selected across boundaries but can be iterated with foreach. Packed arrays map directly to hardware wires; unpacked arrays are primarily a software-side organization structure.
Signed integers use two's complement. A byte stores values from −128 to 127. The binary pattern 0111_1111 (127) plus 1 gives 1000_0000. In two's complement, a set MSB means negative, and 1000_0000 = −128. This is integer overflow — arithmetic wrapped around. To detect it, promote to a wider type before the operation: int wide = byte_val + 1;
Always use always_ff for flip-flop inference, always_comb for combinational logic, and always_latch for latches in SystemVerilog. The specialized keywords are not just style — the synthesizer and simulator check that your code matches the declared intent. always_comb automatically includes all right-hand-side signals in the sensitivity list. Plain always is legal but gives up these checks.
The === operator compares all four logic states — including X and Z — exactly. logic a = 1'bX; if (a === 1'bX) is true. The regular == operator returns X if either operand is X or Z, and any comparison with X evaluates to X (which is treated as false in conditionals). Use === in testbenches when you want to check whether a signal is X or Z explicitly.

SystemVerilog Types in Professional RTL Design

How Type Selection Affects Synthesis Quality

The choice between logic, bit, integer, and int is not merely a stylistic decision — it has measurable impact on the synthesized netlist. Using int (signed, 32-bit) instead of logic [31:0] (unsigned) in arithmetic expressions causes the synthesizer to insert sign extension logic when the result feeds a wider bus, adding gates that would be unnecessary with explicit unsigned arithmetic. More critically, using logic everywhere (including in testbench random stimulus) preserves X-propagation through simulation, which means a uninitialized register will produce X values that propagate visibly through the logic cone — rather than silently resolving to 0 as bit would. This X-propagation is not a simulation artifact; it is a deliberate diagnostic tool that reveals initialization sequences that depend on reset correctness. A design that exits reset with Xs on key control signals has a real hardware initialization vulnerability, even if it "works" in simulation because the simulator initialized the flip-flops to 0 by default.

Enum Encoding Styles and DFT Implications

SystemVerilog enums default to int encoding (2-state, 32-bit). For RTL synthesis, you should always explicitly constrain enum size and encoding. The most common FSM encoding styles are binary (compact, easy to debug), one-hot (fast — only one flip-flop is ever 1, so the next-state logic reduces to OR gates), and Gray code (only one bit changes per transition, which can reduce glitch energy on state buses). The synthesizer can infer one-hot encoding with a synthesis attribute (/* synthesis syn_encoding="one-hot" */ in Synopsys DC) even when the RTL uses binary enum values. For DFT (Design for Testability), one-hot encoding introduces a structural problem: in a k-state FSM with k flip-flops, 2^k − k illegal states exist (those where more than one bit is set). The scan test cannot guarantee these illegal states are never visited during shift, so the RTL must include an always_comb reset to a known safe state for any illegal encoding. Failing to handle this causes stuck-at fault tests to falsely assert errors on state transitions that the RTL engineer never intended to be reachable.

Struct Usage in AXI and Protocol Interface Bundles

One of the most impactful uses of SystemVerilog structs in production RTL is bundling protocol interface signals into a single typed object. An AXI4 write address channel has 10+ signals: AWID, AWADDR, AWLEN, AWSIZE, AWBURST, AWLOCK, AWCACHE, AWPROT, AWQOS, AWVALID, AWREADY. Passing these as individual ports to every module in the hierarchy requires duplicating the port list at every level. A packed struct named axi4_aw_t reduces the port list to a single signal, enables bitwise operations across the whole channel for pipeline registers, and provides named field access for readability. More importantly, a struct-typed port enforces a single point of definition — if the AXI specification changes (say, AWUSER is added), the struct is updated once and all modules see the change automatically. The struct approach is standard practice at companies that design AXI-connected subsystems, and the EcrioniX AXI4 articles use struct-typed interfaces throughout their RTL examples.

X-Propagation Analysis at Gate Level

After synthesis, the gate-level netlist must pass X-propagation analysis before tapeout. In gate-level simulation, every flip-flop starts in state X unless the reset sequence drives it to a known value. A clean reset sequence should clear all Xs within a bounded number of clock cycles. X-propagation analysis (using tools like Mentor Xcelium with X-prop mode or Synopsys VCS with XPROP pragma) applies pessimistic X-propagation rules: a 2-input AND gate with one X input produces X on the output (not 0), because in real hardware the unresolved input could be anything. This pessimism catches cases where the reset controller relies on the simulator's default 0 initialization to "suppress" Xs that would actually corrupt state in silicon. Production VLSI flows require that the design reaches a fully-known state within 32 clock cycles of reset assertion — a timing constraint that goes into the design specification and is verified at gate level before RTL freeze. RTL engineers who understand 4-state logic and X-propagation write reset sequences that satisfy this requirement by design, rather than discovering it as a post-synthesis bug.