Skip to content

SystemVerilog · Module 5

Blocking vs Non-Blocking Assignments

= vs <=, the NBA scheduler, the golden rule, race conditions.

Module 5 · Page 5.6

This page is the SystemVerilog treatment; the Verilog-track lesson on the same operators is blocking and non-blocking assignments, and the multi-process consequences live in race conditions and determinism. The blocks these assignments appear in are procedural blocks.

The Two Assignment Operators

= Blocking Assignment

Executes in order, immediately. The next statement in the same block sees the updated value. Think of it like a regular C assignment.

<= Non-Blocking Assignment (NBA)

Evaluates the RHS now but schedules the LHS update for the end of the current time step. All NBA RHS expressions in a time step are sampled before any LHS is written.

SystemVerilog — blocking vs. non-blocking behaviour
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Blocking (=): sequential execution ──────────────────────────
always_comb begin
    a = b + 1;     // a is immediately b+1
    c = a * 2;     // c uses the NEW value of a → c = (b+1)*2
end
 
 
// ── Non-blocking (<=): all RHS captured first ───────────────────
always_ff @(posedge clk) begin
    a <= b + 1;    // schedules a = b+1 for end of time step
    c <= a * 2;    // uses OLD value of a (before posedge) → c = a_old*2
end
// Both a and c update simultaneously at end of time step.
// This is exactly how flip-flops work in real hardware.
 
 
// ── The classic swap: only non-blocking works without a temp ────
always_ff @(posedge clk) begin
    a <= b;         // schedule: a ← old b
    b <= a;         // schedule: b ← old a
end
// Result: a and b are swapped. No temp variable needed.
 
// ── With blocking = the swap fails ──────────────────────────────
always_ff @(posedge clk) begin
    a = b;          // a is immediately b — old a is LOST
    b = a;          // b = b (the same value!) — swap broken
end

🧠 The Most Important Mental Model: = Sees New Values, <= Sees Old Values

When reading code that uses <=, ask yourself: "What did this signal look like before this time step started?" That is what the RHS captures. With =, ask: "What did the previous statement in this block just set this to?" That is what you see. This distinction drives everything: why pipelines need <=, why combinational chains need =, and why mixing them in the same block is non-deterministic.

The NBA Scheduler — How It Works

SystemVerilog's event scheduler divides each time step into two phases. Understanding this is the key to understanding why <= behaves the way it does. Figure 1 — The Two Phases of a Simulator Time Step

Tutorial diagram

Figure 1 — Each simulator time step has two phases. Blocking assignments happen in Phase 1. Non-blocking LHS updates all happen together in Phase 2, after all RHS values have been sampled.

🚀 RTL Design Insight: Why NBA Makes Multi-Block Designs Deterministic

Without the NBA scheduler, two always_ff blocks at the same posedge clk would race to read and write the same signals — the result would depend on which block the simulator processes first (non-deterministic). The NBA scheduler eliminates this by separating READ and WRITE: all RHS expressions are evaluated in the Active region (everyone reads the pre-clock-edge values simultaneously), then all LHS assignments happen in the NBA region (everyone writes simultaneously). Order of always_ff block processing becomes irrelevant — the result is always the same. This is why <= is mandatory in always_ff.

The Golden Rule

always_comb → use =

Combinational logic has no memory. Statements execute in order and you want each result to be available immediately to the next statement. Use =.

always_ff → use <=

Flip-flops sample their input and hold the value until the next clock edge. Non-blocking exactly models this behaviour — all outputs update simultaneously. Use <=.

SystemVerilog — the golden rule in practice
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── CORRECT: = in always_comb ───────────────────────────────────
always_comb begin
    sum    = a + b;         // purely combinational
    carry  = sum[8];        // uses updated sum immediately
    result = sum[7:0];
end
 
 
// ── CORRECT: <= in always_ff ─────────────────────────────────────
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        q     <= '0;
        count <= '0;
    end else begin
        q     <= d;
        count <= count + 1;
    end
end
 
 
// ── WRONG: = in always_ff ────────────────────────────────────────
always_ff @(posedge clk) begin
    q = d;      // ⚠ blocking in clocked block — race condition risk
end
// Simulates correctly when only one always_ff drives q.
// Breaks silently if two always_ff blocks both assign q.
 
 
// ── WRONG: <= in always_comb ─────────────────────────────────────
always_comb begin
    out <= a | b;  // ⚠ NBA in comb: out updates at end of delta, not now
                   // subsequent uses of out see the OLD value
end

🏗 Synthesis Concern: = in always_ff Creates Tool-Dependent RTL

When synthesis sees = inside always_ff, different tools behave differently. Synopsys DC infers a flip-flop (ignoring the blocking semantics and treating it as <=). Cadence Genus may generate the same. But now your simulation model (which uses blocking semantics — immediate update) disagrees with your synthesized netlist (which always uses flip-flop capture semantics). This is the simulation-synthesis mismatch — the netlist does the right thing, but your verification environment validated the wrong behavior. The discrepancy is only revealed during gate-level simulation or integration testing.

Race Conditions — When = in always_ff Goes Wrong

A race condition between two always_ff blocks occurs when both blocks assign the same signal using blocking assignment. Which block executes first is determined by the simulator's event scheduler — not by the code order, not by any rule you can rely on. The result is simulator-dependent and will not match synthesised hardware.

