Verilog bug catalog

Blocking vs Non-Blocking Assignment in Verilog: When to Use Each

The single most repeated rule in Verilog and SystemVerilog style guides is also the most violated: use non-blocking <= in sequential (clocked) always blocks, and use blocking = in combinational always blocks. Get it backwards and you introduce simulator-only race conditions, RTL-versus-gate mismatches, and bugs that pass on one tool and fail on another. This page covers the rule, the IEEE 1800 scheduling semantics that make it necessary, the classic broken examples (shift register, swap, comb feedback) with correct fixes, why synthesis can hide the bug, and how to enforce the rule in lint and CI.

The rule in two lines

  • Sequential (clocked): always_ff @(posedge clk) q <= d; — non-blocking.
  • Combinational: always_comb sum = a + b; — blocking.

Two corollaries make the rule airtight: never assign the same variable from two different always blocks, and never mix = and <= on the same signal inside one block. Cliff Cummings' 2000 SNUG paper Nonblocking Assignments in Verilog Synthesis, Coding Styles That Kill! is the canonical reference, and decades of style guides (Sutherland, Mills, Cadence, Synopsys) converge on the same guidance. The reason is the simulation scheduler defined by IEEE 1800, not aesthetics.

Why the rule exists: the event regions

SystemVerilog defines an ordered set of event regions that the scheduler iterates within each simulation time step. The two that matter for assignment style are the Active region and the NBA (Non-Blocking Assign update) region. A blocking assignment evaluates its right-hand side and writes its left-hand side immediately, in the Active region, so the very next statement sees the new value. A non-blocking assignment is a two-step operation: its right-hand side is sampled in the Active region, but the left-hand side update is queued and committed later, in the NBA region, after every blocking assignment scheduled in that time step has run.

That two-step semantic is exactly what makes a chain of flip-flops behave like real hardware. Every flop sees the value its predecessor held before the clock edge, no matter what order the simulator happens to visit the always blocks, because all the right-hand sides are sampled before any left-hand side is committed. With blocking assignments the order matters: if the scheduler runs a = d before b = a, then b captures the new a; if it runs them in the opposite order, b captures the old a. Both orderings are legal under the LRM. That undefined ordering is the race.

The classic race: a shift register done wrong

The textbook example is a two-stage shift register (or two-flop synchroniser) written with blocking assignments across two clocked blocks. The two blocks read and write the same variable a on the same edge, so whether the second block sees the old or the new a depends purely on scheduling order.

// BUG: blocking assignment in clocked blocks creates a race.
// 'b' may take either the old or the new value of 'a' depending on
// which always block the simulator schedules first.
module shift_bug (
    input  logic clk,
    input  logic d,
    output logic q
);
    logic a, b;

    always @(posedge clk) a = d;   // blocking
    always @(posedge clk) b = a;   // blocking: reads an 'a' that may have changed

    assign q = b;
endmodule

The fix is the canonical pattern: always_ff with non-blocking assignment. Every right-hand side is sampled at the edge, then every left-hand side commits together in the NBA region, regardless of statement order or block order. The hardware is two flip-flops in series, and now the simulation matches it deterministically.

// FIX: non-blocking in clocked blocks. Every RHS is sampled at the
// edge, then every LHS updates together in the NBA region. 'b' always
// reads the value 'a' held BEFORE the edge -> a true 2-stage shift reg.
module shift_fixed (
    input  logic clk,
    input  logic d,
    output logic q
);
    logic a, b;

    always_ff @(posedge clk) a <= d;
    always_ff @(posedge clk) b <= a;   // reads pre-edge 'a'

    assign q = b;
endmodule

The swap idiom: where non-blocking earns its keep

Exchanging two registers on a clock edge is the cleanest illustration of why the operator matters. With blocking assignments the first line overwrites the source the second line needs, and nothing swaps.

// BUG: swap with blocking assignments. The first statement clobbers
// 'x' before the second can read its old value, so both end up as
// the old 'y'. This does NOT swap.
always_ff @(posedge clk) begin
    x = y;    // x now holds y
    y = x;    // y reads the NEW x, i.e. y -- no swap
end

With non-blocking assignments both right-hand sides are sampled (old x, old y) before either left-hand side updates, so the values cross over correctly. No temporary is needed.

