Skip to content

Verilog · Chapter 19.4 · Timing Regions

Race Conditions & Simulation Determinism in Verilog — The Coding Discipline

Why does code that looks correct pass on one simulator and fail on another, or pass for a year and then fail after a tool upgrade? The answer is a race condition, a result that depends on the order of events the Verilog standard deliberately leaves undefined. Because two simulators may resolve that order differently, a racy design is a latent bug any tool change can expose. The good news is that the same event regions that cause races also tell you how to avoid them. A small coding discipline of non-blocking for sequential logic, blocking for combinational logic, one driver per signal, and settled sampling guarantees the result means the same thing everywhere. This page classifies the race types, explains why determinism matters, and lays out the discipline behind each rule.

Advanced14 min readVerilogRace ConditionsDeterminismNon-BlockingEvent Regions

Chapter 19 · Section 19.4 · Timing Regions

1. The Engineering Problem

A design passes every regression for a year. Then the team adopts a second simulator for sign-off — and the same code, unchanged, produces different results. No bug was introduced; no line changed. How can correct-looking code mean two different things?

passes-here-fails-there.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   // Both blocks drive the SAME register on the SAME edge:
   always @(posedge clk) if (start) busy = 1'b1;   // block A
   always @(posedge clk) if (done)  busy = 1'b0;   // block B
 
   // On a cycle where both 'start' and 'done' are active, which write wins?
   //   - Simulator X happens to run block B last → busy = 0
   //   - Simulator Y happens to run block A last → busy = 1
   // The standard does NOT define the order. The result is whatever the tool
   // chose — and two tools chose differently. A RACE.

This is a race condition: the result depends on an event order the language leaves unspecified. Neither simulator is wrong — the code is, because it asks a question (which block ran first?) the standard refuses to answer. Everything in Chapter 19 has led here: the event regions are why races exist, and they are also how you write code that cannot race.

A race is a result that depends on the undefined order of same-region events. Because different simulators may resolve that order differently, a racy design is a latent bug exposed by any tool or version change. The event regions both cause races and prescribe the discipline that eliminates them.

2. Mental Model — A Race Asks an Unanswerable Question

3. What Makes a Race — Three Flavours

Every Verilog race is the same root cause — undefined same-region order — in one of three shapes:

Race typeShapeWhy it races
Read–writeone block writes a signal, another reads it, same edgereader may see pre- or post-write value (undefined order)
Write–writetwo blocks write the same variable, same edgewhich write lands last is undefined (multiple driver on a reg)
Blocking-in-sequentialsequential logic uses = across blocksimmediate Active updates make later blocks see new values, order-dependently

All three vanish under the same discipline (§7), because all three are the Active region's undefined order leaking into the result.

4. Read–Write Races

The producer/consumer race: one block produces a value, another consumes it, both on the same edge.

read-write-race.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   always @(posedge clk) a = b;     // producer (blocking)
   always @(posedge clk) c = a;     // consumer — old or new a?
   //   block 1 first → c gets NEW a;  block 2 first → c gets OLD a.  RACE.

With blocking assignments, a's update is immediate in Active, so whether the consumer sees the old or new a depends on which block the simulator runs first. The fix is non-blocking: every update defers to NBA, so the consumer's read in Active always sees the old a, consistently, on every simulator. The assignment-operator rule this rests on is developed in blocking vs non-blocking assignments; this page is about the multi-process consequences of getting it wrong.

5. Write–Write Races

The multiple-driver race: two blocks assign the same variable, so the final value depends on which write the simulator applies last.

write-write-race.v
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   always @(posedge clk) if (start) busy = 1'b1;   // driver A
   always @(posedge clk) if (done)  busy = 1'b0;   // driver B
   // When both conditions are true in a cycle, the last write wins — but the
   // order of the two blocks is undefined. busy is 1 on one simulator, 0 on
   // another. (Even with <=, two NBA writes to one var from two blocks is a
   // multiple-driver race.)

A register written from two procedural blocks has two drivers, and the standard does not define which update prevails. This does not even need the conditions to overlap often — a single coincident cycle produces a divergence. Non-blocking does not fix this one: the cure is one driver per signal — consolidate the logic into a single block with explicit priority (§7, §10).