SystemVerilog — race condition caused by blocking in always_ff
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Classic race: shift register with blocking ──────────────────
// WRONG — outcome depends on which always_ff the simulator runs first
always_ff @(posedge clk) a = d_in;   // ⚠ blocking
always_ff @(posedge clk) b = a;      // ⚠ which 'a'? old or new?
 
// If 'a' block runs first: b captures the NEW a (= d_in) — acts like wire
// If 'b' block runs first: b captures the OLD a — acts like flip-flop
// Synthesis always infers a flip-flop. Simulation may not match.
 
 
// ── CORRECT: non-blocking eliminates the race ───────────────────
always_ff @(posedge clk) a <= d_in;   // schedules NBA
always_ff @(posedge clk) b <= a;      // RHS sampled before NBA region
 
// Both RHS values captured in Phase 1 (a_old, b_old).
// Both LHS updated in Phase 2: a = d_in, b = a_old.
// Deterministic regardless of block evaluation order. Matches hardware.

🔍 Debugging Insight: The Two-Simulator Test for Blocking Races

The definitive way to find blocking assignment races in always_ff is to simulate on two different tools — VCS and Questa (or Xcelium). If the results differ between tools, you have a race condition. VCS and Questa have different event scheduling implementations — they process always_ff blocks in different orders within the same time step. Non-blocking (<=) code produces identical results regardless of order. Blocking (=) code may produce different results depending on which block the tool processes first. This is a build-your-CI-pipeline-now insight: always run regressions on two simulators.

Proving It — the Race, Run Twice

The page's central claim is that blocking assignment in always_ff makes the result depend on which block the simulator happens to evaluate first. That is a claim about scheduling, and scheduling claims deserve evidence rather than assertion.

The module below builds the same two-flop shift register three ways and lets one simulation report all three. The only difference between the first two is the textual order of two concurrent always_ff blocks — an edit with no meaning in the language, since concurrent blocks have no source-order semantics.

