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
| State | Symbol | Meaning | When you see it | Drives to hardware? |
|---|---|---|---|---|
| Logic 0 | 1'b0 | Driven low | Normal driven low signal | Yes — GND |
| Logic 1 | 1'b1 | Driven high | Normal driven high signal | Yes — VDD |
| Unknown | 1'bX | Unknown/uninitialized | Before reset, multi-driver conflict, uninitialized arrays | No — simulation-only concept |
| High-Z | 1'bZ | Disconnected / tri-state off | Tri-state buffers, undriven nets, bus interfaces | Models 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
// ── 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| Type | States | Default init | Driven by | Use in | Synthesizable |
|---|---|---|---|---|---|
logic | 4 (0,1,X,Z) | X | assign, always, ports | RTL, TB interfaces | Yes |
wire | 4 (0,1,X,Z) | Z | assign, ports only | Verilog RTL, nets | Yes |
reg | 4 (0,1,X,Z) | X | always, initial only | Legacy Verilog RTL | Yes |
bit | 2 (0,1) | 0 | assign, always, ports | TB internals only | Yes (no X/Z) |
Visual — X Propagation, Initialization, and Signal State
Initialization State at Time 0
| Declaration | Time 0 value | Bit pattern | What the waveform shows |
|---|---|---|---|
logic [7:0] l | 8'hXX | XXXX XXXX | Red/undefined bar — tools show "X" in red |
wire [7:0] w | 8'hZZ | ZZZZ ZZZZ | Mid-level Z bar (floating) |
bit [7:0] b | 8'h00 | 0000 0000 | Clean zero — looks like a valid driven value |
reg [7:0] r | 8'hXX | XXXX XXXX | Same as logic — X at start |
Sampling an X-valued DUT output into logic versus bit
8 cyclesThe 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:
| Expression | a value | b value | Result | Why |
|---|---|---|---|---|
a & b | X | 1 | X | X AND 1 = X (could be 0 or 1) |
a & b | X | 0 | 0 | X AND 0 = 0 (always 0 regardless) |
a | b | X | 0 | X | X OR 0 = X (could be 0 or 1) |
a | b | X | 1 | 1 | X OR 1 = 1 (always 1 regardless) |
a == b | X | 0 | X | Unknown comparison — neither true nor false |
a === b | X | X | 1 | Case equality: X matches X exactly |
if (a) | X | — | else branch | An x or z condition is treated as false, so the else — if present — is executed |
What Happens When bit Receives X From logic
| Operation | logic source value | bit destination | Effect |
|---|---|---|---|
bit_var = logic_var | 8'hXX (X) | 8'h00 (0!) | X is silently converted to 0 — bug hidden |
bit_var = logic_var | 8'hZZ (Z) | 8'h00 (0!) | Z is silently converted to 0 — bug hidden |
logic_var = bit_var | — | 0 | Safe — 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
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
endmoduleExpected 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): 00Example 2 — Intermediate: X Propagation Through Combinational Logic
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
endmoduleExample 3 — Verification: The Scoreboard Bug That bit Hides
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
endmoduleExample 4 — Corner Case: Multiple Drivers on wire vs logic
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
endmoduleSimulation 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.
| Construct | Condition is X | What happens | Risk |
|---|---|---|---|
if (x_sig) | Yes | Neither branch executes | High — silent skip |
case (x_sig) | Yes | Default branch if present, else no match | Medium |
casex (x_sig) | Yes | X matches any value in case items — may execute wrong branch | High |
a !== b (case ineq) | Either | Always 0 or 1 — X is a literal value in case operators | Safe — use this |
a != b (logic ineq) | Either | May return X — if used in if, both branches skip | High — 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
// ── 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
endclassReal Bugs — The Ones That Cost Days to Find
Bug 1 — Scoreboard Passes During Reset Because of bit Reference Model
// 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
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
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
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");
endcaseA 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.
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
endmoduleSection 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.
A reset checker passed for a year on a block whose reset never reached half its flops
BIT-HIDES-XA 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.
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")
endtaskRe-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.
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.
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")
endtaskThe 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 thatregandlogicdenote the same type withlogicbeing 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
elsebranch — 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 case | Recommended type | Reason |
|---|---|---|
| RTL register (flip-flop) | logic [N:0] | X init models pre-reset state accurately |
| RTL combinational output | logic [N:0] | Unified — works with assign and always_comb |
| Tri-state bus / multi-driver | wire [N:0] | Only wire supports multi-driver resolution |
| TB interface signal | logic [N:0] | X reveals undriven signals in TB startup |
| Scoreboard reference model | logic [N:0] | X means "not computed yet" — honest modeling |
| TB counter / loop variable | int or bit | Faster simulation, X never meaningful here |
| rand class data field | bit or logic | Either 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
logiceverywhere in RTL. It unifiedregandwire(for single-driver signals). X initialization is a debugging feature, not a problem. - Keep
wirefor multi-driver nets. Tri-state buses and open-drain configurations need driver resolution — onlywireprovides that. - Use
logicfor all TB interface signals. The X at startup reveals undriven DUT connections. Replacing withbitmasks 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