SystemVerilog logic vs wire vs reg (and bit): When to Use Each

In short: Use logic for almost every signal in SystemVerilog. It holds 4 states (0, 1, X, Z) and takes either one assign or procedural writes. reg is just the old name for the same thing. Use wire only when a signal has more than one driver, like a shared bus or an inout port. Use bit (2-state, only 0 and 1) for testbench counters and data where X can’t happen, never for sampling DUT outputs.

Every new SystemVerilog engineer asks the same question.

“Should this be wire, reg, or logic?”

The short answer is: logic, almost always.

The longer answer is about two separate questions:

  • How many states can it hold? 4 (0, 1, X, Z) or 2 (0, 1).
  • How many drivers can it have? One, or many.

Once you see those two questions, the four types sort themselves out.

Four everyday pictures

  • **wire = a water pipe.** It holds nothing by itself. It shows whatever is flowing in. No tap open? Nothing flows (Z). Two taps pushing different things? A mess (X).
  • **logic = a whiteboard with one marker.** It keeps what was written on it. Only one person holds the marker.
  • **reg = the same whiteboard, with an old label on it.** In SystemVerilog, reg and logic are the same type.
  • **bit = a light switch.** Only on or off. It has no way to say “I don’t know”.
logic as a whiteboard with one marker, reg as the same whiteboard with an old label, wire as a water pipe with two taps, and bit as a light switch that can only be on or off
Figure 1. logic is a whiteboard, reg is the same whiteboard with an old label, wire is a water pipe, and bit is a light switch.

The four types in code

module tb;
  wire  w;        // net: shows whatever drives it
  logic l;        // 4-state variable: 0, 1, X, Z
  reg   r;        // same as logic (old Verilog name)
  bit   b;        // 2-state variable: only 0 or 1

  assign w = 1'b1;          // continuous assignment
  assign l = 1'b0;          // logic can take an assign too (one driver)

  initial begin
    r = 1'bx;               // procedural assignment
    b = 1'bx;               // X does not fit in 2 states: becomes 0
    #1 $display("w=%b l=%b r=%b b=%b", w, l, r, b);
    b = 1'bz;
    #1 $display("bit after writing z: %b", b);
  end
endmodule

Output (tested on Icarus Verilog 12.0, 6 Nov 2026):

w=1 l=0 r=x b=0
bit after writing z: 0

What to notice:

  • l is a variable, yet it took an assign. In plain Verilog, that needed a wire.
  • r kept the X. reg is a 4-state variable, same as logic.
  • b turned X into 0, and Z into 0. A 2-state type has no slot for “unknown”.

Why Icarus and not Verilator here? Verilator is a 2-state simulator, so it can’t show X or Z at all. For this page I needed a simulator that keeps all four states.

4-state vs 2-state, side by side

4-state2-state
Values0, 1, X, Z0, 1
Typeslogic, reg, wire, integerbit, byte, int, shortint, longint
Starts asX (variables), Z (undriven nets)0
X or Z written inKeptBecomes 0
Use forRTL, anything that touches the DUTTestbench counters, loop variables, random data

logic vs wire vs reg vs bit

logicregwirebit
KindVariableVariableNetVariable
States4442
Holds a valueYesYesNo, shows its driversYes
Written by assignYes, one onlyYes, one only (in SystemVerilog; not in old Verilog)Yes, manyYes, one only
Written in always / initialYesYesNoYes
More than one driverNoNoYes, values are resolvedNo
PictureWhiteboardSame whiteboard, old labelWater pipeLight switch
Use it forAlmost every RTL and TB signalOld code onlyShared buses, inout portsTB counters and data

The one rule worth remembering: a variable gets one continuous driver; a net can get many (IEEE 1800-2017, section 6.5).

Still not sure? Answer three questions:

Flowchart: more than one driver, use wire. Touches the DUT, use logic. Testbench-only data where X can never happen, use bit or int. When in doubt, use logic
Figure 2. Three questions that pick between logic, wire and bit.

When you really need wire: a shared data line

Here two devices share one line. Each drives it only when its enable is on. The rest of the time it lets go (Z).

// Two devices share one data line. Each drives it only when enabled.
module tb;
  wire  data;              // a net: can have many drivers
  logic en_a, en_b;
  logic val_a, val_b;

  assign data = en_a ? val_a : 1'bz;   // device A
  assign data = en_b ? val_b : 1'bz;   // device B

  initial begin
    en_a = 0; en_b = 0; val_a = 1; val_b = 0;
    #1 $display("nobody drives      : data=%b", data);
    en_a = 1;
    #1 $display("only A drives 1    : data=%b", data);
    en_b = 1;
    #1 $display("A drives 1, B 0    : data=%b", data);
    val_b = 1;
    #1 $display("both drive 1       : data=%b", data);
  end
endmodule

Output (Icarus Verilog 12.0):

nobody drives      : data=z
only A drives 1    : data=1
A drives 1, B 0    : data=x
both drive 1       : data=1

That’s the water pipe:

  • No tap open: Z.
  • One tap: you see its value.
  • Two taps fighting: X. That X is a bus contention bug, and you want to see it.
  • Two taps agreeing: no conflict.

Change wire data to logic data and it won’t even compile. Icarus says:

ex3_two_drivers.sv:5: error: Unresolved net/uwire data cannot have multiple drivers.

A working example: the register that was never reset

This is a classic reason to keep bit out of monitors.

A register block has two registers. Both should read 0 after reset. But the designer forgot to reset STATUS. So STATUS reads X.

The monitor samples rdata into two fields: one bit, one logic.

