assign statement is the foundation for building these zero-latency circuits where outputs respond instantly to input changes. Mastering assign is essential for designing efficient ALUs, multiplexers, decoders, and data path logic that form the backbone of modern processors.8 min read | Beginner to Intermediate
What You'll Learn
- How to implement combinational logic using the
assignstatement for wire-type signals - Design practical circuits: adders, multiplexers, decoders, and demultiplexers using assign
- Understand how assign statements synthesize to combinational gates with zero latency
- Apply best practices for writing clean, maintainable combinational logic
The Verilog assign statement is used to continuously drive a signal of wire datatype and synthesizes as combinational logic. Unlike sequential logic, combinational logic has no memory elements--the output depends only on the current input values. This makes assign perfect for implementing Boolean equations, arithmetic operations, and data routing circuits.
Real-World Application
Intel's ALU Design: Modern processors like Intel's Alder Lake use thousands of combinational logic blocks implemented with continuous assignments for arithmetic operations. The ALU's adder circuits, bitwise operations, and comparison logic are all combinational blocks that execute in a single clock cycle, achieving sub-nanosecond propagation delays through optimized gate-level synthesis.
Example #1 : Simple Combinational Logic
The code shown below implements a simple digital combinational logic which has an output wire z that is driven continuously with an assign statement to realize the digital equation.
module combo ( input a, b, c, d, e,
output z);
// Continuous assignment: output changes immediately when inputs change
// Implements: z = (a AND b) OR ((c XOR d) AND (NOT e))
// Synthesizes to AND, OR, XOR, and NOT gates
assign z = ((a & b) | (c ^ d) & ~e);
endmodule
The module combo gets elaborated into the following hardware schematic using synthesis tools and can be seen that the combinational logic is implemented with digital gates.

The assign statement creates combinational logic, not sequential. There are no flip-flops--only gates. Output z updates immediately (within gate delays) when any input changes.
Testbench
The testbench is a platform for simulating the design to ensure that the design does behave as expected. All combinations of inputs are driven to the design module using a for loop with a delay statement of 10 time units so that the new value is applied to the inputs after some time.
module tb;
// Declare testbench variables
reg a, b, c, d, e; // Use 'reg' for variables you assign in procedural blocks
wire z; // Use 'wire' for outputs from modules under test
integer i;
// Instantiate the design and connect design inputs/outputs with
// testbench variables
combo u0 ( .a(a), .b(b), .c(c), .d(d), .e(e), .z(z));
initial begin
// At the beginning of time, initialize all inputs of the design
// to a known value, in this case we have chosen it to be 0.
a = 0;
b = 0;
c = 0;
d = 0;
e = 0;
// Use a $monitor task to print any change in the signal to
// simulation console
$monitor ("a=%0b b=%0b c=%0b d=%0b e=%0b z=%0b",
a, b, c, d, e, z);
// Because there are 5 inputs, there can be 32 different input combinations
// So use an iterator "i" to increment from 0 to 32 and assign the value
// to testbench variables so that it drives the design inputs
for (i = 0; i < 32; i = i + 1) begin
{a, b, c, d, e} = i; // Concatenation assigns bits from i to inputs
#10; // Wait 10 time units between test vectors
end
end
endmodule
ncsim> run a=0 b=0 c=0 d=0 e=0 z=0 a=0 b=0 c=0 d=0 e=1 z=0 a=0 b=0 c=0 d=1 e=0 z=1 a=0 b=0 c=0 d=1 e=1 z=0 a=0 b=0 c=1 d=0 e=0 z=1 a=0 b=0 c=1 d=0 e=1 z=0 a=0 b=0 c=1 d=1 e=0 z=0 a=0 b=0 c=1 d=1 e=1 z=0 a=0 b=1 c=0 d=0 e=0 z=0 a=0 b=1 c=0 d=0 e=1 z=0 a=0 b=1 c=0 d=1 e=0 z=1 a=0 b=1 c=0 d=1 e=1 z=0 a=0 b=1 c=1 d=0 e=0 z=1 a=0 b=1 c=1 d=0 e=1 z=0 a=0 b=1 c=1 d=1 e=0 z=0 a=0 b=1 c=1 d=1 e=1 z=0 a=1 b=0 c=0 d=0 e=0 z=0 a=1 b=0 c=0 d=0 e=1 z=0 a=1 b=0 c=0 d=1 e=0 z=1 a=1 b=0 c=0 d=1 e=1 z=0 a=1 b=0 c=1 d=0 e=0 z=1 a=1 b=0 c=1 d=0 e=1 z=0 a=1 b=0 c=1 d=1 e=0 z=0 a=1 b=0 c=1 d=1 e=1 z=0 a=1 b=1 c=0 d=0 e=0 z=1 a=1 b=1 c=0 d=0 e=1 z=1 a=1 b=1 c=0 d=1 e=0 z=1 a=1 b=1 c=0 d=1 e=1 z=1 a=1 b=1 c=1 d=0 e=0 z=1 a=1 b=1 c=1 d=0 e=1 z=1 a=1 b=1 c=1 d=1 e=0 z=1 a=1 b=1 c=1 d=1 e=1 z=1 ncsim: *W,RNQUIE: Simulation is complete.