Visual A — the three race shapes, one root cause

Three race types, all from undefined same-region order

data flow
Three race types, all from undefined same-region orderread–writeconsumer sees old ornewwrite–writewhich driver wins?blocking-in-sequentiallater block sees newvalueundefined Active-region orderundefinedActive-region…the single root cause
Read–write, write–write, and blocking-in-sequential races are three shapes of the same root cause: a result that depends on the undefined order of events within the Active region. The determinism discipline removes that dependence for all three.

6. Why Determinism Matters

A race is not a cosmetic concern — it is a correctness and schedule risk:

  • Portability across simulators. Real projects use several simulators — one for fast regressions, another for sign-off, a third bundled with a piece of IP. A racy design can pass on one and fail on another, with no code difference.
  • Reproducibility. A race can make a failure appear and disappear across runs, tool versions, or even optimisation settings — the hardest kind of bug to pin down.
  • It masquerades as a tool bug. Because the symptom is "works on X, breaks on Y," teams waste days suspecting the simulator. The defect is in the code's reliance on undefined order.
  • It is a latent landmine. A race can lie dormant for years and detonate on a routine tool upgrade. Determinism is insurance you buy up front.

Determinism means the simulation means the same thing everywhere — which is the whole point of a model.

7. The Determinism Discipline

Each rule below removes a dependence on undefined order, and each is justified by a region.

  1. Non-blocking <= for sequential logic. Updates defer to NBA, so all blocks read the old values in Active and update together — order-independent (19.2). Kills read–write and blocking-in-sequential races.
  2. Blocking = for combinational logic. Updates apply now in Active, so dependents read the new value in the same evaluation with no delta delay (19.2). Avoids the combinational stale-read bug.
  3. Never mix = and <= in one block. Mixing blends Active and NBA semantics on related signals and produces results that are hard to reason about and easy to race.
  4. One driver per signal. Each reg is assigned from exactly one procedural block; combine competing logic into a single block with explicit priority. Kills write–write races.
  5. Don't read-and-write a shared signal across blocks in the same region. Separate producer and consumer, or use non-blocking so the consumer reads the old value consistently.
  6. Sample settled values in the Postponed region. Checkers and loggers read post-update values via $strobe/$monitor (19.3), not in a same-edge Active block and not via #0.

Visual B — the discipline maps to the regions

The determinism rules, each rooted in a region

data flow
The determinism rules, each rooted in a regionsequential → <=(NBA)reads before writes →order-independentcombinational → =(Active)update now → no deltadelayone driver persignalno write–write racesample inPostponedsettled, race-freereads
Each discipline rule uses the regions deliberately: non-blocking defers sequential updates to NBA (reads-before-writes determinism); blocking keeps combinational updates in Active (no delta delay); one driver per signal removes write–write races; and settled sampling reads in Postponed. Follow them and undefined order stops affecting the result.

8. The #0 Trap, One Last Time

When a race appears, the worst response is to force an order with #0. As 19.1 showed, #0 only defers an event to the Inactive region — it runs before NBA (so it misses non-blocking updates) and two #0 events are themselves unordered. So #0 does not remove a race; it relocates it, often making the design pass on the simulator you tested and fail on the next. The legitimate fixes are the discipline in §7 — they make the result independent of order, rather than betting on a particular order. Treat a #0 added to "fix" a race as a red flag that the real race is still there.

9. Runnable Proof — Watch the Race, Then Watch It Go Away

Everything above is claim; this section is evidence. The module below builds the same status flag four ways, drives all four with identical stimulus, and reports what each one produced on the cycle where start and done coincide.

