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.
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. 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).
10. Debugging Lab
The design that broke when the simulator changed
11. Interview Q&A
12. 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.
13. 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.
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.