3.5.4 · D3HDL & Digital Design Flow

Worked examples — Blocking vs non-blocking assignments

2,821 words13 min readBack to topic

The scenario matrix

Every worked example below fills one cell of this table. Together they cover all of them.

# Cell (case class) Example that covers it
C1 Sequential swap — <= vs = side by side Example 1
C2 Shift register / pipeline — order-independence of <= Example 2
C3 Combinational chain — = is required Example 3
C4 Degenerate input: self-assign / initial x (unknown) values Example 4
C5 Mixed operators on same var — the illegal/ambiguous case Example 5
C6 Two always blocks driving related signals — race condition Example 6
C7 Limiting case: a counter over many cycles (= vs <= divergence over time) Example 7
C8 Real-world word problem: traffic-light-style state + output register Example 8
C9 Exam twist: reordered lines + a read of a just-written signal Example 9

We trace numeric values everywhere so simulation outcomes are concrete, not hand-wavy.


Example 1 — The swap (cell C1)

Forecast: guess the two (a, b) pairs before reading on.

Version NB (non-blocking):

  1. Active region — sample all RHS from old values. Why this step? <= reads pre-edge values. Old a = 3, old b = 7. So we stash: a's pending value , b's pending value .
  2. NBA region — write both at once. Why this step? All stashed writes land together. Result: a = 7, b = 3. Swapped.✔
Figure — Blocking vs non-blocking assignments

Version B (blocking):

  1. a = ba becomes 7 immediately. Why this step? = updates now, before line 2.
  2. b = a → but a is already 7, so b = 7. Why this step? Line 2 sees the freshly-written a.
  3. Final: a = 7, b = 7. No swap.✘
Recall Verify (sanity check)

Non-blocking preserves the pair as a set: {3,7} before, {7,3} after — a genuine permutation. Blocking loses the value 3 entirely (information destroyed) — a tell-tale sign the swap failed.


Example 2 — Shift register, and why order can't matter (cell C2)

Forecast: does reordering change (q1, q2, q3)?

  1. Sample every RHS from old values (Active region). Why? Every <= reads pre-edge. Stash: q3 ← q2_old = 1, q2 ← q1_old = 0, q1 ← d = 1.
  2. Write together (NBA region). Result: q1 = 1, q2 = 0, q3 = 1.
  3. Reorder proof. Why unchanged? Step 1 read all three RHS before any LHS was written. Shuffling the lines shuffles nothing that step 1 depends on — the old values are fixed. Same hardware, same result.✔
Figure — Blocking vs non-blocking assignments

Contrast with blocking (q1=d; q2=q1; q3=q2;): q1 becomes 1, then q2 = q1 = 1, then q3 = q2 = 1. In one clock d reached q3. Why wrong? Immediate updates let the value ripple through — you built one flip-flop, not a three-stage pipeline.

Recall Verify

After three clocks of the correct <= version, a 1 injected at d walks q1 → q2 → q3. That "one stage per clock" delay is exactly the definition of a shift register; the blocking version delivers it in zero extra clocks (collapsed).


Example 3 — Combinational chain where = is mandatory (cell C3)

Forecast: what is y? What would <= give instead?

  1. tmp = a & b1 & 1 = 1, applied now. Why blocking? This is pure data-flow (combinational logic); tmp must be ready before y reads it, like a signal rippling through gates.
  2. y = tmp | c1 | 0 = 1. Why? Line 2 must see the fresh tmp. Correct answer y = 1.✔
  3. If we used <=: tmp <= a&b samples but does not update in this pass, so line 2 reads the old tmp → stale y, and the tool may infer an unwanted latch → a sim/synthesis mismatch.
Recall Verify

Boolean truth: with a=b=1, c=0, . Blocking gives 1; non-blocking (reading stale tmp = 0 on the very first evaluation) would give 0 — provably wrong.


Example 4 — Degenerate inputs: self-assign & unknown x (cell C4)

Forecast: does r ever become known? Any difference between = and <= here?

  1. r <= r, first edge. Sample RHS = old r = x. Write back x. Why no change? A register assigned only its own old value is a hold; unknown stays unknown. It never resolves to 0 or 1 on its own.
  2. Second edge: identical — still x. This is the degenerate "does nothing but hold" flop.
  3. Blocking r = r: reads r, writes the same r immediately — also a no-op, same x. Why identical here? With a single self-referential statement there is no later line to observe the difference, so = and <= coincide.
  4. Lesson on degeneracy: the blocking/non-blocking distinction only becomes visible when one statement's result is read by another statement in the same block. Self-assign has no such reader.
Recall Verify

x under any bitwise identity op is x. So both versions leave r = x for all cycles — a fixed point. The two operators are indistinguishable in this degenerate single-statement case.


Example 5 — Mixing = and <= on the same variable (cell C5)