nba_race_proof.sv — self-checking; compiles standalone
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────────
//  Three shift registers, identical intent:
//    A: blocking, "producer" block written first
//    B: blocking, "consumer" block written first   <- same logic, swapped text
//    C: non-blocking (correct)
//
//  A and B differ only in the order two concurrent always_ff blocks appear in
//  the file. If they disagree, the result was never in the logic.
//
//  vcs -sverilog nba_race_proof.sv && ./simv
//  xrun -sv nba_race_proof.sv     |     iverilog -g2012 -o r nba_race_proof.sv && ./r
// ─────────────────────────────────────────────────────────────────────────────
module nba_race_proof;
 
  timeunit 1ns; timeprecision 1ps;
 
  logic clk = 1'b0, d = 1'b0;
  logic a1, a2, b1, b2, c1, c2;
  int   errors = 0;
 
  always #5 clk = ~clk;
 
  // ── A: blocking, producer first ───────────────────────────────────────
  always_ff @(posedge clk) a1 = d;      // producer
  always_ff @(posedge clk) a2 = a1;     // consumer - which a1?
 
  // ── B: blocking, consumer first. Same two blocks, swapped in the file. ─
  always_ff @(posedge clk) b2 = b1;     // consumer
  always_ff @(posedge clk) b1 = d;      // producer
 
  // ── C: non-blocking. Order cannot matter: all RHS sample before any LHS
  //      updates, so both blocks read pre-edge values whatever the order.
  always_ff @(posedge clk) c1 <= d;
  always_ff @(posedge clk) c2 <= c1;
 
  initial begin
    // Drive a single 1 through the shift register and watch stage 2.
    @(negedge clk) d = 1'b1;
    @(negedge clk) d = 1'b0;            // one-cycle pulse
    @(posedge clk);                      // edge 2: a true 2-stage sees it now
    #1;
 
    $display("  after two edges:  a2=%0b   b2=%0b   c2=%0b", a2, b2, c2);
    $display("");
 
    // C is the only version with a defined answer, so C is the only one
    // checked. Asserting a specific result for A or B would encode one
    // simulator's arbitrary scheduling choice as if it were the specification.
    if (c2 !== 1'b1) begin
      errors++;
      $display("  ** FAIL: non-blocking stage 2 did not see the pulse");
    end
 
    if (a2 !== b2)
      $display("  ** A and B DISAGREE. Identical logic, opposite answers - the\n"
               "     only difference is which block was typed first.");
    else
      $display("  ** A and B agreed on THIS simulator. That is luck, not a\n"
               "     guarantee: nothing in IEEE 1800 orders these two blocks.");
 
    $display("  ** C is order-independent by construction.");
    if (errors == 0) $display("\n  [PASS] non-blocking behaved as a 2-stage pipeline");
    else             $display("\n  [FAIL] %0d check(s) failed", errors);
    $finish;
  end
 
endmodule

Representative output.

simulation log
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  after two edges:  a2=1   b2=0   c2=1
 
  ** A and B DISAGREE. Identical logic, opposite answers - the
     only difference is which block was typed first.
  ** C is order-independent by construction.
 
  [PASS] non-blocking behaved as a 2-stage pipeline

Note what is and is not checked. Only version C is asserted. A self-check that demanded a2 == 1 would freeze one tool's arbitrary evaluation order and present it as correct behaviour — the precise mistake this page warns against. When a design has no defined answer, the honest test reports what happened and refuses to grade it.

Two things to try on this file. Your simulator may print the same value for a2 and b2; that is not a refutation — it means this tool happens to resolve the order consistently, and tells you nothing about the next tool or the next version. And swapping the two c blocks changes nothing at all, which is the whole argument for <= in one experiment: the correct version is immune to an edit that has no meaning.

Common Mistakes at a Glance

SystemVerilog — mistakes and their correct forms
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Mistake 1: mixing = and <= in the same always_ff ────────────
always_ff @(posedge clk) begin
    temp = a + b;    // ⚠ blocking — tool-dependent, lint error
    out  <= temp;    // ⚠ uses immediately-updated temp — breaks NBA model
end
 
// FIX: split into comb + ff, or use NBA throughout
always_comb temp = a + b;           // combinational intermediate
always_ff @(posedge clk) out <= temp;
 
 
// ── Mistake 2: NBA in always_comb for intermediate ──────────────
always_comb begin
    mid <= a ^ b;   // ⚠ NBA in comb: mid hasn't updated when next line runs
    out <= mid | c;  // ⚠ reads OLD mid
end
 
// FIX: use blocking in always_comb
always_comb begin
    mid = a ^ b;    // mid updates immediately
    out = mid | c;  // uses NEW mid
end
 
 
// ── Mistake 3: using = for reset in always_ff ───────────────────
always_ff @(posedge clk) begin
    if (!rst_n) q = '0;   // ⚠ blocking reset — inconsistent with NBA data path
    else        q <= d;
end
 
// FIX: use <= for reset too
always_ff @(posedge clk) begin
    if (!rst_n) q <= '0;   // consistent NBA throughout
    else        q <= d;
end

Quick Reference

OperatorNameWhen LHS updatesUse inModels
=BlockingImmediately, in statement orderalways_comb, functions, tasksCombinational logic, sequential calculations
<=Non-blocking (NBA)End of current time step (NBA region)always_ff, always_latchFlip-flops, registers, state elements

🧠 Delta Cycle Deep Dive — The Full Event Scheduler

The simplified "Phase 1 / Phase 2" model is useful but incomplete. The IEEE 1800 simulator has nine distinct event regions within each simulation time step. Understanding all of them explains why $display sometimes shows wrong values, why assertions sample differently from RTL, and why testbench code in program blocks sees different values than RTL.

RegionWhat ExecutesAssignment TypeRelevant To
ActiveBlocking (=) assignments, continuous assigns, $display, non-blocking RHS evaluation= executes; <= RHS capturedRTL (always_comb, always_ff), combinational logic
Inactive#0 delay events. Avoid in RTL — used for special ordering tricks.RareLegacy workarounds
NBANon-blocking LHS updates. All <= assignments commit simultaneously.<= LHS writtenalways_ff, all flip-flop outputs
ObservedSVA concurrent assertions sample stable values after NBA.Read-onlyassert property(...)
ReactiveProgram blocks, clocking block driven outputs.Testbench = or <=UVM drivers via clocking block
Postponed$strobe and $monitor. Always shows final settled values for the time step.Read-only$strobe — correct post-NBA display
SystemVerilog — $display vs $strobe: Active vs Postponed Region
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Why $display shows wrong value after posedge ─────────────────
always_ff @(posedge clk) q <= d;  // NBA: q updates in NBA region
 
initial begin
    forever begin
        @(posedge clk);
        // $display fires in ACTIVE region — BEFORE NBA updates q
        $display("$display: q=%h (may be old value)", q);   // ← sees q_old
 
        // $strobe fires in POSTPONED region — AFTER NBA updates q
        $strobe(" $strobe: q=%h (final correct value)", q);  // ← sees q_new
    end
end
 
// ── Expected Output (d=0x55, q was 0x00) ─────────────────────────
// $display: q=00  ← Active region: NBA not yet applied
// $strobe:  q=55  ← Postponed region: q updated by NBA
 
// ── Rule: Always use $strobe when printing flip-flop outputs.
// Use $display only for combinational values or testbench signals
// that are driven with blocking (=) assignments.

Delta Cycle Timeline — posedge clk at T=10nsT=10nsRegionActive Δ0 │ NBA Δ0 │ Active Δ1 │ PostponedEventalways_ff evals RHS │ q ← d_old │ comb sees q↑ │ $strobeq= q_old (unchanged) │= d_old (new) │ visible (d_old) │ finalcomb_out= f(q_old) │ unchanged │= f(d_old) │ final$displayfires here (sees q_old) ─┘ ← may be WRONG$strobe fires here ✅────────────────────────────────────────────────────────────────────── If comb_out changes in Δ1, it triggers another Active cycle (Δ2). This process repeats until no new events — simulation time then advances.

📊 Waveform Analysis — Reading NBA Behavior

When you understand what the NBA scheduler does, waveforms become instantly readable. The signature of correct non-blocking behaviour is that every flip-flop output changes on the same clock edge using pre-edge values. Any deviation from that pattern is a defect, and the two waveforms below are the same two-stage pipeline written both ways.

Non-blocking (correct) — two stages, 1-cycle offset

7 cycles
With non-blocking assignment stage1 lags d by one cycle and stage2 lags stage1 by one cycle, giving a true two-stage pipeline.stage2 lags stage1 by 1 cyclestage2 lags stage1 by 1cycleclkdstage1Xstage2XXt0t1t2t3t4t5t6
Figure 2 — the correct pipeline, written with non-blocking assignment. Both stages update on the same edge, and both sample the values that existed BEFORE that edge: stage1 takes d from the previous cycle, stage2 takes the previous stage1. The result is the 1-cycle offset between the two traces — stage2 always lags stage1 by exactly one cycle, which is what a two-stage pipeline is.

Blocking (wrong) — the two stages collapse into one

7 cycles
With blocking assignment stage2 takes the newly updated stage1, so both traces are identical and the two-stage pipeline collapses to a single stage.stage2 identical to stage1 - collapsedstage2 identical to stage1- collapsedclkdstage1Xstage2Xt0t1t2t3t4t5t6
Figure 3 — the same code with blocking assignment, and the pipeline is gone. Because = updates stage1 immediately, the very next statement reads the NEW stage1, so stage2 receives this cycle's d rather than last cycle's stage1. The two traces become identical: two registers collapsed into one. Two signals that should differ by exactly one cycle tracking each other on every edge is the visual signature of this bug.
the two versions, side by side
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Figure 2 — correct: both RHS sampled before either LHS updates
always_ff @(posedge clk) begin
  stage1 <= d;
  stage2 <= stage1;      // reads the PRE-EDGE stage1
end
 
// Figure 3 — wrong: stage1 updates immediately, stage2 reads the new value
always_ff @(posedge clk) begin
  stage1 = d;
  stage2 = stage1;       // reads the value assigned one line above
end

🔍 How to Spot Blocking Race in a Waveform

In the waveform viewer, look for two registered signals that should have a 1-cycle latency between them but appear to track each other identically on every clock edge. If stage2 and stage1 change together at every posedge instead of stage2 being one cycle behind stage1, you have a blocking assignment collapse. The fix: replace = with <= inside always_ff. The waveform will immediately show the correct 1-cycle offset.

⚙ Pipeline Modeling — Why <= Is Mandatory for Registers

Pipeline modeling is where the difference between = and <= has the most profound practical impact. Every N-stage pipeline in a design depends on non-blocking assignments to correctly model the 1-cycle latency between stages.

SystemVerilog — 4-Stage Pipeline: Non-Blocking vs Blocking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── CORRECT: 4-stage pipeline using non-blocking ─────────────────
module pipeline4_correct (
    input  logic       clk, rst_n,
    input  logic [7:0] d,
    output logic [7:0] q4
);
    logic [7:0] s1, s2, s3;
 
    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            s1 <= '0; s2 <= '0; s3 <= '0; q4 <= '0;
        end else begin
            s1 <= d;   // RHS: d_old
            s2 <= s1;  // RHS: s1_old (= d from 1 cycle ago)
            s3 <= s2;  // RHS: s2_old (= d from 2 cycles ago)
            q4 <= s3;  // RHS: s3_old (= d from 3 cycles ago)
        end                // All LHS update simultaneously in NBA → 4-cycle latency
    end
endmodule
 
// ── WRONG: 4-stage pipeline using blocking — all collapse to 1 stage
module pipeline4_broken (
    input  logic       clk, rst_n,
    input  logic [7:0] d,
    output logic [7:0] q4
);
    logic [7:0] s1, s2, s3;
 
    always_ff @(posedge clk) begin
        s1 = d;    // s1 immediately = d (new value)
        s2 = s1;   // s2 immediately = d (new s1) — 1 cycle latency GONE
        s3 = s2;   // s3 immediately = d (new s2) — 2 cycle latency GONE
        q4 = s3;   // q4 immediately = d (new s3) — 3 cycle latency GONE
    end            // Synthesis: q4 = d with 1 clock latency only (synthesis is right)
                   // Simulation: q4 = d with 1 clock latency (looks the same!)
                   // But: the INTERMEDIATE stages s1, s2, s3 are wrong in simulation
endmodule
 
// ── Test: verify the 4-cycle latency ─────────────────────────────
module tb;
    logic clk=0, rst_n;
    logic [7:0] d, q4_correct, q4_broken;
 
    always #5 clk = ~clk;
 
    pipeline4_correct u_c(.clk,.rst_n,.d,.q4(q4_correct));
    pipeline4_broken  u_b(.clk,.rst_n,.d,.q4(q4_broken));
 
    initial begin
        rst_n = 0; d = 8'h00;
        repeat(3) @(posedge clk); rst_n = 1;
        @(posedge clk); d = 8'hAA;   // push 0xAA into pipeline
        repeat(6) @(posedge clk);
        $strobe("correct: %h  broken: %h", q4_correct, q4_broken);
        // correct: appears at q4 after 4 cycles
        // broken: appears at q4 after 1 cycle
        $finish;
    end
endmodule

🏗 Synthesis Impact — What Each Form Generates in Hardware

Code FormSimulation BehaviorSynthesis ResultMatch?Risk Level
= in always_combSequential combinational evaluation: next line sees new valueCombinational gates — correct✅ MatchNone — this is correct
<= in always_ffAll RHS captured pre-clock, LHS updated in NBAD flip-flops — correct✅ MatchNone — this is correct
= in always_ffSequential: next statement sees new value (race-prone)Most tools infer D flip-flop (correct hardware)❌ Mismatch riskHIGH — sim/synth mismatch possible
<= in always_combIntermediate value NOT updated until next delta — stale readsTool warning; comb logic with delayed update semantics❌ MismatchHIGH — always wrong, tool warns
Mixed = and <= in same always_ffNon-deterministic — IEEE says this is undefined behaviorTool-dependent — may or may not warn❌ Non-deterministicCRITICAL — different simulators give different answers

⚠ Common Industry Mistake: "It Works in Simulation, Must Be Fine"

The most dangerous class of blocking/non-blocking bugs is the one where simulation appears correct. This happens because the simulator happens to process the always_ff blocks in a favorable order — the order that produces the expected result. Change simulator, change version, add more logic, and the ordering changes — now different results. Silicon always infers flip-flops (captures pre-edge values) regardless of whether you used = or <=. The only guaranteed-correct code is: always use <= in always_ff, no exceptions.

🔬 Testbench Usage — Which Assignment to Use Where

The blocking vs non-blocking choice in testbenches is just as important as in RTL — but the rules are different. Incorrect assignment types in testbenches produce checks that fire with stale values or miss functional bugs entirely.

SystemVerilog — Testbench: When to Use = vs <= and Why
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Rule 1: Use = in initial blocks for immediate signal driving ──
initial begin
    rst_n = 0;   // ✅ drives rst_n=0 IMMEDIATELY at T=0
    data  = 8'hAA;
    @(posedge clk);
    rst_n = 1;   // ✅ drives rst_n=1 immediately after posedge
end
 
// ── Wrong: <= in initial block — signal changes in NBA region ────
initial begin
    data <= 8'hAA;   // ❌ NBA: data doesn't change until NBA region
    @(posedge clk);   // @(posedge clk) might fire BEFORE data updates!
    check(data);       // might check old data value
end
 
 
// ── Rule 2: Use $strobe to print flip-flop outputs ───────────────
initial begin
    forever begin
        @(posedge clk);
        $display("q=%h", q);   // ❌ Active region — q still old value!
        $strobe( "q=%h", q);   // ✅ Postponed region — q has new value
    end
end
 
 
// ── Rule 3: For clocking-block driven signals — use <= via cb ────
clocking drv_cb @(posedge clk);
    output #1 valid, data;   // output skew: drives 1ns after posedge
endclocking
 
initial begin
    @(drv_cb);
    drv_cb.data <= 8'hBB;   // ✅ clocking block assignment: use <=
    drv_cb.valid <= 1'b1;    // drives 1ns after next posedge
end
 
 
// ── Rule 4: Checking outputs — sample in Reactive region ─────────
clocking mon_cb @(posedge clk);
    input #-1 data_out, valid_out;  // input skew: samples 1ns before posedge
endclocking
// Clocking block input samples are BEFORE the clock edge — setup time
// This is why UVM monitors use clocking blocks for accurate timing

💡 Senior Verification Engineer Tip: Use Clocking Blocks for All DUT Interface Interactions

The cleanest way to avoid blocking/non-blocking confusion in testbenches is to use clocking blocks for all DUT interface interactions. Clocking blocks automatically handle the timing: <= for driven outputs (via output skew), and input sampling before the clock edge (via input skew). The driver writes cb.signal <= value and the monitor reads cb.signal — the tool handles the scheduling. This is why UVM's clocking_block-based drivers never have blocking/non-blocking issues.

📋 RTL Coding Style Guide — The Complete Rule Set

These are not preferences — they are the rules enforced by RTL sign-off lint checks at every major semiconductor company. Breaking any of these rules is a blocker that prevents RTL from proceeding to synthesis.

ContextUseNever UseReasonLint Rule
always_comb= (blocking)<= (NBA)Combinational logic needs immediate value propagationSTARC-2.1.4.1
always_ff<= (NBA)= (blocking)Registers model simultaneous capture; blocking is race-proneSTARC-2.1.4.2
always_latch= (blocking)<= (NBA)Latch is combinational-style: immediate update when transparentStyle guide
Functions= (blocking)<= (NBA)Functions cannot have NBA semantics — blocking onlyLanguage rule
Tasks (synthesis)= for local; output via calling blockMixed freelySame rules as the calling block apply to synthesizable tasksTool-specific
Testbench initial= for immediate; <= via clocking block<= for direct signal drivingDirect NBA in initial block delays signal until NBA regionMethodology

⚡ Advanced Race Condition Analysis

SystemVerilog — Three Flavors of Race Condition
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ── Race Type 1: Two always_ff blocks writing same signal with = ──
always_ff @(posedge clk) shared = a;   // block 1
always_ff @(posedge clk) result = shared; // block 2 — sees new or old shared?
// VCS runs block 1 first: result = a (looks correct)
// Questa runs block 2 first: result = shared_old (different answer!)
// Synthesis: two flip-flops — result always gets shared_old (correct HW)
// ✅ Fix: use <= in both blocks
 
// ── Race Type 2: Read-after-Write in same always_ff block with = ─
always_ff @(posedge clk) begin
    stage1 = d;       // writes stage1 = d (new value)
    stage2 = stage1;  // reads NEW stage1 → stage2 = d (wrong — should be old)
end
// This is NOT a race between processes — it's sequential in one block.
// It's deterministic but wrong: both get d in same cycle.
// ✅ Fix: use <= for both
 
// ── Race Type 3: = and <= mixed in same block (non-deterministic) ─
always_ff @(posedge clk) begin
    tmp    =  a + b;    // blocking: tmp = a+b immediately (Active)
    result <= tmp;       // NBA: which tmp? pre-blocking (Active) or post?
end
// IEEE says: the value of tmp seen by <= is IMPLEMENTATION-DEFINED.
// VCS may give tmp_new, Questa may give tmp_old. Both are "correct" per spec.
// ✅ Fix: use automatic local variable for intermediate, then <=
always_ff @(posedge clk) begin
    automatic logic [8:0] tmp = a + b;  // local var: no net/reg — no NBA
    result <= tmp[7:0];                   // ✅ result gets tmp from this cycle
end
 
// ── Verification: detect blocking races with assertions ───────────
always_ff @(posedge clk) begin
    // XMR check: assert that result matches EXPECTED pre-clock value
    assert (result === $past(shared)) else
        $error("result=%h expected=%h — blocking race!", result, $past(shared));
end

🔬 Debugging Academy — 8 Blocking/Non-Blocking Bugs from the Field

Bug 1 — Blocking in always_ff: a 2-stage pipeline collapses to 1 stage

1

A two-stage pipeline reports one cycle of latency instead of two, and only on one simulator

NBA-PIPELINE-COLLAPSE
Observed Symptom

A datapath specified for two cycles of latency measures one in RTL simulation. Downstream logic that compensates for the missing cycle is added, the block passes its tests, and gate-level simulation later shows the compensation is now off by one in the other direction.

Expected vs Actual
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// As written
always_ff @(posedge clk) begin
  stage1 = d;          // blocking
  stage2 = stage1;     // reads the value assigned on the line above
end

Expected: stage2 holds what stage1 held before the edge — one cycle behind it. Actual: stage1 is updated to d first, so stage2 receives this cycle's d. Both registers end the edge holding the same value, and the two waveform traces are identical (Figure 3).

Diagnostic

Three checks, in the order that costs least:

  1. Grep before you probe. = inside always_ff on a signal read elsewhere is the bug, and finding it is a text search rather than a waveform session.
  2. Compare the two traces. Two registered signals that should differ by one cycle but change together on every edge is the collapse signature, and it is visible at a glance.
  3. Run the second simulator. Blocking races give tool-dependent results; correct non-blocking code does not. A disagreement between tools is conclusive, and an agreement proves nothing — see the §"Proving It" experiment.

The detail that identifies this bug rather than a generic latency error: the measured latency is short by exactly the number of collapsed stages, and it is short in RTL while the netlist has the full pipeline.

Root Cause

Blocking assignment executes in the Active region and takes effect immediately, so the second statement reads a value that, in hardware, does not exist until after the edge. The RTL models a wire where the design has a register.

The compensation logic added downstream is the expensive part. It corrects a simulation artefact, so once the assignment is fixed the pipeline lengthens by one and the compensation becomes an error of its own — which is why the mismatch surfaced at gate level rather than at the original fix.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
always_ff @(posedge clk) begin
  stage1 <= d;
  stage2 <= stage1;    // samples the PRE-EDGE stage1
end

Both right-hand sides are sampled before either update is applied, so stage2 receives the value stage1 held entering the edge. Then remove the downstream compensation — it was built against the broken behaviour, and leaving it converts a one-cycle shortfall into a one-cycle excess.

Prevention

Make = inside always_ff a lint rule, with a documented exception for block-local temporaries that are never read outside the block. That single rule removes this bug and the cross-block races in the same sweep, and it is mechanically checkable in a way that review is not.

Then verify latency rather than assuming it: a self-checking test that drives a marker through the pipeline and asserts it emerges on the expected cycle catches a collapse the moment it is introduced, instead of after downstream logic has been built around it.

The original buggy code and waveform trace follow.

Category: pipeline collapse. Buggy code:

Bug 1 — Blocking = Collapses Pipeline Register Stages
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: intended 2-stage pipeline with 2-cycle latency
always_ff @(posedge clk) begin
    stage1 = data_in;   // stage1 immediately = data_in (new)
    stage2 = stage1;    // stage2 immediately = data_in (new stage1!)
end
// Result: stage2 = data_in with 1-cycle latency, NOT 2-cycle
// Simulation: both stages have identical value every cycle
// Synthesis: correctly infers 2 FFs, data arrives at stage2 1 cycle late
// Impact: filter/DSP algorithm sees wrong delayed data → incorrect output
 
// ✅ FIX: use non-blocking — both stages capture pre-edge values
always_ff @(posedge clk) begin
    stage1 <= data_in;   // RHS: data_in_old (pre-edge)
    stage2 <= stage1;    // RHS: stage1_old (pre-edge) → 2-cycle latency ✅
end

Root cause / waveform / fix. Waveform symptom: stage1 and stage2 show identical waveforms — they change simultaneously on every posedge. The expected 1-cycle offset between them is missing. This is the definitive visual signature of a blocking-in-pipeline bug.Root CauseBlocking assignment (=) updates stage1 immediately. The next line then reads the NEW stage1, not the pre-clock-edge value. Both assignments complete within the same Active region, eliminating the intended 1-cycle delay between stages.Real ImpactThis bug caused a reported issue in a DSP filter implementation where the FIR tap coefficients were applied at the wrong time step, producing a completely incorrect frequency response in silicon. The RTL simulation passed (because the blocking collapse still produced output, just wrong output), but the gate-level simulation revealed the timing discrepancy.2Non-Blocking in always_comb — Intermediate Value Is StaleStale Comb ValueBuggy Code

Bug 2 — NBA <= in always_comb Reads Stale Intermediate Value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: NBA in always_comb — mid is not updated when 'out' is assigned
always_comb begin
    mid <= a ^ b;     // schedules mid update for NBA region
    out <= mid | c;   // ❌ uses OLD mid (before this always_comb ran)
                      // out = (mid_old | c) — wrong!
end
// Simulation: out is 1 delta cycle behind its correct value
// The always_comb re-triggers (NBA updates mid) → out eventually correct
// But in between there's a glitch where out has wrong value
// Tool warning: "non-blocking assignment in always_comb" — treat as error
 
// ✅ FIX: use blocking = for all intermediate values in always_comb
always_comb begin
    mid = a ^ b;     // mid updated immediately
    out = mid | c;   // uses NEW mid — correct single-cycle behavior
end

Bug 3 — Mixing = and <= in one always_ff: non-deterministic result

Category: non-determinism. Buggy code:

Bug 3 — Mixed = and <= in always_ff: IEEE-Undefined Behavior
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: mixing = and <= in same always_ff
always_ff @(posedge clk) begin
    tmp    =  a + b;     // blocking: tmp updates immediately (Active region)
    result <= tmp;        // NBA: which tmp? IEEE says IMPLEMENTATION-DEFINED
    status <= (tmp[8]);   // IEEE says IMPLEMENTATION-DEFINED
end
// VCS interpretation:  result = new tmp (a+b from this cycle)
// Questa interpretation: result = old tmp (0 on first cycle)
// Both are "correct" per IEEE 1800. Your code is ambiguous.
// Will cause regressions to differ between tool vendors.
 
// ✅ FIX: Use local automatic variable for intermediate, then <=
always_ff @(posedge clk) begin
    automatic logic [8:0] tmp_local = a + b;  // local — no NBA semantics
    result <= tmp_local[7:0];                   // ✅ deterministic
    status <= tmp_local[8];                    // ✅ deterministic
end

Bug 4 — Blocking shift register acts as a wire: both flops hold the same value

Category: race / wire bug. Buggy code:

Bug 4 — Two always_ff with Blocking = on Same Signal
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: shift register split across two always_ff blocks
always_ff @(posedge clk) a = d_in;  // block 1: a = d_in (blocking)
always_ff @(posedge clk) b = a;    // block 2: b = a (but which a?)
 
// Scenario A: Simulator runs block 1 first
// a gets d_in (new). Then block 2: b = a = d_in (new). Result: b = d_in
// This makes b act like a wire to d_in, not a 1-cycle delayed register.
 
// Scenario B: Simulator runs block 2 first
// b = a (old). Then block 1: a = d_in. Result: b = a_old (correct FF behavior)
 
// Synthesis: always scenario B (b = a_old — two FFs in series)
// Simulation: depends on simulator. This is the classic sim/synth mismatch.
 
// ✅ FIX: use non-blocking — deterministic, matches synthesis
always_ff @(posedge clk) a <= d_in;  // RHS: d_in_old captured
always_ff @(posedge clk) b <= a;    // RHS: a_old captured. NBA updates: a=d_in, b=a_old

Real impact — the classic "passes in VCS, fails in Questa" bug. This bug passes regression on the primary simulator (whichever happened to run block 2 first) but is immediately caught when the second simulator (which runs block 1 first) is added to the regression. This is the exact signature the two-simulator test is designed to catch. Running two simulators in CI is the definitive detector for this class of bug.5Mixed Reset (=) and Data (<=) in Same always_ff — Tool WarningInconsistent BlockBuggy Code

Bug 5 — Blocking = for Reset, NBA <= for Data: Inconsistent Block
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: = for reset path, <= for data path — inconsistent
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) q =  '0;   // ❌ blocking reset
    else        q <= d;    // NBA data
