Output depends only on inputs
Here is the defining rule of combinational logic: the output depends only on the inputs you have right now — never on what happened a moment ago. There is no memory, no stored state, nothing to remember. Change an input and the output re-settles; hold the inputs steady and the output is pinned. Think of it as a vending machine with no coin slot: the same buttons always give the same snack, every single time.
Formally, combinational logic computes a pure function of its inputs: y = f(a, b, c, …). The same inputs always map to the same output, with no hidden variable smuggled in from the past. That is why you can describe the whole block with a truth table — list every input combination, write the output beside it, and you have captured the block *completely*.
The multiplexer: a data chooser
The multiplexer — "mux" for short — is the most useful combinational block you will meet. It is a data selector: many inputs go in, a small select signal points at one of them, and that one input is copied to the single output. Picture a railway switch — many tracks converging, a lever choosing which train reaches the platform. The mux doesn't change the data; it just decides *which* data gets through.
A 2-to-1 mux takes two data inputs (`a`, `b`), one select line `s`, and produces `y`. When `s` is 0 you get `a`; when `s` is 1 you get `b`. In Boolean algebra that is exactly y = (~s & a) | (s & b) — read it as "if not-s, pass a; else pass b." In Verilog the same idea collapses to one elegant line.
module mux2(input a, b, s, output y); assign y = s ? b : a; // s picks the winner: 0 -> a, 1 -> b endmodule
Decoders, adders & the ALU
A decoder is the opposite of a mux's chooser instinct: it takes a small binary code and lights up exactly one of many outputs. A 2-to-4 decoder turns a 2-bit number (`00`, `01`, `10`, `11`) into four lines where only the matching one is high. Think of it as apartment mailboxes — a short address selects the one box that opens. Decoders are how a memory address picks a single row, and how an instruction's opcode picks a single operation.
An adder does real arithmetic with nothing but gates. The atom is the full adder: it takes two bits plus a carry-in and produces a sum bit and a carry-out. Chain N of them — each one's carry-out feeding the next one's carry-in — and you get a ripple-carry adder that sums two N-bit numbers. No loops, no clock; just a wide sheet of gates settling into the right answer.
module full_adder(input a, b, cin, output sum, cout); assign sum = a ^ b ^ cin; // XOR of all three bits assign cout = (a & b) | (cin & (a ^ b)); // carry if two or more are 1 endmodule
Stack these blocks and you get the ALU (arithmetic logic unit) — the calculator at the heart of every processor. An ALU is mostly an adder, some logic gates, and a mux that selects which result to emit (add? AND? compare?) based on an operation code. It is pure combinational logic: hand it two numbers and an op, and the answer falls out — no clock required.
Truth table → Boolean → assign
Here is the practitioner's loop, the one move that turns a *specification* into *hardware*. Start with a truth table: enumerate every input combination and the output you want. The table is the ground truth — unambiguous and complete. From there you distill a [[boolean-algebra|Boolean]] expression, then drop that expression into a single Verilog `assign`. Three outfits, one body.
Take a worked example: a 1-bit output that is high when at least two of three inputs (`a`, `b`, `c`) are 1 — the "majority vote" function. The truth table has eight rows; the output is 1 in exactly four of them (`011`, `101`, `110`, `111`). Writing one term per winning row gives y = a&b | a&c | b&c after you cancel the redundancy — three small AND terms OR'd together. (That tidy-up is [[boolean-algebra|Boolean]] simplification: fewer terms means fewer gates and a faster, smaller block.)
module majority(input a, b, c, output y); assign y = (a & b) | (a & c) | (b & c); // high when 2+ inputs are 1 endmodule
Propagation delay: logic takes time
Now the catch that bridges into timing. We keep saying combinational logic settles "instantly," but that is a polite lie. Every gate takes a tiny but real time to swing its output after its inputs change — its propagation delay (often written *t*pd). Inside, the gate is nudging tiny transistors and charging stray capacitance; charge takes time to move, so the output lags the input by picoseconds to nanoseconds.
Delays add up along a path. A signal that ripples through, say, eight gates from input to output pays each gate's toll in turn, one after another. The slowest such route through a block is its [[critical-path|critical path]] — the longest input-to-output journey — and *it* sets how fast the whole block can run. Your majority gate is quick; a 64-bit ripple-carry adder, whose carry must crawl across all 64 stages, is famously slow.
So the mental model upgrades: combinational logic isn't a magic lookup that happens at once — it's a landscape of gates where signals flow downhill and take time to arrive. Get the function right with truth tables and Boolean algebra; then respect the clock by minding the critical path. That second half — making the timing close — is the whole next guide.