In short: Use blocking assignments (=) for combinational logic and non-blocking assignments (<=) for clocked logic, because non-blocking updates are deferred to the NBA region and therefore cannot race with other always blocks sampling the same clock edge.
Why two assignment operators exist
Hardware is concurrent: on a clock edge, every flip-flop samples its D input at the same instant and only then presents a new Q. A software-style simulator, however, executes one process at a time. Verilog bridges this gap with two operators. A blocking assignment evaluates and updates immediately, so the next statement sees the new value. A non-blocking assignment evaluates the right side now but schedules the update for later, so every process that wakes on the same edge still sees the old value.
If you want the short version of the syntax, our guide to blocking vs non blocking Verilog covers it. This article goes deeper into the scheduler, because that is what interviewers at Synopsys, Cadence and Siemens EDA partner companies actually probe.
The stratified event queue
IEEE 1364 divides each simulation time step into ordered regions. The ones that matter for RTL are the Active region, the Inactive region (#0 delays), the NBA region, and the Observed/Postponed regions where $monitor and $strobe print. Within the Active region, the order in which ready processes run is deliberately undefined. That non-determinism is the root of every Verilog race.
Key fact: A non-blocking assignment samples its right-hand side in the Active region but writes its left-hand side only in the NBA region, after every Active event at that time step has finished.
The classic shift register mistake
Consider a three-stage shift register written with blocking assignments in a clocked block:
always @(posedge clk) begin
q1 = d;
q2 = q1;
q3 = q2;
end
On each edge, q1 takes d, q2 immediately takes the new q1, and q3 takes the new q2. All three registers collapse to d after a single clock: you built one flop with two useless aliases. Synthesis tools such as Synopsys Design Compiler or Vivado will often infer the same collapsed hardware, so this is not merely a simulation quirk. Replace = with <= and the hardware becomes a true three-cycle delay line.
Watching it happen
The animation below shows the correct non-blocking behaviour: on each clock edge, every bit moves exactly one stage, because all right-hand sides were sampled before any left-hand side changed.
Where races actually come from
A race exists when two processes touch the same variable in the same time step and the result depends on the unspecified Active-region order. Two common patterns:
- Clock-edge sampling. One block does
a = b;on posedge clk while another doesc = a;. Whether c gets the old or new a depends on which block the simulator picked first, and it can differ between VCS, Xcelium and Questa, or even between two runs after a recompile. - Testbench-versus-DUT. A testbench that drives inputs with blocking assignments exactly on the clock edge competes with the DUT sampling them. Driving with a small delay, or using a SystemVerilog clocking block, removes the race.
Gate-level and zero-delay simulations make this worse, since every event lands in the same time step. Moving to non-blocking assignments for all registers makes the sampling order irrelevant, which is exactly why the rules below work.
Key fact: Mixing = and <= on the same variable inside one always block is legal but undefined in synthesis behaviour, and most lint tools (Spyglass, Verilator, Ascent Lint) flag it as an error.
The coding guidelines that avoid races
| Situation | Use | Reason |
|---|---|---|
Sequential block, always @(posedge clk) | <= | Models flop behaviour; RHS sampled before any update |
Combinational block, always @(*) | = | Later statements must see earlier results |
Latch, always @(*) with enable | <= | Matches storage element semantics |
Continuous assign | assign | Net driver, not a procedural assignment |
| Same variable from two always blocks | Never | Multiple drivers, race and synthesis error |
- Never use a #delay on the left of a non-blocking assignment in synthesizable code; inter-assignment delays like
q <= #1 d;are acceptable in simulation only. - Assign each variable from exactly one always block.
- Use
always @(*)(oralways_combin SystemVerilog) so the sensitivity list cannot go stale. - Keep types straight: variables written in procedural blocks are
reg, nets are driven by assign. See reg vs wire in Verilog and the Verilog data types guide for the details.
A subtle trap: a non-blocking assignment to a bit-select or array element in a combinational block can create ordering surprises, because partial updates are deferred while later reads see stale data. Keep combinational logic blocking, and compute into a temporary variable before registering it.
For a second perspective on the same rules, see blocking vs non blocking assignments from the VLSI side, where the focus is on how synthesis maps each style to flops and gates.
Practical verification tips
Run a lint pass before simulation; Spyglass and Verilator catch mixed assignment styles in seconds. In simulation, vary the process order: VCS offers +ntb_random_seed, and Questa offers randomised scheduling options that expose latent races by reordering Active events. If a design behaves differently under reordering, you have a race. Finally, equivalence-check RTL against the netlist with Synopsys Formality or Cadence Conformal; a collapsed shift register will show up immediately as a mismatch in register count.
Training teams that want hands-on practice with these flows can look at ChipXpert's VLSI training programmes, which pair RTL labs with real tool flows.
Ready to code RTL that simulates and synthesizes the same way every time? Explore the hands-on Verilog and FPGA tracks on our CourseTron courses page.
Frequently asked questions
Can I use non-blocking assignments in combinational logic?
You can, and a complete assignment list will still synthesize correctly, but simulation may glitch or lag by a delta cycle and readability suffers. The convention of blocking for combinational logic keeps behaviour predictable.
Why does the order of blocking assignments matter but not non-blocking ones?
Blocking assignments update immediately, so later statements read the new value. Non-blocking assignments all read their right sides first and write afterwards, so statement order within the block does not change the result, except when two writes target the same variable (the last one wins).
Is a Verilog race a simulator bug?
No. The standard intentionally leaves Active-region ordering undefined, so every compliant simulator may choose differently. The bug is in the RTL.
Does this change in SystemVerilog?
The operators behave identically, but SystemVerilog adds always_ff and always_comb, which let the tools check that you used the right style, and clocking blocks and program blocks that remove testbench races.