Example #2: Half Adder
The half adder module accepts two scalar inputs a and b and uses combinational logic to assign the outputs sum and carry bit cout. The sum is driven by an XOR between a and b while the carry bit is obtained by an AND between the two inputs.
module ha ( input a, b,
output sum, cout);
// Sum bit: XOR gives 1 when inputs differ (1+0=1, 0+1=1)
assign sum = a ^ b;
// Carry bit: AND gives 1 only when both inputs are 1 (1+1=10 in binary)
assign cout = a & b;
endmodule

Real-World Application
Carry-Lookahead Adders: Modern processors use half adders as building blocks in carry-lookahead adder (CLA) circuits. A 64-bit adder in the execution unit chains together sophisticated adder structures derived from basic half and full adders, enabling single-cycle integer addition at multi-GHz frequencies.
Testbench
module tb;
// Declare testbench variables
reg a, b; // Input drivers
wire sum, cout; // Outputs from design under test
integer i;
// Instantiate the design and connect design inputs/outputs with
// testbench variables
ha u0 ( .a(a), .b(b), .sum(sum), .cout(cout));
initial begin
// At the beginning of time, initialize all inputs of the design
// to a known value, in this case we have chosen it to be 0.
a = 0;
b = 0;
// Use a $monitor task to print any change in the signal to
// simulation console
$monitor("a=%0b b=%0b sum=%0b cout=%0b", a, b, sum, cout);
// Because there are only 2 inputs, there can be 4 different input combinations
// So use an iterator "i" to increment from 0 to 4 and assign the value
// to testbench variables so that it drives the design inputs
for (i = 0; i < 4; i = i + 1) begin
{a, b} = i;
#10;
end
end
endmodule
ncsim> run a=0 b=0 sum=0 cout=0 a=0 b=1 sum=1 cout=0 a=1 b=0 sum=1 cout=0 a=1 b=1 sum=0 cout=1 ncsim: *W,RNQUIE: Simulation is complete.

Example #3: Full Adder
A full adder can be built using the half adder module shown above or the entire combinational logic can be applied as is with assign statements to drive the outputs sum and cout. This implementation uses the direct Boolean equations approach.
module fa ( input a, b, cin,
output sum, cout);
// Sum: XOR chain handles the three-input addition
// Result is 1 when odd number of inputs are 1
assign sum = (a ^ b) ^ cin;
// Carry out: Generate carry when (a AND b) OR when sum of a,b generates carry with cin
// This is the standard full adder carry equation
assign cout = (a & b) | ((a ^ b) & cin);
endmodule

A full adder can also be built hierarchically by instantiating two half adders and an OR gate. The structural approach is more modular but may synthesize slightly differently than the Boolean equation approach shown here.
Testbench
module tb;
reg a, b, cin; // Three input bits for full adder
wire sum, cout; // Sum and carry outputs
integer i;
fa u0 ( .a(a), .b(b), .cin(cin), .sum(sum), .cout(cout));
initial begin
a = 0;
b = 0;
$monitor("a=%0b b=%0b cin=%0b sum=%0b cout=%0b", a, b, cin, sum, cout);
// Test all 8 combinations (2^3 = 8)
for (i = 0; i < 8; i = i + 1) begin
{a, b, cin} = i;
#10;
end
end
endmodule
ncsim> run a=0 b=0 cin=0 sum=0 cout=0 a=0 b=0 cin=1 sum=1 cout=0 a=0 b=1 cin=0 sum=1 cout=0 a=0 b=1 cin=1 sum=0 cout=1 a=1 b=0 cin=0 sum=1 cout=0 a=1 b=0 cin=1 sum=0 cout=1 a=1 b=1 cin=0 sum=0 cout=1 a=1 b=1 cin=1 sum=1 cout=1 ncsim: *W,RNQUIE: Simulation is complete.

