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.
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.
Verilog gives you two styles for combinational logic. They describe the same hardware — pick by readability.
| Style | Use when | Target type |
|---|---|---|
| assign y = ...; | short expressions (one-liners) | wire |
| always @(*) begin ... end | multi-branch logic (if / case) | reg |
Two ways to write the same 2:1 mux:
// 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
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.
| Group | Operators | Example |
|---|---|---|
| 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.
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.
// 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
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:
| Port | Dir | Width | Meaning |
|---|---|---|---|
| d0,d1,d2,d3 | input | 1 bit each | the four data inputs to choose from |
| sel | input | 2 bits | select line (00→d0, 01→d1, 10→d2, 11→d3) |
| y | output | 1 bit | the chosen input appears here |
// 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
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:
`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):
iverilog -o mux4_tb mux4.v tb_mux4.v vvp mux4_tb
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
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.
assign (wire, short expr) or always @(*) (reg, if/case).reg ≠ hardware register — in always @(*) it's still combinational.{}, reduction ^.ALL TESTS PASSED.reg instead of wire?^data) compute?Logic whose output depends only on current inputs — no memory, no clock. Gates, muxes, adders, decoders.
assign drives a wire continuously (best for short expressions); always @(*) uses if/case on a reg (best for multi-branch). Same hardware.
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 = driven by assign/connections; reg = assigned in a procedural block. reg in always @(*) is still combinational.