A race needs two processes in the same scheduling region touching one signal. The usual pair is a testbench drive or sample with blocking semantics and an RTL block triggered by the same clock edge.
The LRM leaves same-region process order to the simulator. That unspecified order is the whole race.
// RTL: a registered pipeline stage
module pipe8 (
input logic clk,
input logic [7:0] data_in,
output logic [7:0] data_q
);
always_ff @(posedge clk) data_q <= data_in;
endmoduleThe RTL above is internally race-free. All of its reads happen before any of its updates.
Side | Blocking semantic | Nonblocking semantic |
|---|---|---|
Testbench drive at the edge | Updates the signal mid-region; RTL may read old or new | Lands after RTL reads; RTL always sees the previous value |
Testbench sample at the edge | May run before or after the drive | N/A: sampling discipline comes from clocking blocks |
RTL update at the edge | N/A: RTL uses nonblocking updates | Reads settle first, left-hand sides update in the NBA region |
SAME-EDGE DRIVE/SAMPLE RACE - time step at posedge clk
ACTIVE REGION (process order is SIMULATOR'S CHOICE)
+--------------------------------------------------------+
| runnable: { always_ff (RTL), driver, monitor } |
| |
| ORDER A (driver first): |
| 1. driver: data_in = 8'hA5 (blocking, now) |
| 2. always_ff: reads data_in = A5, schedules NBA |
| 3. monitor: captured = A5 |
| |
| ORDER B (RTL first): |
| 1. always_ff: reads data_in = OLD, schedules NBA |
| 2. driver: data_in = 8'hA5 |
| 3. monitor: captured = A5 (or OLD if the monitor |
| ran before the driver)|
+--------------------------------------------------------+
|
v
NBA REGION
+--------------------------------------------------------+
| data_q <= (whatever always_ff READ above) |
| ORDER A: data_q becomes A5 - DUT saw the new value |
| ORDER B: data_q becomes OLD - DUT saw the old value |
+--------------------------------------------------------+
Same source code. Two legal outcomes. THAT is the race.Both orders are legal per the LRM. Declaration order, flags, or tool version can flip the choice without touching the source.
The bottom row, data_q, holds the previous slot's data_in. This is the NBA-driven behavior both orders must converge to once the race is removed.
Slot 0: data_in is a5, so data_q still holds 00 (the pre-edge value).
Slot 1: data_in is 3c, so data_q takes a5.
Slot 2: data_in holds 3c, so data_q takes 3c.
Slot 3: data_in is f0, so data_q still holds 3c.
Slot 4: data_in is 07, so data_q takes f0.
Slot 5: data_in holds 07, so data_q takes 07.
A race announces itself with a fixed signature. A value is off by exactly one cycle, intermittently or per tool, and behavior moves with the simulator, its flags, or unrelated edits.
A testbench blocking assignment or immediate read sits on the same edge the RTL flops use.
A #0 or #1 patch sits in the history near the driver.
The failure follows the tool or the optimization level, never the stimulus.
Two processes in the same region of the same time step touch one signal, and the LRM leaves their order to the simulator. Each legal order gives a different result.
A pass proves the simulator picked the friendly order today. Any unrelated edit or flag can flip the order tomorrow.
Two flops stage the two LRM-legal outcomes deterministically. An early blocking drive settles before the edge and is captured, while a drive landing just after the edge is missed until the next one. A drive sitting exactly on the edge is a coin flip between these two outcomes, and that flip is the race.
timeunit 1ns; timeprecision 1ps;
module flop8 (
input logic clk,
input logic [7:0] d,
output logic [7:0] q
);
always_ff @(posedge clk) q <= d;
endmodule