← Back to the blog

· 8 min read

Blocking vs. Non-Blocking in Verilog: The Scheduler Explains Everything

  • verilog
  • systemverilog
  • blocking-assignment
  • non-blocking-assignment
  • simulation
  • rtl-design

Cliff Cummings stated in his SNUG 2000 paper — the one most Verilog engineers cite without having read — that six coding guidelines eliminate "90 to 100 percent of the most common Verilog simulation race conditions." Not reduce — the word is eliminate. The paper is twenty-five years old, it's free on sunburst-design.com, and those guidelines still aren't universally understood, which tells you something about how badly this subject gets taught.

The standard explanation you find everywhere is: use = in combinational blocks, use <= in clocked blocks. That's correct, but not enough — because it tells you what to do without telling you why, which means you can't diagnose the violation when it surfaces. The "why" is inside the simulator's event scheduler, and once you see it, the two rules stop feeling like rules and start feeling like physics.

A Verilog simulator doesn't execute your always blocks in one straight pass. At every simulation time step, it divides work across ordered regions. IEEE 1800-2017 §4.4 enumerates seventeen of them in SystemVerilog; the three that matter for this discussion are the Active region, the Inactive region (for #0 delays), and the NBA region — Non-Blocking Assignment update. When a blocking assignment (=) executes, it reads the RHS and writes the LHS immediately, right there in the Active region. When a non-blocking assignment (<=) executes, the simulator evaluates the RHS in the Active region and schedules the LHS update for later — after the Active region drains completely — in the NBA region. Every non-blocking RHS is sampled first, before any non-blocking LHS is written — snapshot first, then commit.

That two-phase behavior is not arbitrary. It's a structural model of how flip-flops actually work in silicon. A real D flip-flop samples its D input at the clock edge and updates its Q output afterward — the combinational path from D to Q through the flop doesn't exist during the transition. On a board with a hundred flip-flops, all hundred sample simultaneously on the rising edge, and all hundred update Q simultaneously. The NBA region does exactly that: all <= RHS values are captured first, then all LHS values are written. If you replace <= with = in a clocked always block, you lose that behavior and the simulator starts playing a different game than your synthesized hardware.

That mismatch is Bug 1, and I've seen it bite people who've been writing Verilog for years. Consider a two-stage shift register written with blocking assignments across separate always blocks:

// WRONG: blocking assignments in separate clocked always blocks
always @(posedge clk) q1 = d;
always @(posedge clk) q2 = q1;

The simulator picks an execution order for those two always blocks at each clock edge — and the standard gives it latitude to pick either one. If Block A fires first, q1 gets the new value of d, then Block B reads that new q1 — so q2 sees the new d in the same simulation time step, with zero clock cycles of delay. Data teleports through both stages in one tick. If Block B fires first, q2 reads the old q1 before Block A updates it, and the behavior matches hardware. ZipCPU's Dan Gisselquist identified blocking-in-sequential as the primary cause of sim/synth mismatch in practice, and this is exactly why: your simulation result depends on which tool runs your always blocks in which order. Synthesis doesn't care about that order — it produces one cycle of delay per stage regardless.

The fix is non-blocking, and it's not a convention:

// CORRECT: non-blocking assignments in clocked blocks
always @(posedge clk) q1 <= d;
always @(posedge clk) q2 <= q1;

Now both RHS expressions (d and q1) are evaluated in the Active region, reading the values that existed at the start of this time step. Both LHS updates (q1 and q2) land in the NBA region, after the Active region is done. The order the simulator runs the two always blocks doesn't matter anymore — you always read the old values, you always write the new values after. One cycle per stage, every time, matching the synthesized hardware exactly.

Bug 2 goes the other direction. If you use non-blocking assignments inside an always @(*) or always_comb block, the LHS update doesn't land until the NBA region — which means that any logic in the same time step that reads that signal will see the stale value from before the NBA update. Berkeley's EECS151 course notes flag this explicitly: non-blocking in combinational logic causes stale reads, and some synthesis tools respond by inferring a latch to hold the value across time steps. That latch is almost certainly not what you intended, and it produces a synthesis result that doesn't match your simulation.

