Modules, Signals, and Numbers
Four-State Logic: 0, 1, X, and Z
What the unknown value X and the high-impedance value Z mean in Verilog simulation, when each one appears, and why == and === treat them differently.
5 min read · Updated
Classic Verilog signals carry one of four values, not two: 0, 1, x (unknown), and z (high-impedance). Most beginner examples only ever see 0 and 1, but x and z appear constantly once a design is simulated realistically — before reset, on an undriven net, or on a bus with no active driver.
x— unknown. The simulator cannot determine whether the real value is 0 or 1. Aregthat has never been assigned reads asx. Whether a gate's output stays known or becomesxwhen one of its inputs isxdepends on the operator and the other input — see "X does not always propagate" below.z— high-impedance. The signal is not being driven by anything right now. This is normal and expected for a tri-stated bus output when its driver is disabled — it is not an error state the wayxoften is.
X does not always propagate
It is tempting to assume any gate fed an x input must output x. It doesn't, in general. Some operator/operand combinations have a controlling value — an input that determines the whole result on its own, regardless of what the other input is. 0 is AND's controlling value (0 & anything is 0); 1 is OR's controlling value (1 | anything is 1). When the other operand happens to be x, a controlling value still wins and the result is known. Only a non-controlling combination — 1 & x, or 0 | x — actually propagates the unknown. The demonstration below shows all four cases directly rather than asserting the rule.
Where each one comes from
A reg declared with no initial value starts as x at time 0, before any procedural statement runs. A wire with no driver, or a tri-stated output whose enable is low, reads as z.
assign bus = drive_en ? data : 4'bz;When drive_en is low, the module deliberately releases bus rather than driving 0 or 1 onto it — the pattern that lets more than one module share a single bus, with exactly one enabled at a time.
Predict before running
Before running it, predict what an uninitialized reg prints at time 0, what the bus reads while its driver is disabled, and which of the four 0 & x / 1 | x / 1 & x / 0 | x combinations actually resolve to a known value.
module tristate_driver (
input wire drive_en,
input wire [3:0] data,
output wire [3:0] bus
);
assign bus = drive_en ? data : 4'bz;
endmodule
module tb;
reg uninitialized;
reg drive_en;
reg [3:0] data;
wire [3:0] bus;
reg [3:0] p, q;
reg zero_op, one_op, x_op;
tristate_driver u_drv (.drive_en(drive_en), .data(data), .bus(bus));
initial begin
$display("uninitialized reg at t=0: %b", uninitialized);
drive_en = 1'b0;
data = 4'ha;
#1;
$display("drive_en=%b bus=%b (high-Z, undriven)", drive_en, bus);
drive_en = 1'b1;
#1;
$display("drive_en=%b bus=%b (driven)", drive_en, bus);
p = 4'bxxxx;
q = 4'bxxxx;
$display("p===q (case equality) = %b", p === q);
$display("p==q (logical equality, unknown-propagating) = %b", p == q);
zero_op = 1'b0;
one_op = 1'b1;
x_op = 1'bx;
$display("0 & x = %b (0 is AND's controlling value)", zero_op & x_op);
$display("1 | x = %b (1 is OR's controlling value)", one_op | x_op);
$display("1 & x = %b (no controlling value here, so x propagates)", one_op & x_op);
$display("0 | x = %b (no controlling value here, so x propagates)", zero_op | x_op);
$display("PASS");
$finish;
end
endmoduleExpected output — reveal after you predict
uninitialized reg at t=0: x
drive_en=0 bus=zzzz (high-Z, undriven)
drive_en=1 bus=1010 (driven)
p===q (case equality) = 1
p==q (logical equality, unknown-propagating) = x
0 & x = 0 (0 is AND's controlling value)
1 | x = 1 (1 is OR's controlling value)
1 & x = x (no controlling value here, so x propagates)
0 | x = x (no controlling value here, so x propagates)
PASSTwo different equality operators appear in that early pair of lines for a reason. ===/!== ("case equality") compare 0, 1, x, and z bit-for-bit exactly as written — two identically-x registers compare equal. ==/!= ("logical equality") propagate unknowns: if either operand has any x or z bit, the whole comparison result is x, meaning "cannot be determined" rather than true or false. If an if (p == q) condition evaluates to x or z, its if body is not taken; when an else is present, the else branch executes.
The last four lines confirm the controlling-value rule directly: 0 & x and 1 | x both resolve to a known value because 0 and 1 are each their operator's controlling value; 1 & x and 0 | x have no controlling operand present, so the unknown genuinely propagates. Reduction Operators and Bitwise vs. Logical Operators apply this same rule per bit across a whole vector.
Common mistakes
- Using
==to check forxin a testbench.if (value == 1'bx)is alwaysx, never1, so the check silently never fires. Use===(if (value === 1'bx)) to test for the literal unknown state. - Assuming any
xinput must produce anxoutput. As shown above, a controlling operand value can still resolve the result. Design-levelx/zpropagation depends on the specific gate and operand, not on a blanket rule. - Reading a bus as
0when it is actuallyz. They print differently (zvs0) and behave differently — aznet can still be driven by another source; a real0is a driven, known value. - Treating "the simulation showed
x" as always the same root cause. An uninitialized register reading before reset is one common source, but contention between drivers, an ambiguous control signal, explicitxstimulus in a testbench, and an unknown arithmetic operand can all produce it too — the fix is to trace the specific signal back to its actual driver, not to assume the tool is misbehaving.
Sources and verification
Example provenance: SkillLift Labs authored the example for this tutorial. tests/scripts/tutorial-verified-examples.test.ts extracts the complete marked example from this Markdown file, compiles it in Verilog-2005 mode with Icarus Verilog, runs it, and requires the simulator output to match the Expected output block exactly. The same test also carries an independent, code-owned semantic oracle, and exercises the article-to-runner source-splitting path. Automation verifies those stated properties; human technical review for indexing remains a separate gate.