Hello! In our last lesson, we mastered modeling multiplexers using the assign statement and the conditional operator. This "dataflow" approach is excellent for logic that can be described with a single, continuous expression.
Today, we're going to learn a different, more versatile method for describing combinational logic: the procedural always block. This approach allows you to describe the behavior of a circuit using more complex statements, similar to a traditional programming language. By the end of this lesson, you will be able to model combinational logic using an always @(*) block, a fundamental skill for any Verilog designer.
1. Dataflow vs. Procedural Modeling
So far, you've used dataflow modeling. The assign statement creates a direct, continuous connection, like soldering a wire from a logic gate's output to an input.
Procedural modeling is different. Instead of describing connections, you describe a block of logic that executes under specific conditions. The primary tool for this is the always block.
Let's look at how these two styles compare for the same simple circuit.

The always block on the right introduces some new syntax: always @(*). Let's break it down:
always: This keyword signifies a block of code that can be executed repeatedly during simulation.@: The "at" symbol introduces a sensitivity list, which is the trigger that causes the block to execute.(*): This is a special Verilog-2001 shorthand that means "execute this block if any signal that is read inside the block changes its value." For combinational logic, this is exactly what we want. It ensures the output is re-evaluated whenever an input changes.
2. The Three Rules of Combinational always Blocks
When you use an always block to model purely combinational logic, you must follow three important rules to ensure your code behaves correctly and synthesizes into the hardware you expect.
Rule 1: Use the always @(*) Sensitivity List
Before the always @(*) shorthand existed, designers had to manually list every single input to the logic. For example: always @(a or b or c or d). Forgetting an input was a common and dangerous bug, as it would cause the simulation to behave differently from the final synthesized hardware.
Using always @(*) is the modern, safe standard. It automatically makes the block sensitive to all the necessary signals, preventing this mismatch.
Verilog always Block - ChipVerify
This article from ChipVerify explains the fundamentals of the always block.
First, read the section "Combinational Element Design Example" to see a clear example of an always block modeling a simple logic circuit. Then, read "Common Beginner Mistakes," focusing on Mistake #1: Incomplete Sensitivity List. This will reinforce why always @(*) is so important.
Rule 2: Declare Outputs as reg
This is a crucial and often confusing Verilog rule. Any signal that gets assigned a value inside a procedural block (like always) must be declared as a reg type.
A common misconception is that reg always implies a hardware register (like a flip-flop). This is not true. In this context, reg simply means it's a variable that can hold a value and be assigned to procedurally. The synthesis tool is smart enough to know that if you follow the rules for combinational logic, it should create gates, not a flip-flop.
Notice in the combo module from the ChipVerify article, the output o is declared as output reg o.
Rule 3: Use Blocking Assignments (=)
Inside a combinational always block, you should use blocking assignments, denoted by a single equals sign (=).
y = a & b;
This means the expressions are evaluated in the order they appear, just like in a standard programming language. The first statement finishes completely before the next one starts. There is another assignment operator, non-blocking (<=), which is used for sequential logic. We will cover the critical differences between them in a later module. For now, just remember: combinational always blocks use blocking (=) assignments.
3. Example: Modeling a Decoder with a case Statement
While a nested conditional operator works for a multiplexer, it becomes clumsy for other components like decoders. A case statement inside an always block is a much cleaner solution. A case statement checks an expression and executes different code blocks based on its value.
This video provides an excellent walkthrough of building a 3-to-8 decoder using this exact technique.
FPGA #12 - Verilog Always Pt. I (Combinational Circuits)
This video from the "John's Basement" channel gives a great demonstration of building a decoder.
Please watch the segment explaining the 3-to-8 decoder implementation. As you watch, focus on: The overall structure of the always @(*) block, using begin and end to enclose multiple statements. How the case statement cleanly maps the 3-bit input to the 8-bit output. The confirmation that the output y must be declared as a reg.
4. Your Turn: Code and Simulate a 2-to-4 Decoder
It's time to apply what you've learned. Your task is to implement and test a 2-to-4 decoder with an enable signal.
Step 1: Write the DUT (Design Under Test)
Create a file named decoder_2_to_4.v. The module should have a 2-bit input in, a 1-bit enable en, and a 4-bit output out.
- If
enis high, the decoder is active. - If
enis low, all output bits should be0.
Use an if statement to check the enable, and a case statement inside it to handle the decoding logic.
// File: decoder_2_to_4.v
// Description: A 2-to-4 decoder with an active-high enable.
module decoder_2_to_4 (
input en,
input [1:0] in,
output reg [3:0] out // Remember: output must be 'reg'
);
// Procedural block for combinational logic
always @(*) begin
if (en) begin
// If enabled, use a case statement to decode 'in'
case (in)
2'b00: out = 4'b0001;
2'b01: out = 4'b0010;
2'b10: out = 4'b0100;
2'b11: out = 4'b1000;
default: out = 4'b0000; // Good practice to have a default
endcase
end else begin
// If not enabled, output is all zeros
out = 4'b0000;
end
end
endmodule
Self-Correction Note: A default case is good practice to ensure out is always assigned a value, which prevents unintended behavior. We'll explore exactly why in the next lesson.
Step 2: Write the Testbench
Create decoder_2_to_4_tb.v to test your decoder. The stimulus should cycle through all possible inputs, including when the decoder is disabled.
// File: decoder_2_to_4_tb.v
`timescale 1ns / 1ps
module decoder_2_to_4_tb;
// Testbench signals
reg en_tb;
reg [1:0] in_tb;
wire [3:0] out_tb;
// Instantiate the DUT
decoder_2_to_4 dut (
.en(en_tb),
.in(in_tb),
.out(out_tb)
);
// Stimulus block
initial begin
$monitor("Time=%0t | en=%b in=%b | out=%b", $time, en_tb, in_tb, out_tb);
// Test Case 1: Decoder disabled
en_tb = 0;
in_tb = 2'b00; #10;
in_tb = 2'b01; #10;
in_tb = 2'b10; #10;
in_tb = 2'b11; #10;
// Test Case 2: Decoder enabled
en_tb = 1;
in_tb = 2'b00; #10; // Expect 0001
in_tb = 2'b01; #10; // Expect 0010
in_tb = 2'b10; #10; // Expect 0100
in_tb = 2'b11; #10; // Expect 1000
#10;
$finish;
end
endmodule
Step 3: Simulate and Verify
Compile and run your files using the tools you are familiar with. The $monitor task will print the signal values to the console every time one of them changes, making it easy to verify the output. Check that the out_tb signal behaves as expected for each combination of en_tb and in_tb.
Conclusion
Great work! You have now learned the second major style for modeling combinational logic in Verilog. While assign is great for simple expressions, the always @(*) block gives you the power to describe much more complex behavior using if, case, and other procedural statements.
Key Takeaways:
- The
always @(*)block is the standard procedural method for modeling combinational logic. - The
(*)automatically makes the block sensitive to any changes in its inputs. - Any signal assigned a value inside an
alwaysblock must be declared asreg. - Use blocking assignments (
=) for combinational logic inside analwaysblock. casestatements are a powerful tool for implementing logic like decoders.
In today's exercise, we made sure our if and case statements covered every possible condition. In our next lesson, we will investigate what happens if you don't. This can lead to a common but tricky bug called an unintended latch, and we'll learn exactly how to identify and avoid it.