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
alwaysblocks with proper sensitivity lists - Apply blocking assignments (
=) for combinational logic modeling - Implement digital circuits (adders, muxes, decoders) using
alwaysblocks - Compare
assignvsalwaysapproaches 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
| Feature | assign Statement | always Block |
|---|---|---|
| Output Type | wire only | reg only |
| Sensitivity | Automatic (continuous) | Manual (requires sensitivity list) |
| Assignment | Continuous assignment | Blocking (=) recommended |
| Procedural Code | No (single expression only) | Yes (if/case statements allowed) |
| Synthesis Result | Identical 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.

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
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

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
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

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
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

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
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

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
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

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
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:
alwaysblocks with complete sensitivity lists model combinational logic identically toassignstatements- Blocking assignments (
=) must be used for combinational logic to avoid simulation mismatches - Outputs in
alwaysblocks requireregdata type (Verilog) orlogic(SystemVerilog) - Incomplete sensitivity lists cause simulation/synthesis mismatches--use
@*(oralways_combin SystemVerilog) - Common digital circuits (adders, muxes, demuxes, decoders) synthesize to identical gates regardless of
assignvsalways - Missing
else/defaultclauses create unintentional latches--ensure all paths assign all signals