Loading header...

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 assign for 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.

flowchart LR A[a] --> AND1["a and b"] B[b] --> AND1 A --> AND2["a and c"] C[c] --> AND2 B --> AND3["b and c"] C --> AND3 AND1 --> OR["or"] AND2 --> OR AND3 --> OR OR --> Y[y]

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 case statements without a default.
  • 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:

  1. with nested conditional expressions;
  2. with always @* and case.

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 proc pass 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.

Mind Map

mindmap root((Combinational Verilog)) Core concept No stored state y equals f of inputs LUT truth table No clock edge Applications Mux Decoder Encoder ALU flags Comparators Calculations Cases equals two to n Sum width needs carry bit LUT inputs limit fan in Path delay between flops Design rules assign for simple logic always star for decisions Blocking equals in comb Default every output Cover every case Practical checks Exhaustive sim for small n Lint latch warnings Width warnings clean Review priority depth Check timing path Common mistakes Missing default Incomplete case Unsized constants Hidden priority chain Treating HDL as software