race_demo.v — four implementations, identical stimulus (compiles standalone)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─────────────────────────────────────────────────────────────────────────
//  race_demo.v — what a write-write race actually does, and what fixes it.
//
//  iverilog -o race_demo race_demo.v && ./race_demo
//  vcs race_demo.v && ./simv        |        xrun race_demo.v
// ─────────────────────────────────────────────────────────────────────────
`timescale 1ns/1ps
 
// (A) RACY — two drivers, blocking. Driver A written first.
module ctrl_racy_a (input clk, input start, input done, output reg busy);
    always @(posedge clk) if (start) busy = 1'b1;   // driver A
    always @(posedge clk) if (done)  busy = 1'b0;   // driver B
endmodule
 
// (B) THE SAME LOGIC — the two blocks written in the opposite order.
//     Swapping two independent always blocks is a semantically NEUTRAL edit:
//     concurrent blocks have no source-order semantics. So if (A) and (B)
//     ever disagree, the answer was never in the logic at all.
module ctrl_racy_b (input clk, input start, input done, output reg busy);
    always @(posedge clk) if (done)  busy = 1'b0;   // driver B
    always @(posedge clk) if (start) busy = 1'b1;   // driver A
endmodule
 
// (C) STILL RACY — two drivers, non-blocking. Non-blocking moves both writes
//     into the NBA region; it does not invent a priority between them. The
//     relative order of two NBA updates to one variable is still unspecified.
module ctrl_racy_nb (input clk, input start, input done, output reg busy);
    always @(posedge clk) if (start) busy <= 1'b1;
    always @(posedge clk) if (done)  busy <= 1'b0;
endmodule
 
// (D) DETERMINISTIC — one driver, explicit priority, non-blocking.
module ctrl_fixed (input clk, input start, input done, output reg busy);
    always @(posedge clk) begin
        if (done)       busy <= 1'b0;   // clear wins — a STATED decision
        else if (start) busy <= 1'b1;
        // else: hold
    end
endmodule
 
module race_demo;
 
    reg  clk, start, done;
    wire busy_a, busy_b, busy_nb, busy_fx;
    integer errors;
 
    always #5 clk = ~clk;
 
    ctrl_racy_a  u_a (.clk(clk), .start(start), .done(done), .busy(busy_a));
    ctrl_racy_b  u_b (.clk(clk), .start(start), .done(done), .busy(busy_b));
    ctrl_racy_nb u_n (.clk(clk), .start(start), .done(done), .busy(busy_nb));
    ctrl_fixed   u_f (.clk(clk), .start(start), .done(done), .busy(busy_fx));
 
    // Drive on the NEGEDGE and sample on the NEXT negedge: the stimulus is
    // settled well before the active edge, and every value read below is a
    // settled post-update value, never a mid-edge one (§7, rule 6).
    task drive_and_settle(input s, input d);
        begin
            @(negedge clk);  start = s;  done = d;
            @(negedge clk);                       // one full cycle later
        end
    endtask
 
    task check_fixed(input exp);
        begin
            if (busy_fx !== exp) begin
                errors = errors + 1;
                $display("   ** FAIL: busy_fx=%b, spec says %b", busy_fx, exp);
            end
        end
    endtask
 
    initial begin
        clk = 1'b0;  start = 1'b0;  done = 1'b0;  errors = 0;
 
        $display("  start done |  A    B    <=   fixed");
        $display("  -----------+----------------------");
 
        drive_and_settle(1'b1, 1'b0);   // set only    — no conflict
        $display("     1    0  |  %b    %b    %b     %b", busy_a, busy_b, busy_nb, busy_fx);
        check_fixed(1'b1);
 
        drive_and_settle(1'b0, 1'b1);   // clear only  — no conflict
        $display("     0    1  |  %b    %b    %b     %b", busy_a, busy_b, busy_nb, busy_fx);
        check_fixed(1'b0);
 
        drive_and_settle(1'b1, 1'b0);   // re-arm
        check_fixed(1'b1);
 
        drive_and_settle(1'b1, 1'b1);   // ── THE COINCIDENT CYCLE ──
        $display("     1    1  |  %b    %b    %b     %b   <-- both drivers active",
                 busy_a, busy_b, busy_nb, busy_fx);
        check_fixed(1'b0);              // 'done' has priority — defined by the source
 
        drive_and_settle(1'b0, 1'b0);   // hold
        check_fixed(1'b0);
 
        // ---- the observation, stated honestly ----------------------------
        $display("");
        if (busy_a !== busy_b)
            $display("  ** (A) and (B) DISAGREE. Identical logic, opposite answers:");
        else
            $display("  ** (A) and (B) agreed on THIS tool. That is luck, not a guarantee:");
        $display("     the only difference between them is the order two concurrent");
        $display("     always blocks appear in the file, which has no meaning in Verilog.");
        $display("     Neither answer is wrong. The code is.");
        $display("  ** (C) shows non-blocking did NOT fix it: <= defers the writes,");
        $display("     it does not order them.");
 
        if (errors == 0)
            $display("\n  [PASS] ctrl_fixed matched its specified behaviour on every cycle.");
        else
            $display("\n  [FAIL] %0d check(s) failed", errors);
        $finish;
    end
 
endmodule

Representative output.

Simulation log — values for A, B and <= are tool-dependent by construction
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  start done |  A    B    <=   fixed
  -----------+----------------------
     1    0  |  1    1    1     1
     0    1  |  0    0    0     0
     1    1  |  0    1    0     0   <-- both drivers active
 
  ** (A) and (B) DISAGREE. Identical logic, opposite answers:
     the only difference between them is the order two concurrent
     always blocks appear in the file, which has no meaning in Verilog.
     Neither answer is wrong. The code is.
  ** (C) shows non-blocking did NOT fix it: <= defers the writes,
     it does not order them.
 
  [PASS] ctrl_fixed matched its specified behaviour on every cycle.

What to take from the log. The first two rows are unanimous — with only one driver active there is no conflict and nothing to resolve. The third row is the whole chapter: on the coincident cycle, two modules containing the same two always blocks report different values, and the only thing separating them is which block was typed first. That is what "the result depends on an undefined order" means concretely.

Two cautions about reproducing this. Your tool may print the same value in the A and B columns — many simulators do not simply follow source order, and some reorder under optimisation. That is not a refutation; it is the same finding. The design has no defined answer, so a tool agreeing with itself today tells you nothing about the next tool, the next version, or the next optimisation setting. And the <= column is the one to show a teammate who believes non-blocking is a blanket race cure: the writes moved to the NBA region and stayed exactly as unordered as they were.

The [PASS] line carries the positive half of the lesson. ctrl_fixed is checkable — done beats start, stated in the source, reproducible everywhere — and a checker that passes here passes on every conforming simulator. Determinism is not a property you observe; it is one you can assert.

10. Common Mistakes

  1. Assuming "passes on my simulator" means correct. A race can pass on one tool and fail on another; only the discipline guarantees portability (§6).
  2. Using #0 to fix a race. It relocates the race, not removes it (§8, 19.1).
  3. Driving one register from two blocks. A write–write race; non-blocking does not fix it — use one driver with priority (§5, DebugLab).
  4. Mixing = and <=, or using the wrong one for the logic type. Sequential needs <= (NBA), combinational needs = (Active) (§7, 19.2).
  5. Sampling DUT outputs in a same-edge Active block. Read settled values in Postponed (§7, 19.3).

11. Debugging Lab

1

The design that broke when the simulator changed

WRITE-WRITE-RACE
Buggy Code
Buggy Code — 'busy' has two drivers
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A 'busy' status flag, SET by a start event and CLEARED by a done event,
// each in its OWN always block — so 'busy' has TWO drivers:
module ctrl(input clk, input start, input done, output reg busy);
    always @(posedge clk) if (start) busy = 1'b1;   // driver A
    always @(posedge clk) if (done)  busy = 1'b0;   // driver B
endmodule

For a year, on Simulator X, this "works": whenever a cycle has both start and done, X happens to run driver B last, so busy clears — the intended behaviour. The team never noticed the latent race.

The project adopts Simulator Y for sign-off. Y resolves the undefined order the other way: on a both-active cycle it runs driver A last, so busy stays set. A unit that should have gone idle remains busy, a transaction stalls, and a test that passed for a year now fails.

The RTL did not change. busy is written from two blocks, and the Verilog standard does not define which write lands last — so the result is whatever each simulator chose, and the two simulators chose differently. A classic write–write (multiple-driver) race.

Symptom

A design that passed every regression on the original simulator fails after migrating to a second simulator for sign-off, with no source changes. A status flag ends up in the opposite state on a cycle where two events coincide. It looks like a simulator defect, and the team spends days suspecting the new tool.

Root Cause

busy is driven by two procedural blocks — one sets it, one clears it — on the same clock edge. When both start and done are active in the same cycle, both blocks assign busy, and the final value depends on which block the simulator applies last. The Verilog standard leaves the order of independent same-region events undefined, so each simulator is free to choose — and the two simulators chose opposite orders, producing opposite results.

This is a write–write (multiple-driver) race, and crucially, switching to non-blocking would not fix it: two non-blocking writes to one variable from two blocks is still a multiple-driver race in the NBA region. The real problem is that busy has two drivers and no defined priority between them.

Fix
Fix — one driver, explicit priority, non-blocking
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Give 'busy' ONE driver, with EXPLICIT priority, using non-blocking for the
// sequential update:
module ctrl(input clk, input start, input done, output reg busy);
    always @(posedge clk) begin
        if (done)       busy <= 1'b0;   // clear wins (explicit priority)
        else if (start) busy <= 1'b1;
        // else: hold
    end
endmodule

Now busy is assigned from a single block, the start/done conflict is resolved by a defined priority (clear over set, here), and the update is non-blocking. There is no undefined order to depend on: the result is identical on every simulator.

Lesson: a write–write race is fixed by one driver per signal with explicit priority — not by #0, and not merely by switching to non-blocking. A race is a latent bug that a tool or version change will eventually expose; determinism comes from the coding discipline, not from luck with a tool.

12. Interview Q&A

13. Exercises

Exercise 1 — Classify the race

For each, name the race type: (a) two blocks both assign flag on the same edge; (b) one block writes x, another reads x on the same edge; (c) a shift register written with blocking across separate blocks.

Exercise 2 — Fix it

always @(posedge clk) if (set) q = 1; always @(posedge clk) if (rst) q = 0; is racy. Rewrite it deterministically, stating the priority you chose.

Exercise 3 — Will non-blocking help?

For each race in Exercise 1, say whether switching to <= fixes it, and why or why not.

Exercise 4 — The tool migration

Explain to a teammate why a design that passed for a year can fail after a simulator change, using the words undefined order and region.

14. Summary

A race is a result that depends on the undefined order of same-region events — and the discipline rooted in the regions removes it:

  • Three race types — read–write (consumer sees old or new), write–write (which driver wins), blocking-in-sequential — all from the Active region's undefined order.
  • Why it matters — portability across simulators, reproducibility, and tool-upgrade safety; a race is a latent bug that masquerades as a tool defect.
  • The discipline<= for sequential (NBA, reads-before-writes), = for combinational (Active, no delta), never mix them, one driver per signal with priority, don't read+write a shared signal across blocks, and sample settled values in Postponed.
  • #0 relocates a race, never removes it — fix the dependence on order, don't bet on an order.

The crux to keep: determinism is something your coding discipline provides, not something the language guarantees — use the event regions deliberately and the undefined order stops mattering.

Where this is specified

The stratified event queue that makes all of this precise — the Active, Inactive, NBA and Postponed regions, and the explicit statement that the simulator may process events within a region in any order — is defined in IEEE Std 1364-2005 (Verilog), clause 11, Scheduling semantics (clause 5 in the earlier IEEE 1364-1995/2001 numbering, which is where most older references point). IEEE 1364 has since been superseded by IEEE Std 1800 (SystemVerilog), whose clause 4, Scheduling semantics, carries the same model forward with the added SystemVerilog regions; that is the clause to cite in current work. Both are listed by the IEEE Standards Association.

The sentence worth reading in the original rather than paraphrasing is the one that grants the simulator freedom of order within a region. Every race in this chapter is a program that depends on a choice the standard explicitly declines to make, and seeing the standard decline to make it is more convincing than being told it does.

Timing regions complete — and the timing curriculum

This closes Chapter 19 Timing Regions — the stratified event queue (overview), the Active region and evaluation order (19.1), the NBA region (19.2), the Postponed region (19.3), and races and determinism (19.4). You now understand the scheduling machinery beneath the blocking/non-blocking rule, the $display/$strobe difference, and every Verilog race.

It also completes the timing curriculum (Chapters 17–19): delays make signals late (17), timing checks judge whether late is too late (18), and timing regions decide the exact order in which every value is computed and observed (19) — the full account of how real hardware behaves in simulation, from the propagation of a single edge to the determinism of an entire design.