// FIX: swap with non-blocking. Both RHS are sampled (old x, old y)
// before either LHS updates, so the values exchange cleanly. This is
// the idiom non-blocking assignment was designed for.
always_ff @(posedge clk) begin
    x <= y;   // RHS sampled: old y
    y <= x;   // RHS sampled: old x
end           // NBA region: x<=old y, y<=old x  -> swapped

The reverse anti-pattern: non-blocking in combinational logic

Using <= in a combinational block is technically legal but wrong in practice. The left-hand side update is deferred to the NBA region, so any logic that reads it within the same time step sees the stale value. In a feedback path the design oscillates across a chain of delta cycles, and because synthesis maps it back to plain combinational gates that settle instantly, post-synthesis simulation disagrees with the RTL.

// BUG: non-blocking in a combinational block. The LHS update is
// deferred to the NBA region, so anything that reads 'sum' in the same
// time step sees the stale value. In a feedback path this oscillates
// across delta cycles and diverges from the synthesized gates.
always_comb begin
    sum  <= a + b;          // wrong: '<=' in comb
    diff <= a - b;
end

The fix is to use blocking assignments, so each result is current the instant it is written, matching the zero-delay gate network synthesis produces.

// FIX: blocking in combinational logic. Each LHS is current the
// instant it is written, matching the zero-delay gates synthesis builds.
always_comb begin
    sum  = a + b;
    diff = a - b;
end

The event regions, in plain English

You do not need to memorise the regions to write good RTL, but knowing them removes the magic. Within a single time step the scheduler iterates these regions in order:

  • Active: blocking assignments execute and become visible immediately. Continuous assignments, primitive evaluations, and the right-hand-side sampling of non-blocking assignments also happen here.
  • Inactive: #0-delayed statements run. Avoid these in synthesisable RTL.
  • NBA (Non-Blocking Assign update): non-blocking assignments commit their previously-sampled right-hand-side values to their left-hand sides.
  • Observed: SystemVerilog assertion properties are evaluated against sampled values.
  • Reactive: program-block and #0 testbench code scheduled with <= in reactive context runs.
  • Postponed: $strobe and $monitor end-of-time-step display tasks run, after all values have settled.

The scheduler can loop back from a later region into Active (an iterative time step) until the design settles, then advances the clock. The Active and NBA regions are the two to remember: everything in an always_ff with <= samples in Active and commits in NBA, which is precisely what makes flip-flop chains deterministic and order-independent.

The narrow exception: block-local temporaries

The one place a blocking assignment belongs inside a clocked block is a temporary local variable that is written before it is read, lives entirely within the block, and is never referenced from outside. The common idiom computes an intermediate with = and publishes the result to a register with <=.

// OK: blocking is fine for a block-local temporary that is written
// before it is read and never escapes the block. Compute with '=',
// publish with '<='.
always_ff @(posedge clk) begin
    logic [7:0] tmp;
    tmp   = a + b;      // local, blocking
    sum_q <= tmp;       // published register, non-blocking
end

That is the only safe blocking-in-sequential pattern. If you find yourself blocking-assigning then non-blocking-assigning the same signal, or driving one signal from two blocks, refactor: you are reintroducing the very race the rule exists to prevent. Testbench code is the other place blocking and non-blocking mix freely — stimulus drives with <= to align to the NBA region and checks with = on locals — but that is verification code, not synthesisable RTL.

Why synthesis hides the bug: sim/synth mismatch

Here is the trap that makes this bug so durable. Synthesis infers a flip-flop from the posedge in the sensitivity list, not from the assignment operator. So a clocked block written with blocking assignments frequently synthesises to exactly the register you intended — the gate-level hardware is correct. The defect lives only in simulation: the RTL can race while the netlist cannot, so RTL simulation and gate-level simulation diverge. A passing RTL regression gives false confidence, and the failure only appears later, at gate level or in silicon.

There are two distinct failure modes. First, races by definition produce implementation-dependent behaviour: a test that passes on one simulator can fail on another, or pass at your desk and fail in CI. Second, RTL and post-synthesis simulation run different netlists, so blocking-in-sequential code can simulate cleanly at RTL (the tool happens to schedule blocks in source order) and then break in gate-level simulation, where the resynthesised netlist has different driver topology. Neither iverilog nor verilator warns about it by default — you see it as a sporadic CI failure, a gate-level hang, or, worst case, a respin.