end
// Lint: "Mixed blocking and non-blocking assignments in always_ff"
// IEEE: behavior when = and <= are mixed is implementation-defined
// VCS may simulate correctly; Questa may show glitches during reset
 
// ✅ FIX: use <= consistently for BOTH reset and data paths
always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) q <= '0;   // ✅ consistent NBA
    else        q <= d;    // ✅ consistent NBA
end
// Rule: every assignment in an always_ff block must use <=. Always.

6$display Shows Stale FF Value — Wrong Debug PrintDebug Print BugBuggy Code

Bug 6 — $display Fires Before NBA, Shows Wrong Flip-Flop Value
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: $display fires in Active region — before NBA updates FF output
always_ff @(posedge clk) q <= d;
 
initial begin
    d = 8'hAA; #10;  // d=0xAA, posedge at T=5
    @(posedge clk);
    $display("q = %h", q);  // ❌ prints 0x00 (old value!) — Active region
    $strobe( "q = %h", q);  // ✅ prints 0xAA (new value)  — Postponed region
end
 
// ── Common engineer mistake that leads to wrong conclusions ───────
// Engineer sees $display showing 0x00 instead of 0xAA
// Thinks: "FF not working, d not being captured"
// Spends hours looking for a sensitivity list bug
// Reality: FF works fine, $display just fires before NBA
 