Example #4: 2x1 Multiplexer
The simple 2x1 multiplexer uses a ternary operator to decide which input should be assigned to the output c. If sel is 1, output is driven by a and if sel is 0 output is driven by b. Multiplexers are fundamental building blocks in data path design.
module mux_2x1 (input a, b, sel,
output c);
// Ternary operator: sel ? true_value : false_value
// When sel=1, output c gets input a
// When sel=0, output c gets input b
// Synthesizes to a 2:1 mux gate or equivalent logic
assign c = sel ? a : b;
endmodule

The ternary operator (?:) is highly efficient for multiplexers. For larger muxes, consider using case statements within always blocks for better readability.
Testbench
module tb;
// Declare testbench variables
reg a, b, sel; // Two data inputs and one select
wire c; // Multiplexer output
integer i;
// Instantiate the design and connect design inputs/outputs with
// testbench variables
mux_2x1 u0 ( .a(a), .b(b), .sel(sel), .c(c));
initial begin
// At the beginning of time, initialize all inputs of the design
// to a known value, in this case we have chosen it to be 0.
a = 0;
b = 0;
sel = 0;
$monitor("a=%0b b=%0b sel=%0b c=%0b", a, b, sel, c);
// Test all 8 input combinations
for (i = 0; i < 8; i = i + 1) begin
{a, b, sel} = i;
#10;
end
end
endmodule
ncsim> run a=0 b=0 sel=0 c=0 a=0 b=0 sel=1 c=0 a=0 b=1 sel=0 c=1 a=0 b=1 sel=1 c=0 a=1 b=0 sel=0 c=0 a=1 b=0 sel=1 c=1 a=1 b=1 sel=0 c=1 a=1 b=1 sel=1 c=1 ncsim: *W,RNQUIE: Simulation is complete.

Example #5: 1x4 Demultiplexer
The demultiplexer routes a single input f to one of four outputs based on the 2-bit select signal sel. Each output signal is driven by a separate assign statement. Note that the same signal is generally not recommended to be driven by different assign statements.
module demux_1x4 ( input f,
input [1:0] sel,
output a, b, c, d);
// Output a is active when sel = 2'b00
assign a = f & ~sel[1] & ~sel[0];
// Output b is active when sel = 2'b10
assign b = f & sel[1] & ~sel[0];
// Output c is active when sel = 2'b01
assign c = f & ~sel[1] & sel[0];
// Output d is active when sel = 2'b11
assign d = f & sel[1] & sel[0];
endmodule

Each output signal should only be driven by one assign statement. Driving the same wire from multiple assign statements creates a multi-driver conflict and will cause synthesis errors or X propagation in simulation.
Testbench
module tb;
// Declare testbench variables
reg f; // Input data signal
reg [1:0] sel; // 2-bit select signal (4 combinations)
wire a, b, c, d; // Four output channels
integer i;
// Instantiate the design and connect design inputs/outputs with
// testbench variables
demux_1x4 u0 ( .f(f), .sel(sel), .a(a), .b(b), .c(c), .d(d));
// At the beginning of time, initialize all inputs of the design
// to a known value, in this case we have chosen it to be 0.
initial begin
f = 0;
sel = 0;
$monitor("f=%0b sel=%0b a=%0b b=%0b c=%0b d=%0b", f, sel, a, b, c, d);
// Because there are 3 inputs, there can be 8 different input combinations
// So use an iterator "i" to increment from 0 to 8 and assign the value
// to testbench variables so that it drives the design inputs
for (i = 0; i < 8; i = i + 1) begin
{f, sel} = i;
#10;
end
end
endmodule
ncsim> run f=0 sel=0 a=0 b=0 c=0 d=0 f=0 sel=1 a=0 b=0 c=0 d=0 f=0 sel=10 a=0 b=0 c=0 d=0 f=0 sel=11 a=0 b=0 c=0 d=0 f=1 sel=0 a=1 b=0 c=0 d=0 f=1 sel=1 a=0 b=0 c=1 d=0 f=1 sel=10 a=0 b=1 c=0 d=0 f=1 sel=11 a=0 b=0 c=0 d=1 ncsim: *W,RNQUIE: Simulation is complete.

Example #6: 4x16 Decoder
A decoder activates one of multiple output lines based on the binary value of the input. This 4-to-16 decoder uses an enable signal en and a compact left-shift operation to set the appropriate output bit.
module dec_4x16 ( input en,
input [3:0] in,
output [15:0] out);
// When enabled, shift 1 left by 'in' positions to activate one output bit
// Example: in=3 produces out=16'b0000_0000_0000_1000 (bit 3 set)
// When disabled (en=0), all outputs are 0
assign out = en ? 1 << in : 0;
endmodule