Forecast: guess the final n.

  1. n = n + 1 (blocking) → n becomes 3 immediately. Why? = updates now.
  2. n <= n + 10 (non-blocking) samples RHS using the current n = 3 → stashes 13, applied in the NBA region.
  3. NBA region: n = 13. So the blocking write of 3 is overwritten. Final n = 13.
  4. Why forbidden? The outcome depends on a subtle read/write ordering across two scheduler regions. A different tool or a reordered line changes the answer → classic ambiguity. Golden Rule 3: never mix operators on one variable.
Recall Verify

Trace: start 2 → blocking makes 3 → NBA RHS sampled as → NBA writes 13. Final = 13. The intermediate 3 is transient and unobservable, which is exactly why this style is dangerous.


Example 6 — Two always blocks, one race (cell C6)

Forecast: does splitting the swap across two blocks break it?

  1. Both blocks trigger on the same edge. Each <= samples its RHS from old values in the Active region: Block 1 stashes a ← 9, Block 2 stashes b ← 5. Why safe here? Non-blocking sampling reads pre-edge values regardless of which block ran first.
  2. NBA region writes both: a = 9, b = 5. Correct swap — no race.✔
  3. The race version: if instead both used blocking (a = b; in one block, b = a; in another), the result depends on which block the simulator runs first — an undefined ordering across separate blocks. That is a true race condition.
  4. Caveat (Golden Rule 4): even the correct <= version drives two different variables from two blocks — fine. Driving the same variable from two blocks is what's illegal.
Figure — Blocking vs non-blocking assignments
Recall Verify

<= across two blocks: {5,9} → {9,5}, a clean permutation. Blocking across two blocks yields either {9,9} or {5,5} depending on run order — provably non-deterministic.


Example 7 — Limiting case: divergence over many cycles (cell C7)

Forecast: guess both values after 4 clocks.

  1. Counter A: each edge samples old cnt, adds 1, writes once. Sequence: 0 → 1 → 2 → 3 → 4. After 4 clocks, cnt = 4. Why? Exactly one increment per edge — the intended counter.
  2. Counter B: per edge, nxt = cnt+1 then cnt = nxt then cnt = cnt+1two increments applied immediately each edge. Sequence: 0 → 2 → 4 → 6 → 8. After 4 clocks, cnt = 8.
  3. Limiting insight. Why care about "many cycles"? A one-cycle test might look "close enough," but the error compounds linearly: Counter B runs at double rate, drifting further every clock. The bug is invisible at cycle 0 and glaring at the limit.
Recall Verify

A after clocks ; B after clocks . At : A , B . The gap grows without bound.


Example 8 — Real-world word problem: state + output register (cell C8)

Forecast: after this edge, is light already 1, or still 0?

  1. Sample RHS from old values. Why <=? This is clocked sequential logic; a registered output must reflect the pre-edge situation to stay glitch-free. Stash state ← 1, light ← old state = 0.
  2. NBA writes: state = 1, light = 0. So the lamp lags the state by exactly one clock — precisely the intended "one clock later" behaviour.
  3. Next edge (now old state = 1): light <= state stashes 1light = 1. The STOP lamp lights one clock after the state changed. ✔
  4. Why not blocking? light = state after state = 1 would show 1 the same cycle, defeating the registered-output timing and risking a decode glitch.
Recall Verify

Edge 1: (state,light): (0,0) → (1,0). Edge 2: (1,0) → (1,1). The output is a one-cycle delayed copy of the state — the hallmark of a registered output.


Example 9 — Exam twist: read a just-written signal (cell C9)

Forecast: after the edge, what are p and q? And after swapping lines?

  1. Sample all RHS from old values (Active region). Why? <= never sees a same-cycle update. Old p = 4. Stash p ← 5, q ← old p = 4.
  2. NBA writes: p = 5, q = 4. So q gets the old p, not the incremented one.
  3. Swap the lines (q <= p; first, then p <= p + 1;): still samples old p = 4 for both. Result identical: p = 5, q = 4. Why? Order-independence of <= (Example 2's principle).
  4. Common exam trap: students expect q = 5 because line 2 is "after" line 1. But <= breaks the software intuition — line 2 reads the pre-edge p.
Recall Verify

p_new = p_old + 1 = 5; q_new = p_old = 4, independent of line order. If a student answers q = 5 they've fallen for the blocking-style intuition.


Wrap-up: the matrix, filled


Active Recall

Value of n after Example 5's edge?
13 — the non-blocking NBA write () overwrites the transient blocking 3.
Counter B's value after 4 clocks (Example 7)?
8 — two increments per edge, so .
(p, q) after Example 9's edge?
p = 5, q = 4; q reads the pre-edge p, unaffected by line order.
After a WALK→STOP transition, when does the lamp light (Example 8)?
One clock after the state changes, because the registered output samples the old state.

Connections