Combinational Logic in Verilog
Combinational logic has no memory. At any instant its outputs are a pure function of the current inputs. In an FPGA, that function is implemented with lookup tables (LUTs), carry chains, multiplexers, and routing. Verilog can describe the function compactly, but the result is hardware, not software that runs line by line.
Learning Objectives
By the end of this lesson, you should be able to:
- translate a truth table into Verilog expressions;
- use
assignfor simple wire-level logic; - use
always @*for larger combinational decisions; - give every output a value on every path;
- identify latch, width, priority, and incomplete-case mistakes before synthesis.
Hardware Meaning
This Verilog:
assign y = (a & b) | (a & c) | (b & c);
does not call a function three times. It creates three AND functions feeding one OR function. A synthesis tool packs that Boolean network into one or more FPGA LUTs, depending on the target LUT width and surrounding logic.
The important rule is simple: if no clock edge is involved and no old value is required, write combinational logic.
Continuous Assignments
Use assign when the output is naturally a Boolean or arithmetic expression.
module majority3 (
input wire a,
input wire b,
input wire c,
output wire y
);
assign y = (a & b) | (a & c) | (b & c);
endmodule
Good uses of continuous assignments include simple gates, bit selections, comparisons, one-line muxes, and combinational flags:
assign zero = (count == 8'd0);
assign msb = data[7];
assign selected = sel ? b : a;
assign sum = {1'b0, x} + {1'b0, y};
Make widths explicit. In the sum example, the leading 1'b0 extends each operand so the carry-out is not silently lost.
Procedural Combinational Blocks
Use always @* when the logic is easier to read as decisions. Inside a combinational block, use blocking assignment = and assign defaults before the decision tree.
module mux4 (
input wire [1:0] sel,
input wire [7:0] d0,
input wire [7:0] d1,
input wire [7:0] d2,
input wire [7:0] d3,
output reg [7:0] y
);
always @* begin
y = 8'h00;
case (sel)
2'b00: y = d0;
2'b01: y = d1;
2'b10: y = d2;
2'b11: y = d3;
default: y = 8'h00;
endcase
end
endmodule
The first assignment is not decoration. It prevents an inferred latch if the case statement is later edited incorrectly. The default branch is still useful documentation and protects against unknown or future states in simulation.
SystemVerilog users should prefer always_comb where the tool supports it:
always_comb begin
y = 8'h00;
unique case (sel)
2'b00: y = d0;
2'b01: y = d1;
2'b10: y = d2;
2'b11: y = d3;
endcase
end
always_comb gives stronger checks than Verilog-2001 always @*, but many beginner FPGA flows still use plain Verilog. Know which language mode your toolchain is using.
Worked Example: Priority Encoder
A 4-input priority encoder reports whether any request is active and returns the highest active request index.
| req[3:0] pattern | valid | code |
|---|---|---|
0000 |
0 | 00 |
xxx1 with only bit 0 highest |
1 | 00 |
xx10 with bit 1 highest |
1 | 01 |
x100 with bit 2 highest |
1 | 10 |
1000 with bit 3 highest |
1 | 11 |
module priority4 (
input wire [3:0] req,
output reg valid,
output reg [1:0] code
);
always @* begin
valid = 1'b0;
code = 2'b00;
if (req[3]) begin
valid = 1'b1;
code = 2'd3;
end else if (req[2]) begin
valid = 1'b1;
code = 2'd2;
end else if (req[1]) begin
valid = 1'b1;
code = 2'd1;
end else if (req[0]) begin
valid = 1'b1;
code = 2'd0;
end
end
endmodule
This is combinational even though it is written inside an always block. There is no clock in the sensitivity list, and both outputs receive defaults before the if chain.
Truth Tables and LUTs
A LUT implements a truth table. A 4-input LUT can implement any one-output Boolean function of four inputs. Larger functions are split across multiple LUTs and routing. That is why a truth table is still useful even when writing HDL: it tells you exactly what the hardware must compute.
For a 2-to-4 decoder:
module decoder2to4 (
input wire [1:0] sel,
output reg [3:0] y
);
always @* begin
y = 4'b0000;
case (sel)
2'b00: y = 4'b0001;
2'b01: y = 4'b0010;
2'b10: y = 4'b0100;
2'b11: y = 4'b1000;
default: y = 4'b0000;
endcase
end
endmodule
Each output bit is a separate Boolean function of sel[1:0]. Synthesis may implement the decoder in LUTs or merge it into downstream logic if the one-hot result feeds other logic.
Priority, Parallelism, and Timing
Verilog if and else if usually imply priority because earlier conditions win. A case over mutually exclusive selector values usually describes a multiplexer. Both are still combinational networks.
Priority logic can become deeper than expected:
assign grant =
req[7] ? 3'd7 :
req[6] ? 3'd6 :
req[5] ? 3'd5 :
req[4] ? 3'd4 :
req[3] ? 3'd3 :
req[2] ? 3'd2 :
req[1] ? 3'd1 : 3'd0;
For a small FPGA exercise this is fine. In a high-speed design, long priority chains can become timing paths that need pipelining or a tree structure.
Verification Checks
For small combinational modules, exhaustive simulation is practical. A module with n one-bit inputs has 2^n input combinations. For example, a 4-input function has 2^4 = 16 cases.
Use these checks before trusting the design:
- simulate every input combination when the input space is small;
- lint with warnings enabled;
- inspect synthesis warnings for inferred latches and width truncation;
- compare the generated schematic or technology view with your intended mux, decoder, or logic tree;
- check timing reports if the combinational path crosses a clock boundary between registers.
Common Mistakes
- Omitting a default assignment in
always @*. - Assigning only some outputs in some branches.
- Writing
casestatements without adefault. - Using nonblocking
<=in combinational logic without understanding tool behavior. - Depending on statement order as if it created real time delay.
- Mixing signed and unsigned operands without explicit casting.
- Ignoring width warnings on constants such as
1,0, and unsized arithmetic.
Practice
Write a 4-to-1 mux twice:
- with nested conditional expressions;
- with
always @*andcase.
Then write a small testbench that checks all four select values. Extend it to an 8-bit data bus and confirm the output width is still correct.
Summary
Combinational Verilog describes logic whose outputs depend only on present inputs. Use assign for compact expressions and always @* for readable decision logic. Defaults, complete cases, explicit widths, and simulation are the habits that keep FPGA combinational logic latch-free and reviewable.
Next: Exercise: Build Combinational Logic and a Testbench.
Further Reading
- Yosys manual, Verilog frontend and
procpass documentation. - Verilator warnings guide, especially latch and width warnings.
- Xilinx and Intel HDL coding guidelines for combinational logic.
- IEEE 1364 Verilog standard, procedural assignment and continuous assignment sections.