Worked examples — Combinational logic in HDL
Before we begin, three plain-word anchors so nothing below uses an unearned symbol:
The scenario matrix
Every combinational-HDL bug or pattern falls into one of these cells. The examples below are labelled with the cell(s) they cover.
| # | Case class | Trigger condition | Safe / Buggy? | Example |
|---|---|---|---|---|
| C1 | Fully-specified if/else |
every path assigns output | ✅ combinational | Ex 1 |
| C2 | if with no else |
one path leaves output unassigned | ⚠️ latch | Ex 2 |
| C3 | case without default, incomplete |
missing selector values | ⚠️ latch | Ex 3 |
| C4 | case fixed by default |
all values covered | ✅ combinational | Ex 3 |
| C5 | Incomplete sensitivity list | manual @(a) misses b |
⚠️ sim ≠ silicon | Ex 4 |
| C6 | Blocking vs non-blocking ordering | wrong operator changes logic | ⚠️ wrong function | Ex 5 |
| C7 | Degenerate input — constant/tied signal | one input never changes | ✅ folds to simpler gate | Ex 6 |
| C8 | Multi-output block, partial assign | some outputs missing on a path | ⚠️ latch on that output only | Ex 7 |
| C9 | Real-world word problem | priority encoder / thermostat | ✅ if fully covered | Ex 8 |
| C10 | Exam twist — overlapping/priority if chain |
later branch shadows earlier | ✅ but subtle | Ex 9 |
The "sign/quadrant" analogue for this topic is the truth-table cell: every input combination is a cell, and our job is to prove every cell is covered. Missing cells = latches, exactly like a missing quadrant broke the naive arctan.
Example 1 — Fully-specified if/else (Cell C1)
Forecast: Guess — will this need a default? How many input cells must be covered, and are they all here?
-
Enumerate the cells.
selis one bit ⇒ exactly two cells:sel=0andsel=1. Why this step? Latch-freedom is a coverage claim; you cannot check coverage without listing what must be covered. -
Write the block with both branches.
always @(*) begin if (sel) y = b; else y = a; endWhy this step? The
elsefills thesel=0cell; theiffillssel=1. Both cells assigny. -
Coverage check. Look at the two rows of the truth table in Figure s01 — both are green (assigned).
Why this step? Green everywhere = no path leaves
yfloating = no latch.
Verify: Boolean form is . Plug sel=0: ✓. Plug sel=1: ✓. Both cells match the spec.
Example 2 — if with no else (Cell C2, the classic latch)
Forecast: What happens to y when enable = 0? Is there a rule for that cell?
-
List the cells of the controlling signal.
enable = 1andenable = 0. Why this step? Same coverage discipline as Ex 1. -
Trace
enable = 0. Theifbody is skipped. No statement assignsy. Why this step? An unassigned output on a reachable path is exactly the latch trigger from the definition box. -
Name the hardware. The tool must "hold the previous
y," so it builds a transparent latch enabled byenable. See Figure s02: the red cell (enable=0) is uncovered.Why this step? Making the missing cell visible is how you spot the bug in review.
-
Fix — default before the branch.
always @(*) begin y = 1'b0; // covers enable=0 if (enable) y = data; endWhy this step? The unconditional
y = 1'b0runs first; the later blockingifoverrides it whenenable=1. Now both cells assigny.
Verify: enable=1 ⇒ default overwritten ⇒ y=data ✓. enable=0 ⇒ default stands ⇒ y=0 (defined, no memory) ✓. Latch eliminated.
Example 3 — case without default, then fixed (Cells C3 → C4)
Forecast: How many cells does a 2-bit sel have? How many are listed?
-
Count the cells. 2 bits ⇒ values:
00, 01, 10, 11. Why this step? An -bit selector always has cells; here . -
Spot the gap. Only
00, 01, 10are listed.11is uncovered ⇒yunassigned whensel=11⇒ latch (Cell C3). See the red cell in Figure s03.Why this step? missing cell is enough for a latch.
-
Repair with
default(Cell C4).case (sel) 2'b00: y = a; 2'b01: y = b; 2'b10: y = c; default: y = c; // covers 2'b11 (and any X) endcaseWhy this step?
defaultsweeps up all remaining cells in one line — you never have to enumerate them.
Verify: Cells covered after fix: {00,01,10} explicit + {11} via default cells . Full coverage ✓, no latch.
Example 4 — Incomplete sensitivity list (Cell C5, sim ≠ silicon)
Forecast: This one has no latch. Where can it possibly go wrong?
-
Separate two questions. "Is
yassigned on every path?" — yes (single unconditional statement). "Does the block re-run when it should?" — that is the sensitivity question, a different axis. Why this step? Coverage (latch axis) and sensitivity (simulation axis) are independent; a block can pass one and fail the other. -
Trace a
b-only change. Holdafixed, flipb. The block is sensitive only toa, so simulation does not re-evaluate — the simulatedystays stale. Figure s04 shows the timing mismatch.Why this step? The synthesized AND gate does react to
b; so simulation and silicon disagree — the worst kind of bug because tests pass on a wrong model. -
Fix.
always @(*) y = a & b;— the*auto-includes every read signal (aandb). Why this step?@(*)makes the simulation model track the same signals the gate does.
Verify (numeric): True hardware, a=1,b=0→1 should give y:0→1. Set a=1. With @(a): block never fires on b's change ⇒ simulated y stuck at old value 0 while hardware says 1. Mismatch confirmed; @(*) yields y = a & b = 1 ✓.
Example 5 — Blocking vs non-blocking ordering (Cell C6)
Forecast: With <=, which value of t does y read — the new one or the old one?
-
Blocking version (correct for combinational).
always @(*) begin t = a & b; // updates t NOW y = t | c; // reads the NEW t endWhy this step?
=runs in order and updates immediately, soysees the freshly computedt— exactly like two gates in series. -
Non-blocking version (wrong here).
always @(*) begin t <= a & b; // scheduled for end of step y <= t | c; // reads OLD t (this step's start) endWhy this step?
<=defers updates, soyuses the previoust, computing a delayed/incorrect function. Figure s05 shows the one-step lag.
Verify (numeric): Take a=1,b=1,c=0, with old t=0.
Blocking: t=1, then y = 1|0 = 1 ✓ (matches intended ).
Non-blocking: y = t_{old}|c = 0|0 = 0 ✗ — a wrong 0. Ordering matters; use =.
Example 6 — Degenerate input, constant-folded (Cell C7)
Forecast: Guess each output before reading the algebra.
-
Apply Boolean identities. (AND with 1 passes through). (OR with 1 forces high). Why this step? A tied-constant input is a degenerate cell; identities collapse the gate.
-
Read off the synthesized hardware.
ybecomes a plain wire froma(no gate).zbecomes a constant1(the synthesizer deletesaentirely). Why this step? Synthesis constant-folds; showing this reassures you no latch or wasted gate appears — a continuousassignis always combinational (Cell C7 is safe).
Verify: a=0: y=0&1=0=a ✓, z=0|1=1 ✓. a=1: y=1&1=1=a ✓, z=1|1=1 ✓. Both hold for all cells of a.
Example 7 — Multi-output block, one output missed (Cell C8)
Forecast: A latch appears per output. Which one?
-
Check each output independently.
hiis assigned in both branches ⇒ fully covered.lois assigned only in theg=1branch ⇒ theg=0cell is missing. Why this step? Latch inference is decided output-by-output, not block-by-block. -
Diagnose.
logets a transparent latch (enabled byg);histays pure combinational. See Figure s06 —hirow all green,lorow red ong=0.Why this step? Pinpointing the specific latched signal is what synthesis warnings do; you should predict them.
-
Fix. Add
lo = 1'b1;(or a chosen default) to theelse, or set both defaults on top.
Verify: After fix, g=1⇒hi=1,lo=0; g=0⇒hi=0,lo=1. Each output covers both cells each ✓. No latch on either.
Example 8 — Real-world word problem: priority encoder (Cell C9)
Forecast: How many input cells, and how does the "none active" case avoid a latch?
-
Cell count and priority rule. 3 inputs ⇒ cells. Priority: check
s2first, thens1, thens0, else default. Why this step? Word problems still reduce to covering every input cell. -
Code with a leading default (the "none" case).
always @(*) begin code = 2'b00; // default = none active if (s2) code = 2'b10; else if (s1) code = 2'b01; else if (s0) code = 2'b00; endWhy this step? The unconditional
code = 2'b00first covers the all-zero cell (and any un-hit cell), so the whole space is assigned ⇒ no latch. Theif/else ifchain enforces priority. -
Trace a mixed case.
s2=1,s1=1,s0=0: firstiffires ⇒code=10; lower branches skipped byelse. Highest priority wins, as required.
Verify (numeric):
s2s1s0=000⇒ default ⇒code=00(0) ✓s2s1s0=001⇒s0branch ⇒code=00(0) ✓s2s1s0=010⇒s1branch ⇒code=01(1) ✓s2s1s0=110⇒s2branch ⇒code=10(2) ✓ (priority overs1)s2s1s0=111⇒s2branch ⇒code=10(2) ✓
Example 9 — Exam twist: shadowed branches (Cell C10)
Forecast: Since the second if runs after the first, guess the final expression.
-
Execute top-to-bottom (blocking). Start
y=0. Ifa, sety=1. Then ifb, sety=0— this can overwrite the previous line. Why this step? Blocking statements are sequential; a later assignment shadows an earlier one when its condition holds. -
Derive the Boolean function.
y=1only ifais true andbis false (otherwise the secondifclears it, oranever set it). So . Why this step? Reading the net effect of overriding assignments is the exam trap — you must simulate the order, not read lines in isolation. -
Latch check.
yis assigned unconditionally on line 1 ⇒ every cell covered ⇒ combinational, despite the missingelses. Why this step? The default line saves it; shadowing changes the function, not the latch status.
Verify (numeric, all 4 cells of a,b):
a=0,b=0:y=0then no changes ⇒0; ✓a=1,b=0:y→1, second if skipped ⇒1; ✓a=0,b=1:y=0, first skipped, second sets0⇒0; ✓a=1,b=1:y→1then→0⇒0; ✓
Recall Scenario matrix — did we cover every cell?
C1 Ex1 · C2 Ex2 · C3+C4 Ex3 · C5 Ex4 · C6 Ex5 · C7 Ex6 · C8 Ex7 · C9 Ex8 · C10 Ex9 — all ten cells worked. ✓
Active recall
Connections
- Latch inference and how to avoid it — the deep dive on Cells C2, C3, C8.
- Blocking vs Non-blocking assignments — Cell C6 in full.
- Multiplexers, adders, decoders — Ex1 and Ex8 are canonical instances.
- Boolean algebra and Karnaugh maps — where Ex6's identities come from.
- Sequential logic in HDL — where
<=and intentional memory belong. - Synthesis and the digital design flow — the tool that reports these latch warnings.