Exercises — Combinational logic in HDL
Throughout, remember the one anchor idea from the parent: a combinational block is a pure function of the present inputs — out = f(inputs) — with no memory. The moment an output can be left unassigned on some path, the synthesizer inserts a memory element (a latch) to hold the old value. That is the villain of almost every exercise below.
Level 1 — Recognition
Goal: can you spot combinational vs. sequential, and read a truth table into an equation?
Exercise 1.1
Classify each as combinational (C) or sequential (S):
(a) assign y = a & b;
(b) A D flip-flop.
(c) always @(*) y = sel ? a : b;
(d) A transparent latch.
Recall Solution 1.1
(a) C — a continuous assign is a permanent equation; the output is always driven, no memory.
(b) S — a flip-flop stores a value between clock edges. That is memory ⇒ sequential.
(c) C — every value of sel (0 and 1) drives y; nothing is held.
(d) S — a latch holds its previous value when not transparent. Holding = memory ⇒ sequential.
Rule of thumb: if the circuit's output can ever depend on a past input, it is sequential.
Exercise 1.2
Given this 1-bit truth table, write the Boolean function .
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Recall Solution 1.2
exactly on the rows where and differ. That is the definition of exclusive-or: Why: means "true when an odd number of inputs are 1". With two inputs, "odd number of 1s" = "exactly one 1" = "they differ". Sum-of-products form (one product term per 1-row): . Both are the same function — see Boolean algebra and Karnaugh maps.
Level 2 — Application
Goal: turn a specification into correct, latch-free HDL.
Exercise 2.1
Write a 4-to-1 multiplexer in Verilog using an always @(*) case block. Inputs a,b,c,d, 2-bit select sel, output y. Make it guaranteed combinational.
Recall Solution 2.1
always @(*) begin
case (sel)
2'b00: y = a;
2'b01: y = b;
2'b10: y = c;
default: y = d; // covers 2'b11 (and X/Z in sim)
endcase
endWhy the default: sel has 4 legal patterns. Listing three of them and using default for the last one means every path assigns y. Fully specified ⇒ combinational, no latch. See Multiplexers, adders, decoders.
Exercise 2.2
Convert the same 4-to-1 MUX to a single continuous assign using nested conditionals.
Recall Solution 2.2
assign y = (sel == 2'b00) ? a :
(sel == 2'b01) ? b :
(sel == 2'b10) ? c : d;Why it's combinational: an assign is a permanent equation with no "hold" branch possible — the final : d catches every remaining sel. Output can never be undriven.
Exercise 2.3
For sel = 2'b10, what is y in both solutions above if a=0, b=1, c=1, d=0?
Recall Solution 2.3
selects the third input . With , in both idioms. (They describe the same function, so they must agree on every input.)
Level 3 — Analysis
Goal: read someone else's code and predict the exact hardware — including bugs.
Exercise 3.1
Does this infer a latch? If so, on which input pattern, and for which output?
always @(*) begin
if (en) begin
y = data;
z = ~data;
end
else
y = 1'b0; // note: z NOT assigned here
endRecall Solution 3.1
Trace both outputs across both branches:
en |
y assigned? |
z assigned? |
|---|---|---|
| 1 | yes (data) |
yes (~data) |
| 0 | yes (0) |
no |
y is assigned on every path ⇒ combinational, no latch for y.
z is not assigned when en==0 ⇒ the tool must hold z's previous value ⇒ latch inferred on z.
Latch condition: en == 0. Fix: add z = 1'b0; (or any value) in the else, or set defaults y=0; z=0; before the if. See Latch inference and how to avoid it.
Exercise 3.2
Predict y after this block runs, given a=1, b=1, c=0. Then say what would change if = were replaced by <=.
always @(*) begin
t = a & b;
y = t ^ c;
endRecall Solution 3.2
With blocking = (correct):
- Line 1: . Update happens immediately.
- Line 2: , using the new .
- Result: .
With non-blocking <=: both right-hand sides are evaluated using the values at the start of the timestep. t still holds its old (stale) value when line 2 reads it. In a @(*) combinational block this produces wrong logic / a simulation-vs-synthesis mismatch. This is exactly why combinational blocks use blocking =. See Blocking vs Non-blocking assignments.
Level 4 — Synthesis
Goal: design a complete combinational unit from a word problem.
Exercise 4.1
Design a 2-bit comparator. Inputs are 2-bit numbers A and B. Outputs: gt (A > B), eq (A == B), lt (A < B). Write latch-free Verilog and give the Boolean idea behind eq.
Recall Solution 4.1
Exactly one of the three outputs is 1 for any inputs — the three cases are mutually exclusive and exhaustive.
always @(*) begin
gt = 1'b0; eq = 1'b0; lt = 1'b0; // defaults ⇒ every path covered
if (A > B) gt = 1'b1;
else if (A == B) eq = 1'b1;
else lt = 1'b1;
endeq in Boolean terms: A == B means every bit matches, i.e. (XNOR of each bit pair, ANDed). The top-of-block defaults guarantee all three outputs are driven on every path ⇒ pure combinational.
Exercise 4.2
Build a 1-bit full adder and verify the outputs for a=1, b=1, cin=1.
Recall Solution 4.2
From the parent note's derivation (sum = odd count of 1s; carry = at least two 1s):
assign s = a ^ b ^ cin;
assign cout = (a & b) | (cin & (a ^ b));Check a=b=cin=1: three 1s.
- (odd number of 1s ⇒ 1).
- .
- Sanity: . ✓ Two continuous
assigns ⇒ always combinational.
Level 5 — Mastery
Goal: reason about subtle, whole-design correctness.
Exercise 5.1
This "priority encoder" is meant to output the index of the highest-set bit of a 4-bit input req, and valid=0 when req==0. Find all bugs (latches and logic) and give a corrected version.
always @(*) begin
if (req[3]) code = 2'd3;
else if (req[2]) code = 2'd2;
else if (req[1]) code = 2'd1;
else if (req[0]) code = 2'd0;
valid = |req;
endRecall Solution 5.1
Bug — latch on code: when req == 4'b0000, none of the four branches fire, so code is left unassigned ⇒ inferred latch. (valid is fine: |req reduction-OR drives it on every path, and it evaluates to 0 for all-zero input, correctly.)
Fix — give code a default before the branches:
always @(*) begin
code = 2'd0; // default covers the req==0 path
valid = |req; // 1 if any bit set, else 0
if (req[3]) code = 2'd3;
else if (req[2]) code = 2'd2;
else if (req[1]) code = 2'd1;
else if (req[0]) code = 2'd0;
endCheck req = 4'b0100: highest set bit is bit 2 ⇒ code = 2 and valid = 1. ✓
Check req = 4'b0000: code = 0 (default, defined — no latch) and valid = 0. ✓
Exercise 5.2
A student swears these two blocks are identical hardware. Are they? If not, which input pattern reveals the difference?
// Block A
always @(*) begin
y = a | b;
end
// Block B
always @(a) begin
y = a | b;
endRecall Solution 5.2
Synthesized hardware is identical — both describe the OR gate . Synthesis reads the logic, not the sensitivity list.
Simulation differs. Block B's list @(a) only re-evaluates when a changes. So if a stays fixed and b alone toggles, Block B's simulated y does not update, while the real gate would.
Revealing pattern: hold a = 0; change b from 0→1. True OR gives . Block B's sim keeps (stale). This is the classic sim ≠ silicon bug. Fix: always use @(*).

Recap flow
Connections
- Parent: Combinational logic in HDL
- Latch inference and how to avoid it — the core failure mode drilled above.
- Blocking vs Non-blocking assignments — Exercises 3.2 and 4.2.
- Boolean algebra and Karnaugh maps — where every truth-table equation comes from.
- Multiplexers, adders, decoders — the canonical blocks built here.
- Sequential logic in HDL — where
<=and clocks actually belong. - Synthesis and the digital design flow — why "sim ≠ silicon" costs real money.