4-state values — 0, 1, unknown (X), and high-impedance (Z) — were already covered in detail in the Verilog tutorial on Verilog Data Types. This article is a quick refresher on that, before focusing on what SystemVerilog adds to the picture: the logic keyword.
Every signal in a design is really two things layered together: an object kind — whether it's a variable or a net — and a data type, the set of values it can hold (like logic, bit, or int). logic itself is just a data type, not an object on its own; SystemVerilog's real improvement is letting that one data type be used for either object kind, which is what this article focuses on.
7 min read | Beginner Level
What You'll Learn
- A quick refresher on 4-state values, with a link to the full explanation
- Why
logicis a data type, not a variable or a net by itself - How to drive
logicfrom a procedural block only, and from a continuous assign only - How
wire logicdiffers from a plainlogicdeclaration
Quick Refresher: 4-State Values
A 4-state data type can hold 0, 1, X (unknown), or Z (high-impedance). Every net (wire) and variable (reg) in Verilog uses 4-state values, but they don't share a default: an undriven net defaults to Z (nothing is driving it), while an undriven variable defaults to X (unknown, uninitialized) — neither ever defaults to 0.
For the full breakdown of what X and Z represent in real hardware, and how nets differ from variables, see Verilog Data Types.
Quick Check
Q: What's the default value of an undriven wire, versus an undriven logic or reg?
Show Answer
A wire is a net, so with nothing driving it, it defaults to Z (high-impedance). logic and reg default to X (unknown) instead, since a plain declaration of either implicitly makes it a variable. Neither ever defaults to 0. See Verilog Data Types for why this matters for catching reset and initialization bugs.
logic: A 4-State Data Type
logic is a 4-state data type — its value set (0, 1, X, Z) is identical to reg's. It isn't, by itself, a variable or a net; it's just the type. When you write a plain declaration like logic a; with no other keyword, SystemVerilog implicitly treats that as var logic a; — a variable of type logic.
That's why a plain logic signal behaves like the classic reg by default: driven from a procedural block, or from a single continuous assign, but never from more than one driver, and never from both a continuous assign and a procedural block on the same signal at once. That single-driver restriction belongs to variables in general, not to the logic type specifically.
In standard Verilog, a reg could only ever be assigned inside a procedural block (always, initial) — driving a reg with a continuous assign statement was a syntax error. SystemVerilog changed this: reg and logic are now identical 4-state variable types, and SystemVerilog relaxed the strict Verilog distinction between nets and variables, allowing a continuous assign to drive a variable of either keyword. In practice almost everyone reaches for logic to take advantage of this, but the rule itself applies equally to reg.
The part that's genuinely new is that logic can also be used to declare a net, by pairing it with a net keyword: wire logic w;. A net of type logic behaves like any other net — it can legally have multiple drivers, resolved by strength.
In fact, plain wire is really shorthand for a net with this same 4-state value set, so wire and wire logic behave identically; the explicit form is rarely written out, but it's what's happening underneath.
Syntax
logic [msb:lsb] signal_name; // implicit variable -- same as: var logic signal_name;
var logic [msb:lsb] signal_name; // explicit variable, identical to the line above
wire logic [msb:lsb] signal_name; // net of type logic -- supports multiple drivers
The comparison above reflects the conventional, most-common usage of each keyword: reg is almost always written as a purely procedural variable (its classic Verilog role), and wire is almost always a net. Technically, since reg and logic are the same underlying type in SystemVerilog, a plain reg variable can also legally take a single continuous assign — it's just rarely written that way, since logic is the modern convention for exactly that case. Pairing either keyword-equivalent type with wire instead (wire logic) produces a net, which supports multiple drivers just like any other net.
Quick Check
Q: Can a logic signal be driven by two separate continuous assign statements at the same time?
Show Answer
Not if it's declared the usual way (logic a;), since that implicitly makes it a variable, and variables support only one driver total. But wire logic a; declares a net of type logic instead, and nets are explicitly built to support multiple drivers, resolved by strength.
Example 1: Driven from Both a Procedural Block and a Continuous Assign
module tb;
logic [3:0] my_data; // Declare a 4-bit logic type variable
logic en; // Declare a 1-bit logic type variable
initial begin
$display ("my_data=0x%0h en=%0b", my_data, en); // Default value of logic type is X
my_data = 4'hB; // logic datatype can be driven in initial/always blocks
$display ("my_data=0x%0h en=%0b", my_data, en);
#1;
$display ("my_data=0x%0h en=%0b", my_data, en);
end
assign en = my_data[0]; // logic datatype can also be driven via assign statements
endmodule
Output
ncsim> run my_data=0xx en=x my_data=0xb en=x my_data=0xb en=1 ncsim: *W,RNQUIE: Simulation is complete.
Right after my_data = 4'hB;, en still prints as x, even though it's continuously driven by assign en = my_data[0];. The continuous assign does react to the change, but as a separate event in the same simulation time step, with no guarantee it completes before the next $display runs. Only after #1; advances time does en correctly show 1.
my_data and en are two separate logic variables here — one driven procedurally, the other by a continuous assign. A single variable can only ever have one of these, never both: mixing a continuous assign and a procedural block on the same variable is illegal, and so is giving one variable two continuous assigns. See Common Mistakes below for what that looks like and how to fix it.
Example 2: Driven Purely from a Procedural Block
my_data above was still only ever assigned procedurally; here's a logic signal driven entirely inside an always_comb block, with no continuous assign involved at all — a 4-to-1 mux.
module tb;
logic [1:0] sel;
logic [7:0] a, b, c, d;
logic [7:0] mux_out;
// mux_out is driven only from inside this procedural block
always_comb begin
case (sel)
2'b00: mux_out = a;
2'b01: mux_out = b;
2'b10: mux_out = c;
2'b11: mux_out = d;
endcase
end
initial begin
a = 8'h11; b = 8'h22; c = 8'h33; d = 8'h44;
sel = 2'b00; #1;
$display ("sel=%0b mux_out=0x%0h", sel, mux_out);
sel = 2'b10; #1;
$display ("sel=%0b mux_out=0x%0h", sel, mux_out);
sel = 2'b11; #1;
$display ("sel=%0b mux_out=0x%0h", sel, mux_out);
end
endmodule
Output
sel=00 mux_out=0x11 sel=10 mux_out=0x33 sel=11 mux_out=0x44
always_comb automatically re-evaluates whenever any signal it reads — sel, a, b, c, or d — changes, so mux_out always reflects the currently selected input. Just like the previous example, the #1 after each sel change gives the block time to settle before the value is displayed.
This is the pattern you'll see in almost every combinational RTL block written in modern SystemVerilog: logic outputs assigned entirely inside an always_comb, with no wire or assign in sight.
Example 3: Driven Purely from a Continuous Assign
logic can just as easily be driven the old wire-style way, with no procedural block at all.
module tb;
logic a, b;
logic y_and, y_or, y_xor;
// All three outputs are driven only by continuous assigns
assign y_and = a & b;
assign y_or = a | b;
assign y_xor = a ^ b;
initial begin
a = 1'b0; b = 1'b0; #1;
$display ("a=%0b b=%0b and=%0b or=%0b xor=%0b", a, b, y_and, y_or, y_xor);
a = 1'b1; b = 1'b0; #1;
$display ("a=%0b b=%0b and=%0b or=%0b xor=%0b", a, b, y_and, y_or, y_xor);
a = 1'b1; b = 1'b1; #1;
$display ("a=%0b b=%0b and=%0b or=%0b xor=%0b", a, b, y_and, y_or, y_xor);
end
endmodule
Output
a=0 b=0 and=0 or=0 xor=0 a=1 b=0 and=0 or=1 xor=1 a=1 b=1 and=1 or=1 xor=0
Nothing here is different from how you'd write this with wire — logic accepts continuous assigns exactly the same way. The only thing that changes is the keyword in the declaration.
In practice, most SystemVerilog style guides now recommend declaring every internal signal as logic by default, and only reaching for wire the moment a signal actually needs more than one driver — for example, a shared bus.
Common Mistakes
Mistake #1: Assuming a logic variable can have multiple drivers
A plain logic declaration is implicitly a variable, and variables are limited to a single structural driver — full stop. Both of the patterns below are illegal for exactly that reason.
module tb;
logic data; // implicitly a variable: var logic data;
assign data = 1'b1; // Driver #1 (continuous)
initial
data = 1'b0; // Driver #2 (procedural) -- illegal: can't mix continuous and procedural drivers on a variable
endmodule
module tb;
logic data;
assign data = 1'b1; // Driver #1
assign data = 1'b0; // Driver #2 -- also illegal: a variable can't have two continuous drivers either
endmodule
The fix isn't to avoid logic — it's to make data a net instead of a variable:
module tb;
wire logic data; // now a net of type logic, not a variable
assign data = 1'b1; // Driver #1
assign data = 1'b0; // Driver #2 -- legal: nets support multiple drivers, resolved by strength
endmodule
Declaring data as wire logic (or just wire) makes it a net, and nets are explicitly designed to support multiple drivers — exactly what's needed for something like a shared tri-state bus.
Mistake #2: Thinking logic and reg are different value systems
As a data type, logic is identical to reg — same 4-state value set, and in SystemVerilog, the same rules: a plain reg variable can technically take a single continuous assign too, since it's the same variable type as logic under a different keyword (this wasn't allowed in classic Verilog, where reg was procedural-only). The real, practical difference is object kind flexibility: reg is essentially always used as a variable, while logic can be paired with var (the default) or a net keyword like wire to become a net instead, as Common Mistake #1 shows.
Mistake #3: Assuming an undriven signal reads as 0
An undriven logic or reg reads as X, and an undriven wire reads as Z — see the refresher above. Neither is 0. Code that implicitly assumes a default of 0 can silently pass in simulation and only reveal the bug much later.
Mistake #4: Confusing logic with 2-state types
logic is a 4-state type — it can hold X and Z, just like reg. Don't confuse it with 2-state types like bit, which can only hold 0 and 1. See SystemVerilog bit, byte and int for more on 2-state types.
Practice Exercise
Challenge: A 2-to-1 Mux, Two Ways
Write a 2-to-1 multiplexer that selects between in0 and in1 based on sel, once using an always_comb block (procedural), and once more using a continuous assign with the ternary operator. Both versions should use logic for every signal.
Hint
For the assign version, the ternary form is assign out = sel ? in1 : in0;.
Solution
module tb;
logic sel, in0, in1;
logic out_proc, out_assign;
// Procedural version
always_comb begin
if (sel) out_proc = in1;
else out_proc = in0;
end
// Continuous assign version
assign out_assign = sel ? in1 : in0;
initial begin
in0 = 1'b0; in1 = 1'b1;
sel = 1'b0; #1;
$display ("sel=%0b out_proc=%0b out_assign=%0b", sel, out_proc, out_assign);
sel = 1'b1; #1;
$display ("sel=%0b out_proc=%0b out_assign=%0b", sel, out_proc, out_assign);
end
endmodule
Summary
In this tutorial, you learned:
- 4-state values were already covered in Verilog Data Types —
0,1,X, andZ; undriven nets default toZ, undriven variables default toX logicis a 4-state data type, identical in value set toreg— it isn't a variable or a net by itself- A plain
logic a;declaration implicitly creates a variable (var logic a;), which is why it behaves likeregby default: driven by one procedural block, or one continuous assign, but never both and never more than one wire logic a;declares a net of typelogicinstead, which supports multiple drivers resolved by strength, just like any other net- A signal driven purely inside
always_comb, or purely by continuousassignstatements, works exactly the same whether it's declaredlogicor the equivalent old-stylereg/wire