// WRONG: non-blocking in a combinational block
always @(*) begin
    y <= a & b;    // LHS update deferred to NBA region
    z <= y | c;    // Reads OLD y, not the value just assigned above
end
// Synthesized result: z = (a & b) | c  — reads the updated y immediately
// Simulation result:  z reads stale y  — mismatch

Bug 3 is the one that shows up in "quick patches." Someone has a signal already driven by <= in one context, and they add a blocking = to the same variable somewhere else — maybe to initialize it, maybe to fix a compile warning. IEEE 1800 does not define the outcome when a signal is written by both a blocking assignment (Active region) and a non-blocking assignment (NBA region) in the same simulation time step. Different simulators produce different results. There is no correct answer according to the standard — the behavior is undefined, and you can't rely on any simulator to be consistent about it.

Cummings' six guidelines from the SNUG 2000 paper turn all of this into a checklist you can hold in your head. Use = in purely combinational always blocks. Use <= in purely sequential always blocks. When you're modeling both sequential and combinational behavior in one block (which you should generally avoid, but it happens), use <=. Never use a #0 delay as a workaround for ordering problems — that's the Inactive region hack, and Cummings' 2002 SNUG paper explains in detail why it creates more problems than it solves. Never assign the same variable from blocking and non-blocking assignments in different always blocks — that's Bug 3, undefined by the standard. And never mix = and <= for the same variable within a single always block. Six rules, and if you follow them, the most common classes of Verilog race conditions don't arise.

A non-blocking assignment says: "remember this value; I'll apply it after everyone's done reading the current state." That's exactly what a flip-flop does in silicon. If your sequential logic uses <=, the simulator is modeling your circuit correctly. If it uses =, the simulator and the synthesizer are solving different problems, and your simulation is lying to you.

This is also why the question shows up in RTL design interviews — and not just at junior levels. "What's the difference between = and <=?" is the surface version; any candidate who prepped will have a memorized answer. The follow-up that separates people is: "Look at this code with two blocking always blocks on the same signal. What does the simulator output?" If you can reason through the Active-region ordering and explain why the result is simulator-dependent, you've demonstrated that you understand event scheduling, not just syntax. The question "what is the NBA region?" is even more direct about this — it's testing whether you know there's a formal scheduling model at all, not just a vague notion that <= "happens at the end of the time step."

The classic swap test — a <= b; b <= a; across two always blocks — correctly swaps the values because both RHS expressions are evaluated before either LHS is written. Try to do that swap with blocking assignments and you get undefined behavior: one of the always blocks runs first, overwrites the variable, and the second always block reads the clobbered value. You get either a = a or b = b depending on simulator ordering. The harder question is: "what happens if you write q = a; q <= b; in the same always block?" The answer is that IEEE 1800 doesn't specify it. The simulator may give you a, may give you b, and may give you something else on a different tool. The point is whether you know that mixing the two assignment types to the same target is a standard-level undefined behavior, not just a style issue.

Verilator, as of at least issue #5210 on its GitHub tracker, handles non-blocking assignments in initial blocks differently from Icarus Verilog — treating them as blocking. This is a deviation from the standard. If you're comparing simulation results across tools and seeing a discrepancy in initial blocks, that's the place to look.

The rule "use = in comb, use <= in sequential" is correct, and it's the right thing to tell someone who's brand new. But it's not a superstition — there's a real mechanism behind it, grounded in IEEE 1800 §4.4 and in how flip-flops physically behave. If you understand the NBA region, you understand why the rule exists, you can explain the failure modes when it's violated, and you can read someone else's code and spot the wrong assignment type on sight. That's the difference between following the rule and understanding it.

Want to work through RTL problems that test exactly this — assignment types, simulation races, and scheduler behavior? Try Logicode.