The left-shift operator (<<) provides an elegant way to implement decoders. The expression 1 << n creates a value with only bit n set to 1, which is exactly the behavior needed for address decoding circuits.
Testbench
module tb;
reg en; // Enable signal
reg [3:0] in; // 4-bit input (16 possible values)
wire [15:0] out; // 16-bit output (one-hot encoded)
integer i;
dec_4x16 u0 ( .en(en), .in(in), .out(out));
initial begin
en = 0;
in = 0;
$monitor("en=%0b in=0x%0h out=0x%0h", en, in, out);
// Test all 32 combinations (enable + 16 input values)
for (i = 0; i < 32; i = i + 1) begin
{en, in} = i;
#10;
end
end
endmodule
ncsim> run en=0 in=0x0 out=0x0 en=0 in=0x1 out=0x0 en=0 in=0x2 out=0x0 en=0 in=0x3 out=0x0 en=0 in=0x4 out=0x0 en=0 in=0x5 out=0x0 en=0 in=0x6 out=0x0 en=0 in=0x7 out=0x0 en=0 in=0x8 out=0x0 en=0 in=0x9 out=0x0 en=0 in=0xa out=0x0 en=0 in=0xb out=0x0 en=0 in=0xc out=0x0 en=0 in=0xd out=0x0 en=0 in=0xe out=0x0 en=0 in=0xf out=0x0 en=1 in=0x0 out=0x1 en=1 in=0x1 out=0x2 en=1 in=0x2 out=0x4 en=1 in=0x3 out=0x8 en=1 in=0x4 out=0x10 en=1 in=0x5 out=0x20 en=1 in=0x6 out=0x40 en=1 in=0x7 out=0x80 en=1 in=0x8 out=0x100 en=1 in=0x9 out=0x200 en=1 in=0xa out=0x400 en=1 in=0xb out=0x800 en=1 in=0xc out=0x1000 en=1 in=0xd out=0x2000 en=1 in=0xe out=0x4000 en=1 in=0xf out=0x8000 ncsim: *W,RNQUIE: Simulation is complete.

Common Beginner Mistakes
Mistake #1: Using assign with reg data types
Why it's problematic: The assign statement can only drive wire types. Attempting to use assign with a reg causes a compilation error because reg variables are driven by procedural assignments inside always or initial blocks, not continuous assignments.
Best Practice: Use wire for all signals driven by assign. Use reg only for signals assigned in procedural blocks (always, initial). For combinational logic, choose between assign (for simple equations) or always @(*) (for complex logic with if-else or case).
Mistake #2: Multiple assign statements driving the same wire
Why it's problematic: If two or more assign statements attempt to drive the same wire, you create a multi-driver conflict. In simulation, this typically produces 'X' (unknown) values. In synthesis, this either causes an error or creates unintended tri-state logic with potential bus contention.
Best Practice: Ensure each wire is driven by exactly one assign statement or one module output port. If you need conditional assignment, use the ternary operator (?:) or combine conditions with Boolean logic in a single assign statement.
Mistake #3: Forgetting operator precedence in complex expressions
Why it's problematic: Verilog has specific operator precedence rules. For example, & (AND) has higher precedence than | (OR), so a | b & c evaluates as a | (b & c), not (a | b) & c. This can lead to incorrect logic that's difficult to debug.
Best Practice: Use parentheses liberally to make your intent explicit, even when not strictly required. Write (a | b) & c instead of relying on precedence rules. This improves readability and prevents errors. See the operator precedence table for reference.
Mistake #4: Mixing blocking and non-blocking assignments with assign
Why it's problematic: Beginners sometimes confuse continuous assignments (assign) with procedural assignments (= or <= inside always blocks). The assign keyword creates hardware that continuously updates, while procedural assignments execute sequentially when triggered by sensitivity lists or events.
Best Practice: Use assign for combinational logic on wires outside procedural blocks. Use = (blocking) for combinational logic inside always @(*) blocks. Use <= (non-blocking) for sequential logic inside clocked always @(posedge clk) blocks. Keep these three paradigms separate.
For more complex combinational logic with conditional statements, explore always blocks for combinational logic. To add memory elements and state, see sequential logic with always. Understanding how these circuits synthesize is covered in the digital design flow tutorial.
Summary
In this tutorial, you learned:
- The
assignstatement creates continuous assignments forwiretypes, synthesizing to zero-latency combinational logic - Common combinational circuits like adders, multiplexers, decoders, and demultiplexers can be elegantly implemented using
assignstatements - The ternary operator (
?:) and shift operators (<<) provide compact syntax for multiplexers and decoders - Each wire must be driven by exactly one
assignstatement to avoid multi-driver conflicts - Using explicit parentheses and proper operator precedence prevents logic errors in complex Boolean equations