// ✅ RULE: Use $strobe for all debug prints involving FF outputs
// Use $display only for combinational signals driven with =

7NBA in Testbench Initial Block — Signal Not Ready When CheckedTestbench TimingBuggy Code

Bug 7 — NBA <= in Initial Block Delays Signal Until After Event
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: <= in initial block — data not ready when @(posedge clk) fires
initial begin
    data  <= 8'hAA;   // NBA: scheduled for NBA region
    valid <= 1'b1;    // NBA: scheduled for NBA region
    @(posedge clk);   // fires in Active region
                      // ← But NBAs haven't committed yet!
    // DUT samples data and valid BEFORE they update
    // DUT sees old values: data=0x00, valid=0
    // Transaction appears to not be driven
end
 
// ✅ FIX: use = in initial blocks for immediate signal driving
initial begin
    data  = 8'hAA;   // ✅ immediate — data=0xAA right now
    valid = 1'b1;    // ✅ immediate
    @(posedge clk);   // DUT samples correct values
end

Bug 8 — Swap fails with blocking: the classic temp-variable requirement

Category: swap bug. Buggy code:

Bug 8 — Blocking Swap Destroys One Value — Non-Blocking Doesn't
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ❌ BUG: swap with blocking — classic value-destruction bug
always_ff @(posedge clk) begin
    if (swap_en) begin
        a = b;   // a = b (new). Old a is LOST forever.
        b = a;   // b = a = b (already overwritten!)
                  // Both a and b now hold b_old. Swap failed.
    end
