Skip to article

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. A reg that has never been assigned reads as x. Whether a gate's output stays known or becomes x when one of its inputs is x depends 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 way x often 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
endmodule
Expected 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)
PASS

Two 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 for x in a testbench. if (value == 1'bx) is always x, never 1, so the check silently never fires. Use === (if (value === 1'bx)) to test for the literal unknown state.
  • Assuming any x input must produce an x output. As shown above, a controlling operand value can still resolve the result. Design-level x/z propagation depends on the specific gate and operand, not on a blanket rule.
  • Reading a bus as 0 when it is actually z. They print differently (z vs 0) and behave differently — a z net can still be driven by another source; a real 0 is 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, explicit x stimulus 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.