Part 1 · Language Foundations · Intermediate
4-State vs 2-State Types
logic/reg/wire vs bit/byte/int — X/Z representation cost, initialization semantics, and where each family belongs.
What 4-state actually buys you
A 4-state type (logic, reg, wire, integer) stores each bit as one of 0, 1, X, Z . X means the simulator does not know the value — an uninitialized flop, a bus conflict, a don't-care from synthesis. Z means high-impedance — nothing is driving the net. These two extra states are not decoration: they are how simulation models the physical reality that silicon powers up in an unknown state and that tri-state buses can float.
A 2-state type (bit, byte, shortint, int, longint) stores only 0 or 1 per bit. The simulator packs them two bits-of-state-per-bit cheaper than 4-state: each 4-state bit needs two physical bits of host memory (one value plane, one control plane for X/Z), while a 2-state bit needs one. On million-element scoreboards and long queues, 2-state types roughly halve memory and speed up every comparison and copy — which is why testbench class fields default to them.
Initialization differs in a way that matters for debug: 4-state variables start at X , 2-state variables start at 0 . An X screams "nobody assigned me" in a waveform. A 0 looks like a perfectly legal value, so a forgotten assignment to an int field sails through checks silently. Interviewers regularly probe exactly this: "what is the reset value of a bit vs a logic, and why does it matter?"
Legend: [TYPE]
PER-BIT STORAGE [TYPE]
4-state bit (logic) 2-state bit (bit)
┌───────────┬───────────┐ ┌───────────┐
│ value bit │ ctrl bit │ │ value bit │
├───────────┼───────────┤ ├───────────┤
│ 0 0 │ → 0 │ │ 0 → 0 │
│ 1 0 │ → 1 │ │ 1 → 1 │
│ 0 1 │ → Z │ └───────────┘
│ 1 1 │ → X │ init value: 0
└───────────┴───────────┘ memory: 1 bit
init value: X ops: native CPU words
memory: 2 bits
ops: dual-plane logic (slower)Where each family belongs
The placement rule almost every methodology converges on: anything that touches the DUT is 4-state; anything internal to the class-based testbench is 2-state . RTL needs X so that reset bugs, don't-care abuse, and bus conflicts are visible. The testbench transaction's addr field never legitimately holds X — by the time the monitor builds a transaction it should have checked the bus for X — so paying the 4-state cost there is waste.
// RTL: 4-state, X-propagating
module dff (
input logic clk, rst_n,
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) q <= '0;
else q <= d; // if d is X, q becomes X — visible bug
end
endmodule
// Testbench: 2-state class fields
class bus_txn;
rand bit [31:0] addr; // 2-state: fast, init 0
rand bit [31:0] data;
rand bit write;
int unsigned delay; // loop math, never X
endclassConversion at the boundary
Assigning 4-state to 2-state silently maps X and Z to 0 (in most simulators; the LRM says X/Z convert to 0 for 2-state types). That is the single most dangerous implicit conversion in the language: the monitor copies an X-laden bus into a bit [31:0] transaction field, the X becomes a clean-looking 0, and the scoreboard compares garbage against the reference model without complaint. Guard the boundary with an explicit $isunknown check before the copy.
// Monitor sampling: guard the 4-state → 2-state copy
task sample_bus();
logic [31:0] raw_addr;
bus_txn t = new();
raw_addr = vif.cb.addr; // 4-state sample
if ($isunknown(raw_addr))
`uvm_error("MON", $sformatf("X/Z on addr: %h", raw_addr))
t.addr = raw_addr; // X→0 happens HERE
t.data = vif.cb.data; // unguarded: X vanishes
endtaskBoundary rules
Sample DUT pins into 4-state locals first; check with $isunknown before copying to 2-state fields.
Driving direction is safe: 2-state values are always valid 4-state values.
Never declare DUT-facing interface signals as bit — you blind the testbench to X.
If a scoreboard mismatch shows a suspicious 0, ask whether it was born as X upstream.
Key takeaways
4-state costs two host bits per design bit; 2-state costs one and runs faster — use 4-state only where X/Z is meaningful.
4-state initializes to X (loud), 2-state to 0 (silent) — the X is a feature for catching missing assignments.
RTL and DUT-facing signals: logic/wire. Class-based TB internals: bit/int.
4-state → 2-state conversion turns X into 0 silently — check $isunknown at every monitor boundary.
Common pitfalls
Declaring interface or port signals as bit — X from the DUT is converted to 0 before the TB can see it.
Using int for a field compared against DUT output — a forgotten assignment reads as legal 0, not obvious X.
Assuming logic is 'the new reg' only — it is also usable where wire was, as long as there is a single driver.
Copying vif signals straight into 2-state transaction fields without $isunknown — the classic hidden-X bug.