end
 
// ✅ FIX 1: non-blocking — RHS captured before any LHS updates
always_ff @(posedge clk) begin
    if (swap_en) begin
        a <= b;   // RHS: b_old captured
        b <= a;   // RHS: a_old captured
                  // NBA: a ← b_old, b ← a_old simultaneously. Swap works. ✅
    end
end
 
// ✅ FIX 2: if you must use blocking, need temp variable
always_ff @(posedge clk) begin
    if (swap_en) begin
        automatic logic [7:0] tmp = a;   // save a before overwriting
        a = b;
        b = tmp;   // restore old a from tmp. But still risky — use <=
    end
end
 
// KEY INSIGHT: This is THE canonical example of why <= exists.
// Hardware registers naturally swap — each FF captures its D input
// at the clock edge simultaneously. <= models this perfectly.

💡 Senior Verification Engineer Tip: Enforce with Lint, Not Code Review

Blocking/non-blocking violations are too subtle and too consequential to rely on code review to catch. Configure your lint tool (Synopsys Spyglass, Cadence JasperGold, Aldec ALINT-PRO) to enforce: (1) STARC-2.1.4.1: no NBA in always_comb, (2) STARC-2.1.4.2: no blocking in always_ff, (3) W_MIXED_BA_NBAS: no mixing in same block. Set all three to ERROR severity, not WARNING. These three rules eliminate an entire class of RTL bugs automatically, without requiring engineers to remember the rule on every code write.

