Skip to content

SystemVerilog · Module 2

4-State vs 2-State Types

logic, bit, reg, wire — X/Z propagation, simulation behavior, synthesis implications.

Module 2 · Page 2.1

The Choice That Shapes Everything Downstream

Walk into any experienced verification engineer's codebase and you'll notice a pattern: RTL signals are declared with logic, testbench counters and loop variables use int or bit, and nobody touches reg anymore — not because it doesn't work, but because logic made it redundant in 2005.

The real question engineers struggle with isn't syntax — it's the philosophical split between four-state and two-state simulation. A logic signal can hold X or Z. A bit signal cannot. That sounds like a trivial detail until you're staring at a scoreboard that passes every single check during reset, and then you realize your reference model was declared as bit: it initialized to 0 instead of X, masked the uninitialized DUT output, and never flagged a real hardware bug.

This tutorial cuts through every misconception — the reg vs logic confusion that's been around since Verilog-1995, the wire restrictions that still trip engineers moving from Verilog to SV, and the very specific scenarios where choosing bit over logic is the right call vs. where it actively hides RTL bugs.

Four Values vs Two — Why the Difference Matters in Real Hardware

Real hardware only ever operates at 0 or 1. But between power-on and the first valid clock edge, every flip-flop in a real chip is in an unknown state. Simulation exists to model that reality. The four-state model — 0, 1, X, Z — gives you that modeling capability. X means "unknown": could be 0, could be 1, the simulator doesn't know. Z means "high-impedance": the wire is disconnected or driven by a disabled tri-state buffer.

Two-state types skip this entirely. A bit variable can only hold 0 or 1. It initializes to 0 automatically. There is no X, no Z. This makes simulation faster — the simulator has fewer cases to track. But that speed comes at a cost: if your DUT is outputting X because a flip-flop hasn't been reset yet, and your reference model is declared bit, the comparison doesn't see X — it sees 0. The bug walks right through.

The Four Logic States

StateSymbolMeaningWhen you see itDrives to hardware?
Logic 01'b0Driven lowNormal driven low signalYes — GND
Logic 11'b1Driven highNormal driven high signalYes — VDD
Unknown1'bXUnknown/uninitializedBefore reset, multi-driver conflict, uninitialized arraysNo — simulation-only concept
High-Z1'bZDisconnected / tri-state offTri-state buffers, undriven nets, bus interfacesModels tri-state buffer

The Four Key Types at a Glance

logic

4-state. The universal replacement for both reg and wire in SV. Can be driven from procedural blocks OR continuous assignment. Initializes to X. Use everywhere by default — see procedural blocks for how the driving block, not the declaration, decides what hardware is inferred.

bit

2-state. Holds only 0 or 1. Initializes to 0 automatically. Faster simulation. Use for TB counters, loop variables, and flags — never for signals connected to hardware.

reg

Legacy Verilog 4-state. Identical to logic in behavior but implies "driven from always block only." Deprecated in SV — use logic instead. You'll see it in older RTL.

wire

4-state net. Must be driven by continuous assignment or port connection. Cannot be driven from always/initial blocks alone. Still used in Verilog-style RTL. In SV, logic handles both roles.

Syntax, Declarations, and Simulator Rules

SystemVerilog — Type Declaration Syntax
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 4-STATE TYPES ────────────────────────────────────────────────
logic              single_bit;     // 1-bit 4-state — default X at start
logic [7:0]         byte_sig;       // 8-bit 4-state vector
logic [31:0]        word_sig;       // 32-bit 4-state
logic signed [7:0] s_byte;         // signed 4-state byte
 
// wire — driven by assign or port; cannot be driven by always/initial alone
wire               net_a;          // single bit net — default value Z (undriven)
wire [7:0]          bus_a;          // 8-bit net
 
// reg — legacy: same as logic for simulation, avoid in new code
reg  [7:0]          old_style;      // avoid in SV — use logic
 
// ── 2-STATE TYPES ────────────────────────────────────────────────
bit                flag;           // 1-bit 2-state — initializes to 0
bit  [7:0]          byte_2s;        // 8-bit 2-state vector
bit  [31:0]         word_2s;        // 32-bit — initializes to 0
bit  signed [7:0] sb;              // signed 2-state byte
 
// ── KEY BEHAVIORAL DIFFERENCE ────────────────────────────────────
logic [7:0] l;  // l = 8'hxx  at time 0 (X)
bit   [7:0] b;  // b = 8'h00  at time 0 (0)
 
// ── logic in RTL: driven from assign OR always block (SV advantage)
logic [7:0] out;
assign out = in_a & in_b;         // OK: continuous assign
 
logic [7:0] reg_out;
always_ff @(posedge clk)          // OK: procedural assign — this was only valid
  reg_out <= data_in;              // for 'reg' in Verilog, now 'logic' works too
 
