Skip to content

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.
  • 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
// ── 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

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)XNeither branchX condition = false in if; neither if nor else

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.

Code Examples — From Basics to Verification Traps

Example 1 — Beginner: Initialization Difference

Example 1 — logic vs bit Initialization
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
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
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
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");  // evaluates to X → treated as false
// Neither branch executes! Silent pass of a real bug.
 
// 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
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
// ── 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
// 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
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
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
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

Interview Questions

Beginner Level

Q1: What is the difference between logic and bit in SystemVerilog?logic is a 4-state type (0, 1, X, Z) that initializes to X in simulation. bit is a 2-state type (0, 1 only) that initializes to 0. logic accurately models hardware pre-reset states. bit is faster to simulate but hides uninitialized conditions. Use logic for anything connected to hardware; use bit for internal testbench variables like counters and flags. Q2: Why is reg considered deprecated in SystemVerilog? In Verilog, reg could only be driven from procedural blocks (always, initial). SystemVerilog introduced logic, which can be driven from both procedural blocks AND continuous assignments (assign statements). Since logic is a strict superset of reg in behavior, reg is redundant. New SV code should always use logic.

Intermediate Level

Q3: A scoreboard compares expected vs DUT output using ==. During reset, no pass/fail events are logged. What is wrong? The DUT output (declared as logic) is X during reset. The logical equality operator == propagates X — so X == expected evaluates to X, which is treated as false in an if condition. Neither the pass branch nor the fail branch executes. The fix: use !== (case inequality) which always returns 0 or 1, never X. Also check for X explicitly with ^signal === 1'bX (XOR reduction — returns X if any bit is unknown). Q4: Can a logic signal have multiple continuous drivers? What about wire?logic supports only a single continuous driver. Assigning it from multiple assign statements causes a compile error or undefined behavior. wire supports multiple drivers and resolves them using the 4-state resolution table (0+1 = X, 0+Z = 0, 1+Z = 1, Z+Z = Z). This makes wire the correct type for tri-state buses and open-collector configurations where multiple drivers share a net.

Experienced Engineer Level

Q5: A synthesis tool and simulator disagree on the behavior of a reset sequence. RTL passes simulation but behaves differently in gate-sim. The RTL uses X-initialized logic signals. Explain why and how to fix it. RTL simulation initializes logic signals to X and X propagates through combinational logic, potentially hiding incomplete reset coverage. Synthesis, however, initializes all flip-flops to a deterministic state (often 0 or whatever is convenient for area optimization) and ignores X. If the reset sequence doesn't cover all flops in RTL simulation, X propagation masks the issue because X AND 0 = 0, etc. But in gate-sim, those un-reset flops have real values (not X), revealing real hardware bugs. Fix: ensure all flip-flops are covered by the reset sequence. Use $isunknown() assertions at the end of reset to detect any remaining X in the design. Enable X-propagation mode in simulation to get maximum coverage of uninitialized state detection.

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.