In short: Use blocking = for combinational logic inside always @(*) and non-blocking <= for every flip-flop inside always @(posedge clk) — mixing them in one block is the classic source of simulation race conditions and sim/synthesis mismatches.
Ask any VLSI interviewer for the single most common Verilog mistake a fresher makes, and “mixing blocking and non-blocking assignments” will be near the top of the list. The two operators look almost identical, both describe hardware, and both compile cleanly — yet one wrong choice can produce a design that passes in simulation and fails on silicon, or worse, behaves differently every time you run it. This article unpacks exactly what each operator does to the event scheduler, when to reach for each, and the race conditions that appear when you blur the line.
What the two operators actually do
A blocking assignment (=) executes immediately and in order. The right-hand side is evaluated and the left-hand side is updated before the simulator moves to the next statement — it blocks execution. A non-blocking assignment (<=) is different: the simulator evaluates every right-hand side first, then schedules all the left-hand updates to happen together at the end of the current time step. Nothing you write after a <= sees its updated value within the same evaluation pass.
This behaviour is not a style preference — it is defined by the IEEE 1364/1800 stratified event scheduler. Within one simulation time step, blocking assignments land in the active region, while non-blocking updates are deferred to the NBA (non-blocking assignment) region, which drains only after all active events are done.
Rule of thumb that survives code review at Synopsys VCS, Cadence Xcelium and Siemens Questa alike: sequential logic → non-blocking (<=); combinational logic → blocking (=).
Sequential logic: why <= models flip-flops correctly
Consider a two-stage shift register clocked on the same edge:
always @(posedge clk) begin q1 <= d; q2 <= q1; // sees OLD q1 end
Because both right-hand sides are sampled before any update, q2 gets the previous value of q1 — exactly what two cascaded flip-flops do in real hardware. Now swap in blocking assignments:
always @(posedge clk) begin q1 = d; q2 = q1; // sees NEW q1 -> collapses to one FF! end
Here q2 immediately picks up the fresh d, so your two-bit shift register degenerates into a single flip-flop driving both outputs. The RTL simulates one way; synthesis with Design Compiler or Genus may even infer something you never intended. This is the first lesson of a Verilog always block: the operator choice encodes register count.
<= but collapse to one with =.Combinational logic: why = is the right call
Inside an always @(*) block modelling combinational cones — adders, muxes, priority encoders — you want each intermediate to feed the next statement right away, like wires settling through gates:
always @(*) begin sum = a + b; carry = sum[4]; // needs the just-computed sum end
Blocking assignment gives you that immediate data flow. Use <= here and carry would read the stale sum from the previous evaluation, producing a phantom one-cycle delay in waveforms that has no corresponding hardware — a textbook sim/synth mismatch.
The race condition: mixing them across blocks
The nastiest bugs appear when two always blocks triggered by the same clock edge use blocking assignments to communicate. Suppose:
always @(posedge clk) a = b; // block 1 always @(posedge clk) b = a; // block 2
The IEEE standard does not define the order in which these two blocks execute at a shared edge. If block 1 runs first, a and b both end up equal to the old b; if block 2 runs first, both equal the old a. The result is non-deterministic — it can differ between VCS, Xcelium and Questa, and even between two runs under different seeds or +optimization flags. Rewrite both with <= and the sampled-then-updated semantics make the swap deterministic and correct.
| Context | Operator | Models | Risk if wrong |
|---|---|---|---|
always @(posedge clk) | <= | Flip-flops / registers | Register collapse, races between blocks |
always @(*) | = | Combinational gates | Phantom delays, sim/synth mismatch |
| Same signal both ways | — | (never do this) | Compiler warning, undefined order |
A single-signal rule that linters (SpyGlass, Questa autocheck) enforce: never assign the same variable with both = and <= across blocks, and never drive one reg from two always blocks.
<=.For deeper practice building these blocks correctly, our Verilog coding guidelines walk through lint-clean templates, and our instructor-led RTL track at ChipXpert VLSI training drills these patterns against real synthesis runs.
Three habits that eliminate the bugs entirely
- One block, one operator. Pick
=for combinational or<=for sequential, and never mix within a block. - Separate concerns. Keep combinational next-state logic in its own
always @(*)and the register update in a tinyalways @(posedge clk). This two-block FSM style removes almost all race questions. - Lint early. Run a linter before simulation; it flags same-variable mixed assignments and multi-driver nets in seconds, long before they cost you a tapeout spin.
Ready to write race-free RTL that survives synthesis? Explore CourseTron’s hands-on Verilog & RTL design track on our courses page and build flip-flop-perfect designs from day one.
Frequently asked questions
Can I ever use blocking assignments inside a clocked block?
Yes, for temporary variables that are declared and fully consumed within the same block before any non-blocking update — for example computing an intermediate with = then registering the result with <=. Even then, many style guides discourage it because it invites mistakes.
Why does my shift register work in simulation but synthesize wrong?
Almost always because you used blocking = in a clocked block, which the simulator resolves in statement order but synthesis interprets as fewer registers. Switch the clocked assignments to <=.
Do blocking and non-blocking exist in VHDL too?
VHDL has variable assignment (:=, immediate, like blocking) and signal assignment (<=, deferred, like non-blocking). The scheduling principle is the same, so the discipline transfers directly.
Which simulators are strictest about this?
All three big tools — Synopsys VCS, Cadence Xcelium and Siemens Questa — follow the IEEE event scheduler, but their race warnings and optimization flags differ, which is exactly why non-deterministic code can pass on one and fail on another.