The verilog always block can be used for both sequential and combinational logic. A few design examples were shown using an assign statement in a previous article. The same set of designs will be explored next using an always block.

12 min read | Beginner to Intermediate

What You'll Learn

  • Write combinational logic using always blocks with proper sensitivity lists
  • Apply blocking assignments (=) for combinational logic modeling
  • Implement digital circuits (adders, muxes, decoders) using always blocks
  • Compare assign vs always approaches for identical hardware synthesis

Syntax

Basic syntax for combinational logic with always blocks:


// Complete sensitivity list - all inputs must be included
always @ (input1 or input2 or input3) begin
 output = expression; // Use blocking assignment (=)
end

// Verilog-2001 shorthand (use @*; SystemVerilog also offers always_comb)
always @* begin
 output = expression;
end

Use blocking assignments (=) when modeling combinational logic with an always block to avoid simulation/synthesis mismatches

assign vs always Block Comparison

Featureassign Statementalways Block
Output Typewire onlyreg only
SensitivityAutomatic (continuous)Manual (requires sensitivity list)
AssignmentContinuous assignmentBlocking (=) recommended
Procedural CodeNo (single expression only)Yes (if/case statements allowed)
Synthesis ResultIdentical hardware gates

Example #1 : Simple combinational logic

The code shown below implements a simple digital combinational logic which has an output signal called z of type reg that gets updated whenever one of the signals in the sensitivity list changes its value. The sensitivity list is declared within parentheses after the @ operator.


module combo ( 	input 	a, b, c, d, e,
								output 	reg z);

	// Sensitivity list contains ALL inputs
	// Logic updates whenever any input changes
	always @ ( a or b or c or d or e) begin
		// Blocking assignment (=) for combinational logic
		// Implements: z = (a AND b) OR ((c XOR d) AND (NOT e))
		z = ((a & b) | (c ^ d) & ~e);
	end

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.

simple combinational logic with assign

Real-World Application

Intel/AMD CPU Control Logic: Complex boolean expressions like this are used in instruction decode units to determine control signals based on opcode bits, addressing modes, and processor state flags. Modern CPUs have thousands of such combinational logic blocks.

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;
 wire z;
 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;
 #10;
 end
 end
endmodule
Output
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.

Note that both methods, assign and always, get implemented into the same hardware logic.

Example #2: Half Adder

The half adder module accepts two scalar inputs a and b and uses combinational logic to assign the output signals 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 reg	sum, cout);

	// Sensitivity list: both inputs a and b
	always @ (a or b) begin
		// Concatenation {cout, sum} receives 2-bit result
		// a + b produces: cout (carry) in bit[1], sum in bit[0]
		{cout, sum} = a + b;
	end

endmodule
half adder circuit with assign

Testbench


module tb;
	// Declare testbench variables
 reg a, b;
 wire sum, cout;
 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
Output
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

An always block can be used to describe the behavior of a full adder to drive the outputs sum and cout.


module fa (	input 	a, b, cin,
			output reg	sum, cout);

 // All THREE inputs in sensitivity list
 always @ (a or b or cin) begin
 	// Three input addition: a + b + cin
 	// Result is 2 bits: cout (bit[1]) and sum (bit[0])
 {cout, sum} = a + b + cin;
 end

endmodule
full adder circuit with assign

Testbench


module tb;
 reg a, b, cin;
 wire sum, cout;
 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 cout=%0b sum=%0b", a, b, cin, cout, sum);

 for (i = 0; i < 8; i = i + 1) begin
 {a, b, cin} = i;
 #10;
 end
 end
endmodule
Output
ncsim> run
a=0 b=0 cin=0 cout=0 sum=0
a=0 b=0 cin=1 cout=0 sum=1
a=0 b=1 cin=0 cout=0 sum=1
a=0 b=1 cin=1 cout=1 sum=0
a=1 b=0 cin=0 cout=0 sum=1
a=1 b=0 cin=1 cout=1 sum=0
a=1 b=1 cin=0 cout=1 sum=0
a=1 b=1 cin=1 cout=1 sum=1
ncsim: *W,RNQUIE: Simulation is complete.

Real-World Application

ARM Cortex-M ALU Design: Full adders are chained together to create ripple-carry adders for 32-bit or 64-bit arithmetic operations. ARM's Cortex-M series uses optimized adder designs (carry-lookahead) built from these fundamental full adder cells for energy-efficient embedded processors.

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.


module mux_2x1 (input 	a, b, sel,
				output 	reg c);

 // All inputs in sensitivity list
 always @ ( a or b or sel) begin
 	// Ternary operator: condition ? true_value : false_value
 	// If sel=1, output c = a; if sel=0, output c = b
	c = sel ? a : b;
 end
endmodule
2x1 multiplexer

Incomplete sensitivity lists create simulation/synthesis mismatches. Always include ALL inputs that affect the output in the sensitivity list (or use @*, available since Verilog-2001)

Testbench


module tb;
	// Declare testbench variables
 reg a, b, sel;
 wire c;
 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);

 for (i = 0; i < 3; i = i + 1) begin
 {a, b, sel} = i;
 #10;
 end
 end