// ── wire restriction: cannot be driven from initial/always alone
wire [7:0] w;
// always_comb w = x;  ← ILLEGAL for wire — use logic instead
assign w = some_signal;           // OK: continuous assignment
TypeStatesDefault initDriven byUse inSynthesizable
logic4 (0,1,X,Z)Xassign, always, portsRTL, TB interfacesYes
wire4 (0,1,X,Z)Zassign, ports onlyVerilog RTL, netsYes
reg4 (0,1,X,Z)Xalways, initial onlyLegacy Verilog RTLYes
bit2 (0,1)0assign, always, portsTB internals onlyYes (no X/Z)

Visual — X Propagation, Initialization, and Signal State

Initialization State at Time 0

DeclarationTime 0 valueBit patternWhat the waveform shows
logic [7:0] l8'hXXXXXX XXXXRed/undefined bar — tools show "X" in red
wire [7:0] w8'hZZZZZZ ZZZZMid-level Z bar (floating)
bit [7:0] b8'h000000 0000Clean zero — looks like a valid driven value
reg [7:0] r8'hXXXXXX XXXXSame as logic — X at start

Sampling an X-valued DUT output into logic versus bit

8 cycles
Across the reset window the DUT output is X; the logic copy shows X while the bit copy shows zero, and the two agree only after reset completesDUT drives X; the logic copy shows it, the bit copy already reads 00DUT drives X; the logiccopy shows it, the bit copyalready reads 00a check on the bit copy compares 00 against 00 and reports a matcha check on the bit copycompares 00 against 00 andreports a matchreset completes - from here the two copies agree foreverreset completes - from herethe two copies agreeforeverclkrst_ndut_outXXXXXX002A2A3C3Clogic copyXXXXXX002A2A3C3Cbit copy000000002A2A3C3CX visiblet0t1t2t3t4t5t6t7
Figure 1 — the same DUT output sampled into a logic variable and into a bit variable, across reset. Before reset completes the DUT drives X. The logic copy holds X, so a check on it fires and the missing reset is visible. The bit copy holds 0 from time zero and continues to hold 0 while the DUT is driving X, because assigning a 4-state value carrying X into a 2-state variable silently converts it — so the bit copy is indistinguishable from a correctly reset signal. After reset the two agree for the rest of the simulation, which is why the divergence is only ever visible in the reset window and only if something looks there.

The window in which the two types disagree is narrow, and that is exactly the problem: it is the reset window, which is the one place a missing reset could have been caught.

X Propagation — How Unknown Spreads

When a logic signal is X, any operation involving it typically propagates X through the result. This is simulation's way of telling you: "I can't determine the output because an input is unknown." The key cases:

Expressiona valueb valueResultWhy
a & bX1XX AND 1 = X (could be 0 or 1)
a & bX00X AND 0 = 0 (always 0 regardless)
a | bX0XX OR 0 = X (could be 0 or 1)
a | bX11X OR 1 = 1 (always 1 regardless)
a == bX0XUnknown comparison — neither true nor false
a === bXX1Case equality: X matches X exactly
if (a)Xelse branchAn x or z condition is treated as false, so the else — if present — is executed

What Happens When bit Receives X From logic

Operationlogic source valuebit destinationEffect
bit_var = logic_var8'hXX (X)8'h00 (0!)X is silently converted to 0 — bug hidden
bit_var = logic_var8'hZZ (Z)8'h00 (0!)Z is silently converted to 0 — bug hidden
logic_var = bit_var0Safe — 0 is a valid 4-state value

This silent X→0 conversion is the core danger of using bit for interface signals. A DUT outputting X looks like it's outputting 0 to your reference model. The unknowns themselves usually originate in the places catalogued in where X comes from, and they behave differently once the design is a netlist — see RTL versus netlist behaviour. For the comparison operators that do and do not propagate X, see case, casex and casez.

Code Examples — From Basics to Verification Traps

Example 1 — Beginner: Initialization Difference

