3.5.2 · D4HDL & Digital Design Flow

Exercises — Combinational logic in HDL

2,360 words11 min readBack to topic

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
end

Why 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
end
Recall 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;
end
Recall 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;
end

eq 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;
end
Recall 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;
end

Check 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;
end
Recall 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 01. True OR gives . Block B's sim keeps (stale). This is the classic sim ≠ silicon bug. Fix: always use @(*).

Figure — Combinational logic in HDL

Recap flow

derive

describe

audit

yes

no

add default or else

Spec or truth table

Boolean function out = f inputs

HDL: assign or always at star

Every output on every path?

Pure combinational

Inferred latch bug

Connections