endmodule
Output
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
ncsim: *W,RNQUIE: Simulation is complete.

Example #5: 1x4 Demultiplexer

The demultiplexer uses a combination of sel and f inputs to drive the different output signals. Each output signal is of type reg and used inside an always block that gets updated based on changes in the signals listed in the sensitivity list.


module demux_1x4 (	input 				f,
										input [1:0]	 	sel,
										output reg		a, b, c, d);

 // Sensitivity list: input signal f and 2-bit selector sel
 always @ ( f or sel) begin
 	// Each output gets f signal gated by specific sel combination
 	// Output a: active when sel=00
 a = f & ~sel[1] & ~sel[0];
 	// Output b: active when sel=10
 b = f & sel[1] & ~sel[0];
 	// Output c: active when sel=01
 c = f & ~sel[1] & sel[0];
 	// Output d: active when sel=11
 d = f & sel[1] & sel[0];
 end

endmodule
1x4 demultiplexer

Testbench


module tb;
	// Declare testbench variables
 reg f;
 reg [1:0] sel;
 wire a, b, c, d;
 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
Output
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


module dec_3x8 ( 	input 			en,
					input 	[3:0] 	in,
					output reg [15:0] 	out);

 // Sensitivity list: enable and 4-bit input
 always @ (en or in) begin
 	// When en=1: shift 1 left by 'in' positions (one-hot encoding)
 	// When en=0: all outputs are 0
 	// Example: in=4'b0101 (5) -> out=16'b0000_0000_0010_0000 (bit 5 set)
 out = en ? 1 << in: 0;
 end

endmodule
4x16 decoder

Testbench


module tb;
 reg en;
 reg [3:0] in;
 wire [15:0] out;
 integer i;

 dec_3x8 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);

 for (i = 0; i < 32; i = i + 1) begin
 {en, in} = i;
 #10;
 end
 end
endmodule
Output
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 Non-Blocking Assignment (<=) in Combinational Logic


always @ (a or b) begin
 sum <= a ^ b; // WRONG: Non-blocking for combinational
end

Why it's problematic: Non-blocking assignments (<=) schedule updates at the end of the simulation time step, causing race conditions and simulation/synthesis mismatches. Synthesized hardware will be combinational (immediate), but simulation behavior will be delayed by one delta cycle.

Best Practice: Use blocking assignments (=) for all combinational logic in always blocks. Reserve non-blocking (<=) for sequential logic with clock edges.

Mistake #2: Incomplete Sensitivity List


always @ (sel) begin // WRONG: Missing 'a' and 'b'
 out = sel ? a : b;
end

Why it's problematic: If a or b changes but sel doesn't, the always block won't execute in simulation. Synthesis tools ignore sensitivity lists and infer combinational logic correctly, creating a simulation/synthesis mismatch where simulation shows latches/old values but hardware works correctly.

Best Practice: Include ALL inputs that affect any output in the sensitivity list. Use always @* (Verilog-2001) or, in SystemVerilog, always_comb to automatically infer the complete list.

Mistake #3: Mixing Assignment Types (reg vs wire)


output wire result; // WRONG: can't assign to wire in always block
always @ (a or b) begin
 result = a & b; // Error: procedural assignment to wire
end

Why it's problematic: Verilog requires outputs assigned inside always blocks to be declared as reg type, even though the hardware is combinational. Attempting to assign to a wire in a procedural block causes a compilation error.

Best Practice: Declare outputs as output reg when using always blocks. The reg keyword is just a data type--it doesn't imply sequential logic. SystemVerilog's logic type eliminates this confusion.

Mistake #4: Creating Unintentional Latches with Missing else/default


always @ (sel or a) begin
 if (sel)
 out = a;
 // WRONG: No else clause - out retains previous value when sel=0
end

Why it's problematic: When the if condition is false and there's no else clause, the output retains its previous value, requiring a latch (memory element) in hardware. This creates unexpected sequential behavior in what should be pure combinational logic and often causes synthesis warnings.

Best Practice: Ensure every signal is assigned a value in ALL execution paths. Use else clauses in if statements and default cases in case statements. Better yet, assign default values at the start of the always block.

Real-World Application: Barrel shifters are critical components in processors for implementing efficient shift/rotate operations in a single clock cycle. The ARM instruction set includes barrel shifter functionality that can shift the second operand before ALU operations, enabling powerful single-instruction operations like ADD R0, R1, R2 LSL #2 (R0 = R1 + R2*4) used for array indexing and bit manipulation.

Summary

In this tutorial, you learned:

  • always blocks with complete sensitivity lists model combinational logic identically to assign statements
  • Blocking assignments (=) must be used for combinational logic to avoid simulation mismatches
  • Outputs in always blocks require reg data type (Verilog) or logic (SystemVerilog)
  • Incomplete sensitivity lists cause simulation/synthesis mismatches--use @* (or always_comb in SystemVerilog)
  • Common digital circuits (adders, muxes, demuxes, decoders) synthesize to identical gates regardless of assign vs always
  • Missing else/default clauses create unintentional latches--ensure all paths assign all signals
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5