Example 1 — logic vs bit Initialization
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_init_comparison;
 
  logic [7:0] l_val;      // 4-state: starts as X
  bit   [7:0] b_val;      // 2-state: starts as 0
  wire  [7:0] w_val;      // net: starts as Z (undriven)
 
  initial begin
    // At time 0, before anything is driven:
    $display("logic at t=0: %h",  l_val);   // xx
    $display("bit   at t=0: %h",  b_val);   // 00
    $display("wire  at t=0: %h",  w_val);   // zz
 
    // Use === (case equality) to detect X and Z reliably
    $display("l_val is X: %0b",  l_val === 8'hxx);  // 1 — correctly detected
    $display("b_val is X: %0b",  b_val === 8'hxx);  // 0 — no X possible
    $display("w_val is Z: %0b",  w_val === 8'hzz);  // 1 — undriven net
 
    // X in if condition — the silent false behavior
    if (l_val)
      $display("logic: if-branch");
    else
      $display("logic: else-branch");
    // Neither prints when l_val is X!
    $display("(nothing printed above — X condition skips both branches)");
 
    // Assign X-valued logic to bit: silent conversion
    b_val = l_val;
    $display("b_val after = l_val (was X): %h", b_val);  // 00 — X became 0!
 
    $finish;
  end
 
endmodule

Expected output:

Simulation Output
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic at t=0: xx
bit   at t=0: 00
wire  at t=0: zz
l_val is X: 1
b_val is X: 0
w_val is Z: 1
(nothing printed above — X condition skips both branches)
b_val after = l_val (was X): 00

Example 2 — Intermediate: X Propagation Through Combinational Logic

Example 2 — X Propagation Modeling
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_x_propagation;
 
  logic [7:0] a, b, result;
 
  initial begin
    // Propagation through AND
    a = 8'hXX; b = 8'hFF;
    result = a & b;
    $display("XX & FF = %h", result);    // xx — X propagates
 
    a = 8'hXX; b = 8'h00;
    result = a & b;
    $display("XX & 00 = %h", result);    // 00 — AND with 0 dominates
 
    // Propagation through OR
    a = 8'hXX; b = 8'h00;
    result = a | b;
    $display("XX | 00 = %h", result);    // xx — OR with 0 propagates X
 
    a = 8'hXX; b = 8'hFF;
    result = a | b;
    $display("XX | FF = %h", result);    // ff — OR with 1 dominates
 
    // Partial X — some bits unknown, others masked
    a = 8'b1111_XXXX; b = 8'hF0;
    result = a & b;
    $display("1111XXXX & F0 = %b", result); // 1111_0000 — lower X bits ANDed with 0
 
    a = 8'b1111_XXXX; b = 8'hFF;
    result = a & b;
    $display("1111XXXX & FF = %b", result); // 1111_xxxx — X bits survive
 
    // === vs == for X detection
    a = 8'hXX;
    $display("a == 8'hXX : %0b", a == 8'hXX);  // X — logical == propagates X
    $display("a === 8'hXX: %0b", a === 8'hXX); // 1 — case equality detects X
 
    $finish;
  end
 
endmodule

Example 3 — Verification: The Scoreboard Bug That bit Hides

Example 3 — Scoreboard Type Selection Matters
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_scoreboard_types;
 
  logic [7:0] dut_output;    // DUT output — starts as X (no reset yet)
  bit   [7:0] ref_model_bad; // WRONG: bit reference — initializes to 0
  logic [7:0] ref_model_ok;  // CORRECT: logic reference — initializes to X
 
  initial begin
    // Simulate pre-reset: DUT hasn't been reset, output is X
    // dut_output is never assigned — stays X
 
    // DANGEROUS CHECK: bit reference masks the bug
    if (dut_output !== ref_model_bad)
      $error("BAD_SB MISMATCH");      // fires? Let's check...
    // dut_output = XX, ref_model_bad = 00
    // XX !== 00 → TRUE → $error fires... but only because !== sees X
    // Now what if someone uses == instead of !==?
 
    if (dut_output == ref_model_bad)
      $display("BAD_SB: looks like PASS");   // XX == 00 evaluates to X
    else
      $display("BAD_SB: reported a mismatch");
    // An x/z condition is treated as FALSE, so the ELSE branch runs. It is not
    // that "neither branch executes" - the else does, which is exactly why an
    // X can slip through a check written the wrong way round:
    //     if (mismatch) $error(...);          <- X condition -> no error
    // With == against a `bit` reference, a DUT output of X compares as X,
    // the condition is false, and the mismatch is never reported.
 
    // CORRECT CHECK: logic reference, use !== for case inequality
    if (dut_output !== ref_model_ok)
      $error("GOOD_SB MISMATCH: DUT output is X — no reset?");
    // Both are X → XX !== XX → FALSE → no error
    // But we should still check for X explicitly!
    if (^dut_output === 1'bX)
      $error("DUT output contains X — check reset sequence");
 
    $finish;
  end
 
endmodule

Example 4 — Corner Case: Multiple Drivers on wire vs logic

Example 4 — Multi-Driver Resolution
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module tb_multi_driver;
 
  wire  shared_wire;        // wire: supports multiple drivers with resolution
  logic shared_logic = 0;  // logic: only ONE continuous driver allowed
 
  // wire: two drivers — resolved using wired-AND/wired-OR table
  assign shared_wire = 1'b0;    // driver 1
  assign shared_wire = 1'b1;    // driver 2 — conflict → X
  // shared_wire = X (both strong drivers conflict)
 
  // Tri-state bus modeled correctly with wire
  logic oe1, oe2, drv1, drv2;
  wire  data_bus;
  assign data_bus = oe1 ? drv1 : 1'bz;  // driver 1: active when oe1=1
  assign data_bus = oe2 ? drv2 : 1'bz;  // driver 2: active when oe2=1
  // When only one oe is active, the bus is cleanly driven
  // When both oe are active with different values → X
  // When neither oe is active → Z (high impedance)
 
  initial begin
    oe1 = 1; drv1 = 1; oe2 = 0; drv2 = 0;
    #1;
    $display("Bus (oe1 only): %b", data_bus);  // 1
 
    oe1 = 0; oe2 = 0;
    #1;
    $display("Bus (no driver): %b", data_bus); // z
 
    oe1 = 1; drv1 = 0; oe2 = 1; drv2 = 1;
    #1;
    $display("Bus (conflict): %b", data_bus);  // x
 
    $finish;
  end
 
endmodule

Simulation Behavior — What the Simulator Actually Tracks

How the Simulator Models 4-State vs 2-State

Internally, a 4-state simulator stores 2 bits per signal bit — one bit for the value (0 or 1) and one bit for the strength/unknown flag (X or Z override). A 2-state simulator stores only 1 bit per signal bit. This is why 2-state simulation can be up to 2× faster and use half the memory — useful for large testbenches with millions of flip-flops. But that efficiency comes from discarding the X/Z tracking entirely.

X in if/case Conditions — The Silent Behavior

An X in an if condition evaluates to neither true nor false — the simulator skips both branches entirely. This is intentional: the simulator is saying "I don't know which branch to take." In practice this means hardware bugs during reset can go completely undetected if your checker depends on an if statement whose condition involves an X signal.

ConstructCondition is XWhat happensRisk
if (x_sig)YesNeither branch executesHigh — silent skip
case (x_sig)YesDefault branch if present, else no matchMedium
casex (x_sig)YesX matches any value in case items — may execute wrong branchHigh
a !== b (case ineq)EitherAlways 0 or 1 — X is a literal value in case operatorsSafe — use this
a != b (logic ineq)EitherMay return X — if used in if, both branches skipHigh — use !== instead

Synthesis: X and Z Don't Exist in Gates

Synthesis tools ignore X and Z entirely — they only care about 0 and 1 logic. When you write logic [7:0] out = 8'hXX in RTL, the synthesizer treats that initial assignment as "don't care" for optimization, potentially choosing any value that minimizes area. This is actually useful for unused states in FSMs — assigning X tells synthesis "optimize freely here." But it means X-initialized signals in RTL give the synthesizer optimization freedom, not a hardware unknown.

Where Type Choice Matters in Real Verification Work

Verification Patterns — Type Selection Guidelines
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── 1. INTERFACE SIGNALS: always logic ────────────────────────────
interface axi_if (input logic clk);
  logic        awvalid, awready;
  logic [31:0] awaddr;           // logic: X at start reveals undriven signals
  logic [7:0]  awlen;
endinterface
 
// ── 2. SCOREBOARD: logic for comparisons, bit for counters ─────────
class axi_scoreboard;
  logic [31:0] exp_data;     // logic: X means "not yet computed"
  logic [31:0] got_data;     // logic: X means "not yet captured"
  int           pass_count;   // int: pure counter, X never meaningful
  int           fail_count;
 
  task check(logic [31:0] exp, got);
    // Step 1: detect X in DUT output — always a bug
    if (^got === 1'bX)
      $error("DUT output contains X");
    // Step 2: use !== for comparison (detects X differences)
    else if (exp !== got)
      fail_count++;
    else
      pass_count++;
  endtask
endclass
 
// ── 3. SVA ASSERTIONS: logic for RTL signals ──────────────────────
// assert property (@(posedge clk) awvalid |-> ##[1:3] awready);
// awvalid is logic — X on awvalid won't trigger the assertion
// Use $isunknown() for explicit X detection in SVA
 
// ── 4. DRIVER: drive using logic values including Z ────────────────
task automatic drive_bus(ref logic [7:0] bus, input bit oe, input logic [7:0] val);
  bus = oe ? val : 8'hzz;    // drive Z when output-enable is low
endtask
 
// ── 5. CONSTRAINTS: bit is fine for rand variables ────────────────
class rand_txn;
  rand bit  [7:0] len;    // bit is fine — constraint solver never generates X/Z
  rand bit  [1:0] btype;
  rand logic [31:0] addr;  // logic fine for rand too — solver won't produce X
endclass

Real Bugs — The Ones That Cost Days to Find

Bug 1 — Scoreboard Passes During Reset Because of bit Reference Model

Bug 1 — bit Reference Model Hides Pre-Reset X
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BUGGY: reference model uses bit — starts at 0
bit [31:0] ref_data;      // = 0 at start
logic [31:0] dut_data;    // = X at start (DUT not yet reset)
 
if (dut_data == ref_data)  // X == 0 → evaluates to X → neither branch!
  $display("PASS");         // doesn't print
else
  $error("FAIL");           // doesn't print either — bug silently passes!
 
// Waveform clue: scoreboard shows no pass/fail events at all during reset
// Engineer assumes "the checker hasn't started yet" — it started, it just found X
 
// FIXED: use logic for reference model, !== for comparison
logic [31:0] ref_data_ok;  // = X at start — matches DUT state honestly
if (^dut_data === 1'bX)     // explicitly detect X first
  $error("DUT output is X — no reset applied yet");
else if (dut_data !== ref_data_ok)
  $error("MISMATCH");

Bug 2 — wire in Procedural Block Causes Compile Error

Bug 2 — wire Cannot Be Driven From always
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module bad_rtl (input logic clk, input logic d, output wire q);
  // BUGGY: wire cannot be driven from always_ff in Verilog
  always_ff @(posedge clk)
    q <= d;   // ERROR: cannot drive wire from procedural block
endmodule
 
// FIXED: use logic (or reg in Verilog)
module good_rtl (input logic clk, input logic d, output logic q);
  always_ff @(posedge clk)
    q <= d;   // OK: logic works with both continuous and procedural
endmodule
 
// Note: many tools silently accept wire in procedural blocks and infer
// a latch. This is a portability bug — always use logic for procedural.

Bug 3 — Using == Instead of !== in Reset Checker

Bug 3 — == vs !== for X-Aware Comparison
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [7:0] dut_out;         // X before reset
logic [7:0] expected = 8'h00;
 
// BUGGY: == propagates X — if condition is X, checker silently skips
if (dut_out == expected)
  pass_count++;      // never executes when dut_out is X
else
  fail_count++;      // never executes either — both branches skipped!
// Symptom: pass_count and fail_count both stay at 0 during reset
 
// CORRECT: !== catches X mismatches; the else branch always fires for X
if (dut_out !== expected)
  fail_count++;      // fires for ANY non-match including X != 00
else
  pass_count++;      // only fires for exact binary match
 
// BEST PRACTICE: check for X explicitly first
if (^dut_out === 1'bX)
  $error("X in DUT output — check reset");
else if (dut_out !== expected)
  $error("Mismatch: exp=%h got=%h", expected, dut_out);

Bug 4 — X on FSM State Causes casex to Match Wrong Branch

Bug 4 — casex With Uninitialized FSM State
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
logic [1:0] state;     // X at start — FSM not yet reset
 
// BUGGY: casex treats X in the expression as wildcard
// state = X means X matches EVERY case item → first match executes!
casex (state)           // dangerous with X in state
  2'b00: $display("IDLE");   // may execute even if state is X!
  2'b01: $display("RUN");
  2'b10: $display("DONE");
  default: $display("UNKNOWN");
endcase
 
// CORRECT: use case (not casex) and add explicit X check
if (^state === 1'bX)
  $display("FSM state is X — reset not applied");
else
case (state)            // plain case: X in state matches nothing except default
  2'b00: $display("IDLE");
  2'b01: $display("RUN");
  2'b10: $display("DONE");
  default: $display("UNKNOWN STATE");
endcase

A Runnable Proof — What Each Type Actually Does With X

Every claim on this page is checkable in about forty lines. This file prints the behaviour rather than describing it, and it is worth running once because two of the results surprise most engineers.

four_state_proof.sv — self-checking demonstration of X handling
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module four_state_proof;
 
  logic [7:0] l_undriven;      // never assigned: stays 8'hXX
  bit   [7:0] b_from_logic;
  logic [7:0] l_from_logic;
  int fails = 0;
 
  task automatic show (input string what, input logic [7:0] v);
    $display("  %-34s = %b (%0s)", what, v,
             $isunknown(v) ? "contains X/Z" : "fully known");
  endtask
 
  task automatic expect (input string what, input bit cond);
    if (cond) $display("  PASS  %s", what);
    else begin $display("  FAIL  %s", what); fails++; end
  endtask
 
  initial begin
    $display("\n1. Initialisation at time zero");
    show("logic [7:0], never assigned", l_undriven);
    show("bit   [7:0], never assigned", b_from_logic);
    expect("logic initialises to X", $isunknown(l_undriven));
    expect("bit   initialises to 0", b_from_logic === 8'h00);
 
    $display("\n2. Assigning an X-valued logic into each type");
    b_from_logic = l_undriven;      // 4-state -> 2-state: X becomes 0
    l_from_logic = l_undriven;      // 4-state -> 4-state: X survives
    show("bit   <= X-valued logic", b_from_logic);
    show("logic <= X-valued logic", l_from_logic);
    // This is the whole danger in one line: the X did not survive the
    // assignment, and nothing warned.
    expect("bit silently converts X to 0", b_from_logic === 8'h00);
    expect("logic preserves X",            $isunknown(l_from_logic));
 
    $display("\n3. Comparison operators against an X");
    // == propagates X; === compares X literally.
    expect("(X == 0) is X, not false",  (l_undriven == 8'h00) === 1'bx);
    expect("(X === 0) is a clean 0",    (l_undriven === 8'h00) === 1'b0);
    expect("(X !== 0) is a clean 1",    (l_undriven !== 8'h00) === 1'b1);
 
    $display("\n4. What an if statement does with an X condition");
    // The most-misremembered rule on this page: x/z is treated as FALSE,
    // so the else branch IS taken. It is not that neither branch runs.
    if (l_undriven == 8'h00) begin
      $display("  if-branch taken");
      expect("if-branch should NOT be taken on an X condition", 1'b0);
    end else begin
      $display("  else-branch taken");
      expect("else IS taken when the condition is X", 1'b1);
    end
 
    $display("\n5. The check that actually catches an unreset signal");
    // Do not rely on a comparison to surface an X - ask directly.
    expect("$isunknown finds the X", $isunknown(l_undriven));
 
    $display("\n%0s (%0d failures)\n", fails == 0 ? "ALL CHECKS PASSED"
                                                  : "CHECKS FAILED", fails);
    if (fails) $fatal(1, "four_state_proof failed");
    $finish;
  end
endmodule

Section 3 is the one to internalise. (X == 0) is neither true nor false — it is X, a third outcome that most code silently rounds down to false. Section 4 then shows what "rounds down to false" actually means: the else runs. Whether that saves you or hides the bug depends entirely on which branch you put the error message in.

1

A reset checker passed for a year on a block whose reset never reached half its flops

BIT-HIDES-X
Symptom

A block-level environment contained an end-of-reset check that had never failed. During SoC integration the same block produced non-deterministic behaviour on the first few transactions after reset — different results on different simulator versions, and different again in gate-level simulation. The block-level regression remained clean throughout, including on the exact stimulus that failed at SoC level.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class dut_state;
  bit [31:0] ctrl;        // ✗ 2-state - an X sampled from the DUT becomes 0
  bit [31:0] status;
  bit [31:0] count;
endclass
 
task automatic check_reset_state();
  dut_state s = new();
  s.ctrl   = vif.ctrl;    // DUT drives X here; s.ctrl receives 0
  s.status = vif.status;
  s.count  = vif.count;
 
  if (s.ctrl != 32'h0)  `uvm_error("RST", "ctrl not reset")
  if (s.status != 32'h0) `uvm_error("RST", "status not reset")
  if (s.count != 32'h0)  `uvm_error("RST", "count not reset")
endtask
Diagnostic Evidence

Re-declaring one field as logic and re-running the existing test produced the error immediately — which localised the problem to the sampling boundary rather than to the reset logic or the check.

The waveform then confirmed it directly: vif.ctrl was 32'hxxxxxxxx at the moment the checker sampled it, while the checker's own s.ctrl read 32'h0. Two different values for the same quantity, one cycle apart, with a plain assignment in between.

The corroborating detail was that the failure at SoC level was simulator-dependent. That is the signature of a design whose behaviour depends on uninitialised state: each tool resolves the unknown differently, so the same RTL produces different answers. A genuine functional bug would have been consistent.

Root Cause

The reset sequence genuinely did not cover several flops — that part was a real design bug. What made it invisible was the checker.

bit has no representation for X, so assigning a 4-state value carrying X into a 2-state variable yields 0. The conversion is defined behaviour, not an error, so nothing warns. By the time the checker's comparison ran, every unreset field had already been converted to exactly the value the checker was looking for. The check was comparing the DUT's unknown state against zero and finding zero, because the type system had supplied the zero.

Two properties of this failure are worth separating. First, the check was not wrong — if (s.ctrl != 32'h0) is a perfectly reasonable thing to write. It was operating on data that had already been corrupted. Second, no comparison operator would have saved it: switching to !== compares against a field that is already 0, so it still passes. The evidence was destroyed one line earlier, at the assignment.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class dut_state;
  logic [31:0] ctrl;      // ✓ 4-state - an X sampled from the DUT survives
  logic [31:0] status;
  logic [31:0] count;
endclass
 
task automatic check_reset_state();
  dut_state s = new();
  s.ctrl = vif.ctrl;  s.status = vif.status;  s.count = vif.count;
 
  // Ask the question directly instead of hoping a comparison surfaces it.
  // $isunknown returns 1 if any bit is x or z, and cannot itself return x.
  if ($isunknown({s.ctrl, s.status, s.count}))
    `uvm_error("RST", $sformatf(
      "unreset bits after reset: ctrl=%h status=%h count=%h",
      s.ctrl, s.status, s.count))
 
  // Only after the X check is a value comparison meaningful.
  if (s.ctrl !== 32'h0) `uvm_error("RST", "ctrl not reset to 0")
endtask

The type change alone makes the existing test fail, which is the direct proof. The $isunknown check is the durable part, because it does not depend on the reset value being zero — a flop that resets to 32'hFFFF_FFFF is equally well covered.

Two habits follow, and they are worth more than this one fix. Sample into 4-state types all the way from the interface through the transaction class to the reference model, converting to bit only after a value has been validated as X-free; the conversion destroys evidence, so it should happen after checking, not before. And check for X at the boundary rather than in the scoreboard — in the monitor, on the sampled value, before it is packed into anything. By the time a value reaches the scoreboard it may have passed through several assignments, and only the first one that narrowed the type had the information.

For the wider consequences of unknowns crossing this boundary see where X comes from, and for the reset-window behaviour that produces them in the first place, RTL versus netlist behaviour.

Interview Questions

logic is a 4-state type holding 0, 1, X or Z, and it initialises to X. bit is a 2-state type holding only 0 or 1, and it initialises to 0.

The practical consequence is what matters. A logic signal that has not been reset shows up as X, which propagates through downstream logic and makes the omission visible. The same signal declared bit shows up as 0 — indistinguishable from a value that was deliberately driven — so a missing reset looks exactly like a working one.

bit simulates faster because the simulator tracks one value bit per bit rather than two, and that is a genuine benefit for testbench bookkeeping: loop counters, array indices, flags, scoreboard IDs. None of those model hardware and none of them can meaningfully be X.

The rule that follows is a scoping rule rather than a preference: use logic for anything that touches the DUT — ports, interface signals, reference-model values compared against DUT outputs — and bit for testbench-internal variables that never carry a hardware value.

Because it means the same thing as logic and communicates the wrong intent. IEEE 1800 states directly that the keyword reg does not accurately describe user intent and that logic is the more descriptive term for the same 4-state variable type.

The name is the problem: reg reads as "register", so generations of engineers have believed that declaring something reg infers a flip-flop and declaring it wire infers combinational logic. Neither is true. What infers a flop is a procedural block with a clock edge in its sensitivity list; the declaration type has nothing to do with it. A reg assigned in an always_comb is combinational, and always was.

logic also carries a small capability reg does not: a logic variable may be driven by a single continuous assignment, so one declaration covers both the procedural and the assign styles and you stop having to choose the declaration based on how you intend to drive it.

Existing reg in legacy code is not a defect and does not need mass conversion. New code should use logic.

The DUT output is X during reset, and == is the logical equality operator, which propagates X: X == anything evaluates to X, not to 0 or 1.

Follow what that does to the check. if (dut_out == expected) with an X condition is treated as false, so the else branch runs — and in a scoreboard written as if (match) pass; else fail; that means the failure is reported. But the far more common shape is if (dut_out != expected) $error(...); with no else, and there the X condition is false, the $error never fires, and the mismatch vanishes silently.

So the precise statement is not that "neither branch executes" — an if/else with an x or z condition always takes the else. It is that an X condition is false, and whichever outcome you attached to the false side is the one you get. If you attached "everything is fine" to it, X reads as success.

The fix is !==, the case-inequality operator, which compares X against X literally and always returns 0 or 1 rather than X. Beyond that, check for X explicitly rather than hoping a comparison surfaces it: if ($isunknown(dut_out)) $error("DUT output contains X"); at the end of reset catches the underlying problem instead of its symptom.

logic is a variable, and a variable may have at most one continuous driver. Two assign statements targeting the same logic, or an assign plus a procedural assignment, is an error rather than a resolution — most tools reject it at elaboration.

wire is a net, and nets exist precisely to be resolved between multiple drivers using the 4-state resolution table: 0 with 1 gives X, 0 with Z gives 0, 1 with Z gives 1, Z with Z gives Z. That is what makes wire the right type for a tri-state bus or an open-collector line, where several devices drive one physical conductor and the contention behaviour is the point.

This is the one place the logic-everywhere advice has a real exception. If a signal genuinely has multiple drivers, it must be a net, and the resolution behaviour you get is a feature you are relying on rather than an accident. Conversely, a compile error about multiple drivers on a logic is usually the tool telling you about a real design mistake — two blocks that both think they own a signal — and changing the declaration to wire to silence it converts a caught bug into a resolved X.

The two simulations disagree about what an un-reset flop contains, and each disagreement can hide the bug in a different direction.

In RTL, an un-reset flop is X. That X propagates and usually makes the omission obvious — but not always, because X propagation is optimistic in places. X & 0 is 0 and X | 1 is 1, so an unknown can be absorbed by downstream logic and never reach an observable output. A multiplexer selected by a known signal passes only the selected input, so an X on the unselected leg disappears entirely. This is X-optimism, and it is why RTL simulation can be clean while a real gap exists.

In gate simulation, those flops hold real values rather than X, so the design behaves deterministically — and deterministically wrong, if the reset never covered them. The bug that RTL optimism absorbed now produces a concrete incorrect value that propagates all the way out.

The fix is not to argue about which simulation is right; it is to stop relying on X propagation as a reset checker. Assert the property directly: at the end of the reset sequence, check $isunknown() on every signal that reset is supposed to have defined. That converts "did the X reach an output" — which depends on the logic in between — into "was this flop reset", which is the question you actually care about. Many teams also run an X-pessimistic simulation mode, which forces X through paths where the optimistic rules would have absorbed it.

At every assignment from a 4-state source. Assigning a logic carrying X or Z into a bit silently yields 0, with no warning, because 2-state types have no representation for either value and the conversion is defined rather than exceptional.

The danger is that it happens at exactly the boundary where you are least likely to look: the point where DUT values enter the testbench. A reference model whose fields are bit, a transaction class with bit [31:0] data, a monitor that samples into a bit variable — each of these turns "the DUT drove X" into "the DUT drove 0" before any check sees it. The scoreboard then compares 0 against 0 and reports a match.

This is why the type choice is a boundary discipline rather than a style preference. Anything that carries a value observed from the DUT should be logic, all the way through the transaction class and the reference model, so that an X survives long enough to be checked. Once a value has been validated as X-free, converting to bit for internal bookkeeping is fine.

The check that catches an existing case: sample the DUT into logic, call $isunknown() on it in the monitor before the transaction is built, and error there. Doing it in the monitor rather than the scoreboard matters, because by the time the value has been packed into a bit field the evidence is already gone.

Where This Is Specified

  • IEEE 1800-2023 §6.11 — Integer data types. The 2-state (bit, byte, int, shortint, longint) and 4-state (logic, reg, integer, time) families, and the statement that reg and logic denote the same type with logic being the more descriptive keyword.
  • IEEE 1800-2023 §6.3.1 — Logic values. The four values 0, 1, x and z, and the rule that assigning x or z to a 2-state variable converts it to 0.
  • IEEE 1800-2023 §11.4.5 — Equality operators. The distinction between logical equality (==, !=), which returns x when either operand contains x or z, and case equality (===, !==), which compares x and z literally and always returns a known result.
  • IEEE 1800-2023 §12.4 — Conditional if-else statement. The rule that a condition evaluating to zero, x or z is treated as false, so the else branch — if present — is executed.
  • IEEE 1800-2023 §20.9 — $isunknown. Returns true if any bit of the expression is x or z; the direct test that avoids relying on comparison operators to surface an unknown.
  • IEEE 1800-2023 §6.6 — Net types. Multiple-driver resolution on nets, and why a variable cannot take that role.

Best Practices — Type Selection Done Right

RTL: always logic

Every signal in synthesizable RTL — ports, registers, combinational outputs — should use logic. No exceptions. This replaces both reg and wire for single-driver signals.

Multi-driver nets: wire

Tri-state buses, open-drain outputs, and any net with multiple continuous drivers must use wire (or tri). Only wire supports driver resolution logic.

TB interfaces: logic

All signals in interfaces, monitors, and drivers connected to DUT ports must be logic. The X initialization is a feature — it reveals undriven signals during startup.

TB internals: bit/int

Loop variables, counters, transaction IDs, timestamps — use int or bit. They're faster, initialize to 0, and X is never meaningful for these values.

Use caseRecommended typeReason
RTL register (flip-flop)logic [N:0]X init models pre-reset state accurately
RTL combinational outputlogic [N:0]Unified — works with assign and always_comb
Tri-state bus / multi-driverwire [N:0]Only wire supports multi-driver resolution
TB interface signallogic [N:0]X reveals undriven signals in TB startup
Scoreboard reference modellogic [N:0]X means "not computed yet" — honest modeling
TB counter / loop variableint or bitFaster simulation, X never meaningful here
rand class data fieldbit or logicEither works — solver never generates X/Z
Scoreboard comparison!== (case ineq)Catches X differences; != silently skips

Summary

The 4-state vs 2-state distinction is not academic — it directly determines whether your simulation catches pre-reset bugs or silently ignores them. logic is the right default for essentially everything in RTL and testbench interfaces. wire remains relevant for multi-driver nets. bit and int are the right choice for internal testbench bookkeeping where X is meaningless. reg is a relic — retire it from your vocabulary.

  • Use logic everywhere in RTL. It unified reg and wire (for single-driver signals). X initialization is a debugging feature, not a problem.
  • Keep wire for multi-driver nets. Tri-state buses and open-drain configurations need driver resolution — only wire provides that.
  • Use logic for all TB interface signals. The X at startup reveals undriven DUT connections. Replacing with bit masks those bugs.
  • Always use !== in scoreboards. The logical != propagates X and causes silent pass-through of real mismatches.
  • Detect X explicitly with ^signal === 1'bX. XOR reduction returns X if any bit is unknown — use this at end-of-reset and in assertions.

Part of SystemVerilog Fundamentals·Data Types·Lesson 5 of 53

View program

Continue learning