Intuition What this page is
The parent note gave you the rules: assign for combinational, <= for clocked, = for combinational-inside-always. Rules are cheap. Traps are expensive. This page walks the whole space of situations the two assignment operators can throw at you — every ordering, every clock edge, every degenerate case — and traces exactly what hardware pops out. By the end you should never meet an HDL snippet whose behaviour surprises you.
The main code language on this page is Verilog (it makes the two-operator distinction sharpest), but every sequential example is mirrored in VHDL so you can read either dialect. VHDL says the same physical things with different keywords — we point them out as we go.
Definition The VHDL translation table (read once, reuse everywhere)
Idea
Verilog
VHDL
Combinational, continuous
assign y = a & b;
y <= a and b; (concurrent, outside a process)
Combinational block
always @(*)
process(all) / process(a,b,...)
Clocked block
always @(posedge clk)
process(clk) ... if rising_edge(clk) then
"Sample old, flip together"
non-blocking <=
signal assignment <= (VHDL signals already behave this way)
"Do it now, next line sees it"
blocking =
variable assignment := (declared with variable)
The key insight: in VHDL a signal <= already means "scheduled update, old value seen until the process suspends" — exactly Verilog's non-blocking. A variable := means "immediate", exactly Verilog's blocking =. So the whole <= vs = story maps onto VHDL's signal vs variable .
Before anything, one convention we will reuse everywhere. A bit is one physical wire that is either 0 (low voltage) or 1 (high voltage). A register (flip-flop) is a tiny box that holds one bit and only changes its held value at the instant the clock rises. We draw a clock as a square wave; the moment it goes from 0 up to 1 is the rising edge (posedge clk in Verilog, rising_edge(clk) in VHDL). That instant is the only time a clocked register updates.
Every worked example below is tagged with the cell it covers. The matrix lists every kind of situation the assignment operators can produce.
Cell
Situation class
What could go wrong
A
Pure combinational, assign
order-independence, re-evaluation on any input change
B
Combinational inside always @(*) with =
must assign every path, else latch
C
Clocked <=, single register
basic edge-triggered store
D
Clocked <=, mutual reference (swap/shift)
old-value-vs-new-value: the reason <= exists
E
Clocked with = (the WRONG operator)
collapses N registers into 1: shift-register bug
F
Degenerate / zero input, missing branch
inferred latch, unknown x
G
Bit-width & literal edge cases
truncation, zero-extension, signed vs unsigned
H
Real-world word problem
full FSM-flavoured design
I
Exam twist (blocking ordering trap)
reading = results mid-block
We now cover every cell .
assign lines, deliberately out of order
Verilog
wire a, b, c, y;
assign y = c & b; // line 1 uses c
assign c = a | b; // line 2 *defines* c — written AFTER it's used
VHDL (same two gates, concurrent signal assignments)
y <= c and b; -- order on the page is irrelevant here too
c <= a or b;
With a=1, b=0, what is y?
Forecast: Guess before reading. If you think "line 2 runs after line 1, so c is garbage on line 1" — that's the software instinct. Hold that thought.
List the physical gates. Each assign (VHDL: each concurrent <=) is a separate gate soldered on the board , existing simultaneously. Line 2 is an OR gate; line 1 is an AND gate. Why this step? Because order on the page is irrelevant — combinational logic has no notion of "before".
Solve the OR gate: c = a ∨ b = 1 ∨ 0 = 1 . Why this step? ∨ (| / or) outputs 1 if either input is 1 .
Feed into the AND gate: y = c ∧ b = 1 ∧ 0 = 0 . Why this step? ∧ (& / and) outputs 1 only if both inputs are 1 ; here b = 0 kills it.
Answer: y = 0.
Verify: Try any input change and both gates re-settle instantly. E.g. flip b → 1 : c = 1 ∨ 1 = 1 , y = 1 ∧ 1 = 1 . The wire "never sleeps" — matches the dataflow promise. ✓
Worked example A 2-to-1 multiplexer with a default
Verilog
reg y;
always @( * ) begin
y = 1'b0 ; // default first
if (sel) y = a; // override on one path
else y = b;
end
VHDL (a combinational process; signal assignment)
process ( all ) begin
y <= '0' ; -- default
if sel = '1' then y <= a;
else y <= b;
end if ;
end process ;
A multiplexer (mux) picks one of two inputs based on a select bit. With sel=0, a=1, b=1, what is y?
Forecast: Does the default y=1'b0 "win" or get overwritten?
always @(*) = combinational block. The @(*) means "re-run whenever any input changes". Why this step? It models a block of gates, not a clocked register — so we use ==blocking ===, which updates immediately in listed order. (In VHDL the last signal assignment in a process wins, giving the same "default then override" effect.)
Execute in order. First y = 0. Then the else branch (since sel=0) sets y = b = 1. The later write wins. Why this step? Inside one block, statements do have order — the default is a safety net that a later branch overrides.
Result: y = b = 1 .
Answer: y = 1.
Verify: Every path assigns y (default + both branches), so no latch is inferred. Flip sel=1: y = a = 1. The block is pure combinational. ✓
Worked example D flip-flop, three clock ticks
Verilog
reg q;
always @( posedge clk) q <= d;
VHDL
process (clk) begin
if rising_edge (clk) then
q <= d;
end if ;
end process ;
Input d over time is 1 , 0 , 1 sampled on three rising edges. Initial q = 0 . Trace q .
Forecast: Does q follow d instantly, or one tick late?
<= samples on the edge only. Between edges, q holds. Why this step? posedge clk / rising_edge(clk) fires only at the rising edge (figure s01, red arrow); nothing else touches q.
Tick 1: at the edge, d=1 → q becomes 1 . Tick 2: d=0 → q=0. Tick 3: d=1 → q=1. Why this step? Each edge copies the current input into the box. See the waveform in figure s03.
Answer: q sequence = 1 , 0 , 1 (one edge apart — this is a flip-flop ).
Verify: At each edge n , q captures the value d had just before that edge; after the edge q holds it until edge n + 1 . So the value living on q between edge n and edge n + 1 is exactly the d sampled at edge n — that one-edge-hold is the defining property of a D-FF. ✓
Worked example Two registers swapping every clock
Verilog
always @( posedge clk) begin
a <= b;
b <= a;
end
VHDL (signals <= already sample old values)
process (clk) begin
if rising_edge (clk) then
a <= b;
b <= a;
end if ;
end process ;
Start with a=1, b=0. What is (a,b) after one edge? After two ?
Forecast: Software says line 1 makes a=0, then line 2 copies that new a into b, so both become 0. Is that what hardware does?
Non-blocking = read ALL right-hand sides first, then write ALL left-hand sides. The right-hand side (RHS) is the expression after the <= (what we read); the left-hand side (LHS) is the register before the <= (what we write). Why this step? <= models real flip-flops that all sample at the same instant on the shared clock — they can't see each other's new values. (VHDL signals behave identically for the same reason.)
Read phase (before any write): RHS of line 1 is b=0; RHS of line 2 is a=1. Both captured from old values.
Write phase: the LHS updates: a←0, b←1. A genuine swap! Why this step? This is exactly what figures s02 and s04 show — two boxes exchanging contents in one instant.
Answer: after 1 edge (a,b) = (0,1); after 2 edges back to (1,0).
Verify: The pair oscillates ( 1 , 0 ) → ( 0 , 1 ) → ( 1 , 0 ) … , period 2 — a correct swap, not the "both zero" software bug. ✓
Worked example Same swap, but with blocking
=
Verilog
always @( posedge clk) begin
a = b; // blocking
b = a; // blocking
end
VHDL (the analogue: variables with :=, which update immediately)
process (clk) variable a, b : std_logic ; begin
if rising_edge (clk) then
a := b; -- immediate, like Verilog '='
b := a; -- sees the NEW a
end if ;
end process ;
Start a=1, b=0. After one edge?
Forecast: Compare against Cell D — will you get a swap or a collapse?
Blocking = (VHDL :=) updates immediately, in order. Why this step? It means "do this now, then the next line sees the result" — hardware-ly wrong for parallel registers.
Line 1: a = b = 0. Now a=0 for the rest of the block .
Line 2: b = a, but a is now the new 0. So b=0.
Answer: (a,b) = (0,0) — the swap is destroyed. Both collapse to old b.
Verify: This is the classic accidental behaviour: = in posedge makes a chain (shift register / value collapse), causing simulation-vs-synthesis mismatch . Contrast Cell D's correct ( 0 , 1 ) — the same contrast is drawn in figure s04. ✓
else in a combinational block
Verilog
reg y;
always @( * ) begin
if (en) y = d; // no else!
end
VHDL (same omission, same latch)
process ( all ) begin
if en = '1' then y <= d; end if ; -- no else -> latch
end process ;
With en=0, what is y?
Forecast: What does y do when en=0 — is there even an answer?
Trace the only path. When en=0, the if body is skipped and y is never assigned on this pass. Why this step? We must check every branch of a combinational block.
What can hardware do with "no assignment"? It has no choice but to remember the last value of y. Why this step? Remembering a value between events is precisely what a latch is — a memory element you did not ask for.
Result: y holds its previous value → an inferred latch , a bug. Why this step? You wanted pure gates; you got hidden storage.
Answer: y = its previous value (latch inferred). When en=1, y=d; when en=0, y freezes.
Verify: The fix (Cell B pattern) is a default y = 1'b0; before the if, giving y=0 when en=0 — no latch, every path assigns. ✓
Worked example Width mismatches, concatenation, and sign
wire [ 3 : 0 ] nib; // 4-bit
assign nib = 8'hA5 ; // assign an 8-bit value to a 4-bit wire
wire [ 7 : 0 ] cat = { 4'b1010 , 4'b0101 };
wire [ 7 : 0 ] u = 4'b1000 ; // unsigned widen 4 -> 8
wire signed [ 7 : 0 ] s = $ signed ( 4 'sb1000); // signed widen 4 -> 8
What binary value lands in nib? What is cat? What are u and s?
Forecast: 8'hA5 is 8 bits but nib only has 4 wires — what happens to the top half? And when a 4-bit value goes into 8 bits, do the new top bits become 0 , or a copy of the sign?
Expand the literal. 8'hA5 in binary is 1010_0101 (hex A = 1010 , hex 5 = 0101 ). Why this step? Every literal in HDL carries an explicit width & base; hex → 4 bits per digit.
Truncate to nib's 4 wires. A 4-bit wire keeps the low 4 bits (bits 3 : 0 ); the top 4 (1010) have nowhere to go and are dropped. Why this step? Physical wires are fixed in number — extra bits are simply not connected. nib = 0101 (hex 5 ).
Concatenation {high, low} stacks 1010 then 0101 = 1010_0101 = 8'hA5. Why this step? {} glues bit-vectors left-to-right, MSB first.
Widening — unsigned: an unsigned value pads the new high bits with 0 (zero-extension) . So 4'b1000 → 8'b0000_1000 = decimal 8 . Why this step? Unsigned numbers have no sign to preserve; extra bits are just more zeros above.
Widening — signed: a signed value copies its top bit (the sign bit) into every new high bit (sign-extension ). 4'sb1000 has sign bit 1 → value − 8 in 4-bit two's complement; widened to 8 bits it becomes 8'b1111_1000, still − 8 . Why this step? Sign-extension keeps the numeric value identical; zero-extension would have turned − 8 into + 8 . The signed/unsigned declaration is what tells the tool which rule to use.
Answer: nib = 4'b0101 (=5); cat = 8'b10100101 (=0xA5 =165); u = 8 (0000_1000); s = -8 (1111_1000).
Verify: 0 x A 5 = 165 , low nibble = 165 mod 16 = 5 . Zero-extend 1000 = 8 . Sign-extend 1000: in 4-bit two's complement 100 0 2 = − 8 , and 1111 100 0 2 = 248 − 256 = − 8 in 8-bit — value preserved. ✓
Worked example "Count how many times a button is pressed, wrapping at 4."
A push-button feeds btn (already de-bounced, one clean pulse per press). Build a 2-bit counter cnt that increments each rising clock while btn=1, and wraps 3 → 0 .
reg [ 1 : 0 ] cnt;
always @( posedge clk) begin
if (rst) cnt <= 2'b00 ;
else if (btn) cnt <= cnt + 1 ; // wraps automatically at 2 bits
else cnt <= cnt; // hold (explicit)
end
VHDL
process (clk) begin
if rising_edge (clk) then
if rst = '1' then cnt <= "00" ;
elsif btn = '1' then cnt <= cnt + 1 ;
else cnt <= cnt;
end if ;
end if ;
end process ;
Starting cnt=2, with btn=1 on the next three edges (rst=0), trace cnt.
Forecast: A 2-bit register can only hold 0..3 . What happens after 3 ?
Use <= because this is clocked storage. Why this step? Cell C/D rule: posedge → <=.
Edge 1: c n t = 2 + 1 = 3 . Edge 2: c n t = 3 + 1 = 4 , but only 2 wires exist → 4 mod 4 = 0 . Why this step? A 2-bit value counts 0 , 1 , 2 , 3 then overflows; the carry-out has no wire, so it wraps (modulo $2^2$ ). See figure s05.
Edge 3: c n t = 0 + 1 = 1 .
Answer: cnt sequence = 3 , 0 , 1 .
Verify: In general c n t n + 1 = ( c n t n + 1 ) mod 4 . Sequence from 2: 3 , 0 , 1 , 2 , 3 , … period 4. This is the seed of a simple FSM . ✓
Worked example Chained blocking assignments (the trap the graders love)
reg [ 3 : 0 ] x , y, z ;
always @( * ) begin
x = a + 1 ; // a = 4'd5
y = x + 1 ; // uses NEW x
z = y + x ; // uses NEW y and NEW x
end
With a = 5, find x, y, z.
Forecast: Do y and z see the old x (like <=) or the new x?
This is @(*) with blocking = — combinational, ordered , each line sees results of the previous. Why this step? Blocking = updates immediately, unlike Cell D's <=.
x = 5 + 1 = 6. Why this step? Straight addition, fits in 4 bits (6 ≤ 15 ).
y = x + 1 = 6 + 1 = 7 — uses the new x. Why this step? Blocking means line 2 sees the just-written x=6.
z = y + x = 7 + 6 = 13. Why this step? Both operands are the freshly computed values.
Answer: x=6, y=7, z=13.
Verify: 6 , 7 , 13 all fit in 4 bits (< 16 ), no wrap. If these had been <=, y and z would use the old x (whatever it held before) — a different result. The operator choice is the whole point. ✓
Recall Which cell was which? (quiz yourself)
Order-independent assign gates ::: Cell A
Complete @(*) mux with default ::: Cell B
Basic D-FF with <= ::: Cell C
Correct swap needs <= ::: Cell D
Wrong = collapses the swap ::: Cell E
Missing else → inferred latch ::: Cell F
Width truncation, zero- vs sign-extension ::: Cell G
Wrapping counter word problem ::: Cell H
Blocking chain sees new values ::: Cell I
VHDL analogue of Verilog non-blocking <= ::: a signal assignment <=
VHDL analogue of Verilog blocking = ::: a variable assignment :=
Mnemonic The two operators in one breath
<= = "everyone freeze, sample old values, flip together" (parallel, clocked, Cells C/D/H; VHDL signal ).
= = "do it now, next line sees the result" (ordered, combinational, Cells A/B/I; VHDL variable :=).
Combinational vs Sequential Logic — Cells A/B are combinational; C–E, H sequential.
Flip-Flops and Latches — Cell C builds a flip-flop; Cell F accidentally builds a latch.
Synthesis vs Simulation — Cell E is the canonical mismatch.
Finite State Machines in HDL — Cell H is a one-state FSM seed.
Number Systems and Bit-Widths — Cells G & H rely on width/wrap and sign arithmetic.
Testbenches — where you'd stimulate these edges to reproduce every trace above.