Interview Q&A — Blocking vs Non-Blocking

= updates its target immediately, so the next statement in the same block reads the new value. <= samples its right-hand side immediately but defers the update, so every statement in the block reads the values that existed before the edge.

The consequence that matters is not about one block but about all of them. Because <= separates reading from writing, every always_ff in the design reads pre-edge values and writes post-edge values, so the order the simulator happens to evaluate those blocks in cannot affect the result. That is exactly what real flip-flops do, and it is why <= is the rule rather than a preference.

Where This Is Specified

The two assignment forms are defined in IEEE Std 1800 (SystemVerilog), clause 10 — Assignment statements — blocking in §10.4.1 and non-blocking in §10.4.2. The region model that makes <= deterministic is clause 4, Scheduling semantics, which defines the Active and NBA regions and, critically, states that the simulator may process events within a region in any order. The IEEE Standards Association listing is the primary source.

That last point is the one worth reading in the standard rather than taking on trust. The non-determinism this page describes is not a tool defect or a quality-of-implementation gap — it is explicitly permitted behaviour, which is why "it works on my simulator" carries no weight and why the proof above refuses to assert a result for the blocking versions.

Related lessons. The blocks these assignments live in are procedural blocks; the scheduling regions in depth are IPC and the scheduling regions. The Verilog-track treatment of the same operators is blocking and non-blocking assignments, and the multi-process consequences — including why non-blocking does not fix a write-write race — are in race conditions and determinism. For where the RTL/netlist mismatch surfaces, see RTL vs netlist behaviour.

Part of SystemVerilog Fundamentals·Procedural Statements·Lesson 34 of 53

View program

Continue learning