logic is a 4-state data type: each bit holds 0, 1, x (unknown) or z (high impedance). A variable declared with it may be written by one continuous assignment or port, or by procedural assignments. Ordinary procedural processes can write the same variable; logic alone does not enforce one procedural writer. Keep one writer per RTL variable, and use always_ff/always_comb plus lint to check ownership.
Type | States | Driven by | Use for |
|---|---|---|---|
| 0 1 x z | One | Almost every RTL variable |
| 0 1 x z | Continuous drivers, possibly several | Multi-driver nets: tri-state buses |
| 0 1 | Procedural | Testbench counters and flags (never for RTL storage you need |
| 0 1 (2-state, 32-bit signed) | Procedural | Loop indices, testbench counters |
| 0 1 x z (4-state, 32-bit signed) | Procedural | Legacy Verilog code; prefer |
// bad-example: two drivers on one logic signal
module two_drivers (input logic a, b, sel, output logic y);
assign y = a; // driver 1
always_comb if (sel) y = b; // illegal mixing; always_comb also forbids other writers
endmoduleThe example is illegal because it mixes continuous and procedural assignments to the same variable, and violates always_comb writer ownership. That is different from two plain procedural processes assigning a variable: the language permits those, so simultaneous writes can race unless lint or a stronger process construct catches the mistake.
The following standalone testbench uses two ordinary initial writers at distinct times. Its result is deterministic and demonstrates that the data type itself does not reject multiple procedural writers. This is a simulation example, not a synthesizable RTL pattern. Resolved shared buses instead need a net such as wire (or explicitly wire logic); logic describes the stored bit values, while the net kind supplies resolution.
module tb_logic_writers;
logic q;
int errors = 0;
initial begin
q = 1'b0;
#5 q = 1'b1;
end
initial begin
#1 q = 1'b1;
#2 q = 1'b0;
end
initial begin
#2;
assert (q === 1'b1) else errors++;
#2;
assert (q === 1'b0) else errors++;
#2;
assert (q === 1'b1) else errors++;
if (errors == 0) $display("PASS: plain procedural writers update logic at separate times");
else $display("FAIL: procedural-writer errors=%0d", errors);
$finish;
end
endmoduleEvery logic bit starts as `x` until assigned. An un-reset flop shows x in the waveform instead of a misleading 0.
x propagates through logic: x & 0 is 0, but x & 1 is x. An x in an if condition is treated as false, so simulation deterministically takes the else branch, which can hide the bug.
The conditional operator behaves differently. With c = 1'bx, c ? a : b does not choose a branch: it merges the two values bit by bit, keeping bits where they agree and giving x where they differ. So c ? 4'b1010 : 4'b1000 gives 10x0 (iverilog), while the same x in an if silently takes else.
Simulators differ on x handling (optimistic or pessimistic modes, and x-propagation options). An x condition in an if is optimistic and can hide a bug; a two-state tool such as Verilator, by default, never shows the x at all. That is why explicit x checks and assertions matter.
z means nothing is driving. On an internal signal it usually indicates an unconnected port.
logic show in simulation, and what would a bit show?logic shows x, flagging the missing initialization. A bit would show 0, which looks valid and hides the bug.
A small testbench that checks uninitialized logic and the basic x identities.
module tb_logic_x;
logic a; // never assigned: starts as x
logic [3:0] v = 4'b1x0z;
int errors = 0;
initial begin
#1;
if (a !== 1'bx) errors++; // uninitialized logic is x
if ((1'bx & 1'b0) !== 1'b0) errors++; // x AND 0 is known: 0
if ((1'bx & 1'b1) !== 1'bx) errors++; // x AND 1 stays unknown
if (^v !== 1'bx) errors++; // reduction over an x bit is x
if (errors == 0) $display("PASS: x behaves as described");
$finish;
end
endmodule