HomeFPGA from ScratchDay 6
DAY 6 · HDL FUNDAMENTALS

Combinational Logic in HDL (Verilog)

By EcrioniX · Updated Jun 8, 2026

On Day 5 you wrote your first tiny module. Now let's master the foundation of all digital design: combinational logic. By the end you'll know the two ways to write it in Verilog (assign vs always), every operator you'll use, the single most common beginner bug (the latch trap), and you'll build a complete 4:1 multiplexer — design and testbench, with the exact output to expect.

1. What "combinational" means

Combinational logic = output depends only on the current inputs. No memory, no clock. Change an input → the output changes a tiny moment later (propagation delay). Gates, multiplexers, adders, comparators, decoders — all combinational.

The opposite is sequential logic (flip-flops, registers) that remembers past values and updates on a clock edge — that's Day 7. Keeping these two straight is the whole game in HDL.

Combinational: output = f(inputs) only a b logicgates / mux / adder y no clock · no memory · changes the instant inputs change
Figure — Combinational logic: pure input→output, no stored state.

2. Two ways to write it: assign vs always

Verilog gives you two styles for combinational logic. They describe the same hardware — pick by readability.

StyleUse whenTarget type
assign y = ...;short expressions (one-liners)wire
always @(*) begin ... endmulti-branch logic (if / case)reg

Two ways to write the same 2:1 mux:

two_styles.v — same mux, two ways
// Style A: continuous assign + ternary  (target is a wire)
module mux2_a (input a, input b, input sel, output y);
    assign y = sel ? b : a;     // sel=1 -> b, else a
endmodule

// Style B: procedural always @(*)  (target must be reg)
module mux2_b (input a, input b, input sel, output reg y);
    always @(*) begin
        if (sel) y = b;
        else     y = a;
    end
endmodule

3. wire vs reg — the rule

✅ The only rule you need

Assigned with assign or by a module connection → it's a wire. Assigned inside an always/initial block → it's a reg. And crucially: reg does not mean a hardware register. A reg assigned in a combinational always @(*) still becomes plain combinational logic. The name is historical — ignore the literal word.

4. The operators you'll actually use

GroupOperatorsExample
Bitwise&  |  ^  ~y = a & b;
Logical&&  ||  !if (a && b)
Comparison== != < <= > >=eq = (a==b);
Arithmetic+  -  * s = a + b;
Shift<<  >>y = a << 2;
Ternary? :y = s ? b : a;
Concatenate{ }y = {a, b};
Reduction&  |  ^ (unary)p = ^data; // parity

Two that trip up beginners: bitwise (&) works bit-by-bit on a vector, while logical (&&) treats the whole value as true/false. And reduction (^data) ANDs/ORs/XORs all bits of one vector into a single bit — XOR-reduction is an instant parity generator.

5. ⚠️ The latch trap (read this twice)

The #1 combinational bug

In a combinational always @(*), if you don't assign the output on every path, the synthesizer keeps the old value — which needs memory — so it silently builds an unwanted latch. Symptoms: weird simulation, timing failures, "why is my comb logic remembering things?"

Three fixes: (1) always include an else; (2) put a default: in every case; (3) assign a default value at the top of the block, then override. The third is bulletproof.

latch.v — bad vs good
// BAD: no else -> infers a latch (y "remembers" when sel==0)
always @(*) begin
    if (sel) y = b;          // what happens when sel==0? -> latch!
end

// GOOD: default first, then override -> pure combinational
always @(*) begin
    y = a;                   // default covers every path
    if (sel) y = b;
end

6. Build it: a 4:1 multiplexer (design)

A 4:1 mux picks one of four inputs using a 2-bit select. We'll use a case with a default — clean and latch-free. First, the ports, explained cleanly:

PortDirWidthMeaning
d0,d1,d2,d3input1 bit eachthe four data inputs to choose from
selinput2 bitsselect line (00→d0, 01→d1, 10→d2, 11→d3)
youtput1 bitthe chosen input appears here
mux4.v — 4:1 multiplexer (design)
// 4:1 multiplexer - combinational, latch-free via default
module mux4 (
    input  wire       d0, d1, d2, d3,  // four data inputs
    input  wire [1:0] sel,             // 2-bit select
    output reg        y                // chosen output
);
    always @(*) begin
        case (sel)
            2'b00:   y = d0;
            2'b01:   y = d1;
            2'b10:   y = d2;
            2'b11:   y = d3;
            default: y = 1'b0;         // safety: no inferred latch
        endcase
    end
endmodule

7. Test it: the testbench (with expected output)

Per our golden rule — every design module gets a testbench. This one drives each sel value with a known pattern on d0–d3 and checks the result with $display:

tb_mux4.v — testbench
`timescale 1ns/1ps
module tb_mux4;
    reg        d0,d1,d2,d3;
    reg  [1:0] sel;
    wire       y;

    // instantiate the design under test
    mux4 dut (.d0(d0), .d1(d1), .d2(d2), .d3(d3), .sel(sel), .y(y));

    integer errors = 0;
    task check(input exp);
        begin
            #1;  // let combinational logic settle
            if (y !== exp) begin
                $display("FAIL sel=%b -> y=%b (expected %b)", sel, y, exp);
                errors = errors + 1;
            end else
                $display("ok   sel=%b -> y=%b", sel, y);
        end
    endtask

    initial begin
        // distinct pattern so we can see WHICH input was routed
        d0=1'b1; d1=1'b0; d2=1'b1; d3=1'b0;

        sel=2'b00; check(d0);   // expect 1
        sel=2'b01; check(d1);   // expect 0
        sel=2'b10; check(d2);   // expect 1
        sel=2'b11; check(d3);   // expect 0

        if (errors==0) $display("ALL TESTS PASSED");
        else           $display("%0d TEST(S) FAILED", errors);
        $finish;
    end
endmodule

Run it (Icarus Verilog, or paste both files into our browser Verilog simulator):

run.sh — compile & simulate
iverilog -o mux4_tb mux4.v tb_mux4.v
vvp mux4_tb

Expected output:

expected output
ok   sel=00 -> y=1
ok   sel=01 -> y=0
ok   sel=10 -> y=1
ok   sel=11 -> y=0
ALL TESTS PASSED

💡 A 4:1 mux is a 4-way train switch

Four tracks (d0–d3) lead into one junction. The 2-bit sel is the switch operator: set it to 10 and only track d2's train reaches the output. Nothing is stored — flip the switch and a different track connects instantly. That "instant, no memory" behaviour is exactly what makes it combinational.

🎯 Day 6 takeaways

Quick check

  1. What single fact defines combinational logic?
  2. When must a signal be reg instead of wire?
  3. What causes an inferred latch, and how do you prevent it?
  4. What does XOR-reduction (^data) compute?

FAQ

What is combinational logic?

Logic whose output depends only on current inputs — no memory, no clock. Gates, muxes, adders, decoders.

assign vs always?

assign drives a wire continuously (best for short expressions); always @(*) uses if/case on a reg (best for multi-branch). Same hardware.

What is an inferred latch?

When a comb always block doesn't assign the output on every path, the tool keeps the old value via an unwanted latch. Fix with defaults/else.

wire vs reg?

wire = driven by assign/connections; reg = assigned in a procedural block. reg in always @(*) is still combinational.

Previous
← Day 5: Verilog vs VHDL & first module

← Back to the full roadmap  ·  Open the Verilog simulator →