A register that was never reset reads X. A bit monitor field turns it into 00, so the reset check passes and the bug is missed. A logic field keeps X, and $isunknown catches it
Figure 3. Same DUT, same read: the bit field hides the X and the check passes, the logic field keeps it and the check catches the bug.
// A register block with a bug: STATUS (addr 1) was never reset.
module regs (input logic clk, rst_n, input logic [1:0] addr, output logic [7:0] rdata);
  logic [7:0] ctrl, status;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n) ctrl <= 8'h00;           // bug: status is missing here
  assign rdata = (addr == 0) ? ctrl : status;
endmodule

module tb;
  logic clk = 0, rst_n = 0;
  logic [1:0] addr;
  logic [7:0] rdata;
  regs dut (.*);
  always #5 clk = ~clk;

  bit   [7:0] seen_2state;     // monitor field declared as bit
  logic [7:0] seen_4state;     // monitor field declared as logic

  initial begin
    #12 rst_n = 1;
    for (int a = 0; a < 2; a++) begin
      addr = a;
      @(posedge clk);
      seen_2state = rdata;
      seen_4state = rdata;
      // expected reset value of both registers is 8'h00
      $display("addr %0d: bit sees %h -> %s | logic sees %h -> %s", a,
        seen_2state, (seen_2state == 8'h00) ? "PASS" : "FAIL",
        seen_4state, ($isunknown(seen_4state) || seen_4state !== 8'h00) ? "FAIL" : "PASS");
    end
    $finish;
  end
endmodule

Output (Icarus Verilog 12.0):

addr 0: bit sees 00 -> PASS | logic sees 00 -> PASS
addr 1: bit sees 00 -> PASS | logic sees xx -> FAIL

Same DUT, same read, two answers:

  • The bit field turned X into 00. The reset check passed. The bug walks into silicon.
  • The logic field kept the X, and $isunknown() caught it.

The light switch can’t say “I don’t know”, so it says “off”. And “off” happened to be the expected value.

In a UVM monitor, the same rule applies: any field that copies a DUT signal should be logic. Keep bit for things the testbench makes up itself.

Common mistakes (and the fix for each)

These come from real Verification Academy threads. Each one is retold in my own words with a link to the original.

1. Two assign statements on one logic

  • What happened: an engineer drove one logic signal from two assign lines, each with its own condition, and got “assigned by multiple continuous statements” (Verification Academy).
  • Why: a variable gets one continuous driver. Two is an error, even if you think only one is “active” at a time.
  • Fix: merge the conditions into one assign. If the line really is shared, like the bus above, make it a wire.

2. Mixing assign and always on the same signal

  • What happened: someone asked for a code example of “cannot be driven by multiple structural drivers”. An initial block plus an assign on the same logic gave “written by continuous and procedural assignments” (Verification Academy).
  • Why: pick one style per variable. Many procedural writes are legal, but then the last write wins, and two blocks writing at the same time is a race.
  • Fix: one driver per signal. If you need procedural writes, put them all in one block.

3. “Fixing” an X by driving the DUT output from the testbench

  • What happened: in 2014 an engineer wanted a DUT output that showed X to read as 0. They added assign out = (out === 1'bx) ? 1'b0 : out; in the testbench, and the X got worse (Verification Academy, 2014).
  • Why: that assign is a second driver on the same net. The DUT drives X, the testbench drives 0, and the wire resolves them to X. Two taps fighting.
  • Fix: never drive a DUT output. Read it, and handle X in the checker with === or $isunknown().

4. Using a logic variable for a bidirectional line

  • What happened: in 2023 someone asked why logic can’t be used for inout ports (Verification Academy, 2023).
  • Why: an inout is driven from both sides, so it needs many drivers. Only nets allow that.
  • Fix: use wire for inout ports and shared buses. In an interface, declare the shared line as wire and drive it from each side with assign ... ? value : 'z.

5. Expecting X to stay in its own bits during math

  • What happened: in 2022 an engineer added two values with X in the top bits and expected the low bits to stay 0. The whole sum came out X (Verification Academy, 2022).
  • Why: arithmetic is pessimistic. One X bit anywhere makes the whole result X. Bitwise operators keep X where it is.
module tb;
  logic [3:0] a = 4'bxx00, b = 4'bxx00;
  logic [4:0] c;
  logic [3:0] m;
  initial begin
    c = a + b;               // arithmetic: one X bit spoils the whole result
    m = a & 4'b0011;         // bitwise: X only where it really matters
    #1 $display("a + b       = %b", c);
    $display("a & 4'b0011 = %b", m);
  end
endmodule

Output (Icarus Verilog 12.0):

a + b       = xxxxx
a & 4'b0011 = 0000
  • Fix: if you see an all-X sum, look for one X input bit. Don’t trust a 2-state type to hide it for you; that only turns the bug into a wrong number.

Frequently asked questions

Is reg the same as logic in SystemVerilog?

Yes. They are the same 4-state type. logic is the clearer name, because a reg was never always a register.

Can logic replace wire everywhere?

No. Anything with more than one driver needs a net: shared buses, tri-state lines, inout ports.

When should I use bit instead of logic?

  • For values the testbench creates itself: loop counters, random data, IDs.
  • Never for anything copied from the DUT. A bit turns X into 0 and hides the bug.

What does X mean in simulation?

Unknown. A register never reset, two drivers fighting, or math on an unknown input. In silicon it would be a real 0 or 1, you just can’t tell which.

Does logic make simulation slower?

A 4-state value takes more memory than a 2-state one, which is why big testbench arrays often use bit or int. For DUT signals, the X checking is worth it.

What to read next

References

  • IEEE 1800-2017, SystemVerilog standard: section 6.5 (nets and variables), 6.6 (net types), 6.11 (integer data types, 2-state and 4-state).
  • Verification Academy forum threads linked in the mistakes section (accessed 6 Nov 2026).
  • All code tested on Icarus Verilog 12.0 on 6 Nov 2026.