How to enforce the rule at scale

There are three pragmatic, layered approaches:

  • Use the typed always constructs. Migrate every clocked block to always_ff and every combinational block to always_comb. These SystemVerilog constructs let the compiler enforce intent: a tool can reject a latch inferred under always_comb or a multiply-driven signal, and the explicit intent makes a stray operator obvious in review.
  • Lint the source. Run Verible lint with its always-comb / always-ff and explicit-begin style rules enabled to flag the mismatched operator at commit time.
  • Static analysis on the AST. Classify each always block by its sensitivity list and report any = in a clocked block or any <= in a combinational block, plus any signal driven from more than one block.

ChipVerify AI is a pre-signoff RTL evidence tool (closed beta — not foundry signoff), and its TinyTapeout scanner flags this rule as blocking_in_sequential and nonblocking_in_combinational automatically, with a per-finding hint pointing at the exact assignment. It is one lint check among many in a broader RTL verification workflow, not a substitute for it.

FAQ

What is the difference between blocking (=) and non-blocking (<=) in Verilog?

A blocking assignment (=) evaluates its right-hand side and updates its left-hand side immediately, before the next statement runs, so later statements in the same block see the new value. A non-blocking assignment (<=) samples its right-hand side at the current time but defers the left-hand side update to the Non-Blocking Assign (NBA) region, after every blocking statement in the time step has finished. Use blocking in combinational always_comb blocks and non-blocking in clocked always_ff blocks.

Why must clocked always blocks use non-blocking assignments?

Because non-blocking assignments sample every right-hand side before any left-hand side updates, every flip-flop in a chain reads the value its source held before the clock edge, regardless of the order the simulator visits the always blocks. With blocking assignments in clocked blocks the result depends on that visitation order, which the IEEE 1800 standard leaves unspecified, producing a race condition.

Does the order of statements inside one always block matter?

For non-blocking assignments to different registers, no: all right-hand sides are sampled before any left-hand side updates, so their textual order does not change the result. (Multiple non-blocking writes to the SAME register in one block are ordered — the last one scheduled wins.) For blocking assignments, yes: statements execute top to bottom and each one sees the new value produced by the prior statement. Use that property deliberately for local intermediates, not to model flip-flops.

Why does using = in a clocked block sometimes still synthesize to the correct hardware?

Synthesis infers a flip-flop from the clock edge in the sensitivity list, not from the assignment operator, so it often builds the register you intended even with blocking assignments. The problem is a simulation/synthesis mismatch: the gate-level netlist behaves like real hardware while the RTL simulation can race, so RTL and gate-level simulations disagree and a passing RTL test gives false confidence.

Can you ever use a blocking assignment inside a clocked always block?

Yes, for a temporary local variable that is written before it is read within the same always block and never referenced from outside it. A common idiom computes an intermediate with a blocking assignment and then publishes the result with a non-blocking assignment to the output register. Do not blocking-assign a signal that another block or a later non-blocking statement also drives.

Related rules

  • Inferred latches — non-blocking inside a combinational block, or an incomplete always_comb, is a common cause of latch inference on top of the race issues.
  • Width mismatches — the next bug class to scan for once your assignment style is clean: silent truncation and sign-extension surprises.
  • Clock domain crossing — the two-flop synchroniser in the shift-register example is the canonical CDC structure; getting the assignment style right is a prerequisite for trustworthy CDC analysis.
  • Verible lint rules — the source-level style checks that flag mismatched assignment operators at commit time.
  • Verilator simulation — why a race can pass on one simulator and fail on another, and how RTL vs gate-level runs expose the mismatch.
  • RTL verification — where assignment-style linting fits in the broader pre-signoff evidence workflow.

Scan your repo

Scan your repo for blocking-vs-nonblocking violations: paste a GitHub URL at chipverify.ai/tinytapeout and you will get the rule-matched always blocks it detects whose sensitivity list and assignment operator do not match, plus any signal driven from more than one block, ranked by likely impact — a pre-signoff lint check, not an exhaustive proof. No install, no setup.

Find assignment-style races in your RTL

Sign in and point ChipVerify AI at your design. Its lint flags always blocks whose sensitivity list and assignment operator disagree, plus multi-driver signals, with file-and-line evidence — a pre-signoff structural check, not a foundry signoff.