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?
// 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 type | Shape | Why it races |
|---|---|---|
| Read–write | one block writes a signal, another reads it, same edge | reader may see pre- or post-write value (undefined order) |
| Write–write | two blocks write the same variable, same edge | which write lands last is undefined (multiple driver on a reg) |
| Blocking-in-sequential | sequential logic uses = across blocks | immediate 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.
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.
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 flow6. 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.
- 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. - 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. - 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. - One driver per signal. Each
regis assigned from exactly one procedural block; combine competing logic into a single block with explicit priority. Kills write–write races. - 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.
- 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 flow8. 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 — 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
endmoduleRepresentative output.
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
- Assuming "passes on my simulator" means correct. A race can pass on one tool and fail on another; only the discipline guarantees portability (§6).
- Using
#0to fix a race. It relocates the race, not removes it (§8, 19.1). - Driving one register from two blocks. A write–write race; non-blocking does not fix it — use one driver with priority (§5, DebugLab).
- Mixing
=and<=, or using the wrong one for the logic type. Sequential needs<=(NBA), combinational needs=(Active) (§7, 19.2). - Sampling DUT outputs in a same-edge Active block. Read settled values in Postponed (§7, 19.3).
11. Debugging Lab
The design that broke when the simulator changed
WRITE-WRITE-RACE// 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
endmoduleFor 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.
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.
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.
// 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
endmoduleNow 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. #0relocates 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.
Related Tutorials
- The Postponed Region — $strobe & $monitor — Chapter 19.3; race-free sampling of settled values.
- The NBA Region — Why Non-Blocking Updates Last — Chapter 19.2; the deferral that makes sequential logic deterministic.
- The Active Region & Evaluation Order — Chapter 19.1; where undefined order — and races — are born.
- Blocking and Non-Blocking Assignments — Chapter 14.3; the rule this chapter reveals as a determinism rule.