3.5.2 · D2HDL & Digital Design Flow

Visual walkthrough — Combinational logic in HDL

1,988 words9 min readBack to topic

We assume nothing. If you have never seen a truth table, a gate, or a line of HDL, start at line one — everything is built here.

Parent: Combinational logic in HDL.


Step 1 — What a "combinational rule" really is

WHAT. Imagine a light with one switch called and one wire of data called . We want to build a box that produces an output . A combinational box is a lookup table: for every possible input, it lists exactly one output. Nothing is left blank.

Here is a single wire that is either (off) or (on). is likewise or . So there are only four possible input situations. A complete combinational rule must fill in all four boxes.

WHY. A rule with a blank box is not a function — it does not say what to do. And "what to do when I didn't say" is the exact crack through which memory leaks in. So before any code, we look at the table and ask: are all boxes filled?

PICTURE. The full 4-row table. The accent (red) column is — the thing we must define in every row.

Figure — Combinational logic in HDL

Step 2 — Writing the tempting (broken) code

WHAT. A beginner writes exactly what they care about:

always @(*) begin
    if (enable)
        y = data;   // only says what happens when enable == 1
end

Let us read this like a sentence. always @(*) means "re-run this whole block whenever any input changes." The if (enable) means "when is , do the next line." y = data copies onto .

WHY. It feels efficient: "I only need when is on, so why write the off case?" This intuition is the trap — it is why the bug feels correct.

PICTURE. The code drawn as a decision fork. The red branch (the if body) is the only path that assigns . The other branch is empty — no assignment.

Figure — Combinational logic in HDL

Step 3 — Overlaying the code onto the table

WHAT. We now colour the truth-table rows by which code path handles them. The two rows where are covered by path A (red — assigned). The two rows where are covered by path B — but path B does nothing, so those output cells stay blank.

WHY. This is the moment of proof. Step 1 said a combinational box needs all four boxes filled. Step 2's code only fills two. So the code does not describe the complete table — half of it is missing.

PICTURE. The table again, but now the two output cells are shown as red empty boxes: "undefined by the code."

Figure — Combinational logic in HDL

Step 4 — What the tool must do with a hole

WHAT. The synthesizer (the program that turns your code into gates) reads the code and asks: "When , what should be?" Your code never answers. But real hardware wires cannot be "undefined" — a wire must carry a value. So the tool needs a rule for the silent case. The only rule consistent with "you didn't tell me to change it" is: keep whatever was before.

WHY. "Keep the previous value" is memory. To keep a value, the hardware must store it — and a storage element that holds a value while and passes new data while has a name: a transparent latch. The tool is not being clever or dumb; it is doing the only thing that satisfies your incomplete description.

PICTURE. A single arrow diagram: your code (with the hole) → the tool's forced question → the latch it must build. The latch symbol is the red key object.

Figure — Combinational logic in HDL

Step 5 — Watching the latch remember (a timing walk)

WHAT. Let us feed the buggy circuit real signals over time and watch . Suppose:

  1. follows → .
  2. drops to (while later changes to ) → stays , ignoring the new .
  3. rises to again → follows once more.

WHY. This is the observable symptom of the bug: holds a stale value through the whole window. That "stuck at the old value" plateau is the latch remembering. A pure combinational would have no dependence on the past at all — but here it clearly does.

PICTURE. Three stacked waveforms — , , and (red) . The red plateau where ignores is the memory made visible.

Figure — Combinational logic in HDL

Step 6 — The fix: fill every box with a default

WHAT. We add one line before the if so that no path can be silent:

always @(*) begin
    y = 1'b0;        // DEFAULT: fills the enable==0 rows
    if (enable)
        y = data;    // overrides in the enable==1 case
end

Read the flow: first is set to unconditionally. Then — only if — it is overwritten with . Because the block uses blocking = (executes in order, top to bottom, like signals rippling through gates), the second line simply replaces the first when it runs.

WHY. Now trace both paths: if , becomes then → defined. If , the if body is skipped, so keeps its default also defined. Every row of the Step 1 table is now filled. No hole ⇒ no forced memory ⇒ no latch. The tool builds a plain multiplexer instead.

PICTURE. The same table as Step 3, but now the two formerly-empty cells are filled with a red from the default — the hole is closed.

Figure — Combinational logic in HDL

Step 7 — The edge cases (so no scenario surprises you)

WHAT. We check the corners a careless reader might hit.

  • Multi-bit outputs. If is a bus (many wires), the default must cover every bit, e.g. y = 4'b0000;. A default that sets only some bits leaves the rest with holes → per-bit latches.
  • case without default. A case (sel) that lists some values but not all is the same hole in disguise — unlisted values leave silent → latch. Fix: add default: (Verilog) or when others => (VHDL). See Multiplexers, adders, decoders.
  • The "always driven" idiom. A continuous assign y = enable ? data : 1'b0; has no branch that can be skipped — the ?: names both outcomes. This is why assign is inherently safe (parent, Idiom 1).
  • Genuinely wanting memory. If you do want to hold — that is Sequential logic in HDL, built on purpose with a clock and non-blocking <=. The latch is only a bug when it appears in code that was meant to be combinational.

WHY. The latch trap is not one line of code — it is the general shape "a path that leaves an output unassigned." Every case above is that same shape. Recognising the shape is the real skill.

PICTURE. A "hole detector" flow: does some input combination leave the output unassigned? Yes → latch (red). No → combinational.

Figure — Combinational logic in HDL
Recall Reveal — spot the latch

Does always @(*) begin case(sel) 2'b00: y=a; 2'b01: y=b; endcase end infer a latch? ::: Yes — and are unlisted, so is unassigned there. Add a default:.


The one-picture summary

Everything on this page compressed into a single diagram: an unassigned path forces a hold, a hold is a latch, and a default closes the path.

Figure — Combinational logic in HDL
Recall Feynman: retell the whole walkthrough

Think of the box as a form with one row for every button combination. Step 1: a good combinational box fills in every row. Step 2–3: the tempting code only writes the " on" rows and leaves the " off" rows blank. Step 4: a real wire can't be blank, so the tool asks "what now?" and, getting no answer, decides "just keep the old value" — and to keep a value you need a tiny memory cell, the latch. Step 5: you can literally see it: wiggle while is off and sits frozen, remembering. Step 6: the cure is one line — write a default value first, so even the "off" rows are filled; now nothing is blank, the tool builds a clean selector, and the memory vanishes. Step 7: the same "blank row = latch" trap hides in multi-bit outputs and in case without default — spot the blank, fill the blank. That's the entire art: never leave a row empty.


Connections

  • Latch inference and how to avoid it — the rules this page derived from pictures.
  • Blocking vs Non-blocking assignments — why the default-then-override trick relies on blocking =.
  • Sequential logic in HDL — where holding a value is the goal, not a bug.
  • Multiplexers, adders, decoders — the clean circuit the tool builds once the holes are gone.
  • Boolean algebra and Karnaugh maps — the truth tables our steps kept filling in.
  • Synthesis and the digital design flow — the tool that turns these tables into gates.