Ordinary simulation resolves every setup-hold race deterministically: the flop takes the old or the new value according to simulator event order, always the same way for the same stimulus. Real silicon resolves late and unpredictably. A metastability-aware model replaces the deterministic outcome with a random old-or-new choice whenever data changes inside a window around the clock edge, so the testbench exercises both resolutions across many runs.
The model keeps a copy of data delayed by the modelled setup window (1 ns). If the live data differs from that copy at the clock edge, the change fell inside the window and the output resolves pseudo-randomly to the old or the new value; otherwise it captures exactly. The randomness comes from a fixed-seed LFSR that advances only on window hits, so the run is fully reproducible. The listing below is a compact runnable demonstration: exact capture outside the window, and a reproducible mix of old and new resolutions inside it:
`timescale 1ns/1ps
// Compact metastability-injection flop: window hits resolve to old or new
// from a fixed-seed LFSR, clean captures stay exact. The window is detected
// with a delayed copy of d (no $realtime bookkeeping), and the seed makes
// the run bit-identical on every supported engine. The full design-side
// model with hold-window handling is built in the RTL synchroniser lessons.
module inject_ff (
input logic clk, d, output logic q
);
wire d_old; // d as it was 1 ns ago (the setup window)
assign #1.0 d_old = d;
logic [15:0] lfsr = 16'hACE1; // fixed seed: reproducible resolutions
initial q = 1'b0;
always @(posedge clk) begin
if (d_old !== d) begin // changed inside the window: inject
lfsr <= {lfsr[14:0], lfsr[15] ^ lfsr[13] ^ lfsr[12] ^ lfsr[10]};
q <= lfsr[0] ? d : q; // take the new value or keep the old one
end
else q <= d; // clean capture: exact
end
endmodule
module tb_inject;
logic clk = 0, d = 0, q;
inject_ff dut (.clk, .d, .q);
always #5 clk = ~clk; // edges at 5, 15, 25, ...
int ones = 0, zeros = 0, safe_errors = 0;
initial begin
repeat (100) begin // clean changes: exact
@(negedge clk); d = ~d;
@(posedge clk); #1;
if (q !== d) safe_errors++;
end
repeat (400) begin // window hits: mixed
@(negedge clk); #4.5 d = ~d; // 0.5 ns before the edge
@(posedge clk); #1;
if (q === d) ones++; else zeros++;
end
$display("safe errors %0d, window: new %0d old %0d", safe_errors, ones, zeros);
if (safe_errors == 0 && ones > 50 && zeros > 50)
$display("PASS: exact outside the window, mixed old/new inside it");
else $display("FAIL");
$finish;
end
endmoduleThe run prints safe errors 0, window: new 267 old 133 on both supported engines: zero wrong values outside the window, and a genuine mix of 267 new-value and 133 old-value resolutions across the 400 window hits.
The run shows | It proves | It does not prove |
|---|---|---|
Correct behaviour across many random resolutions | The protocol tolerates either resolution of any single crossing | Anything about resolution time or failure rates |
A mutation (e.g. binary pointers) failing | The testbench is sensitive to the CDC structure | That all structures present are correct |
Clean behaviour with the injection disabled | Functional correctness of the logic | CDC safety at all |
Injection is a complement to structural checks and formal proof, not a substitute: it exercises resolutions, not timing, and it says nothing about mean time between failures. Use it to shake out protocol bugs that only appear under one resolution, and cite the structural classification for the metastability argument itself.
Yes, if the structure is wrong in a way random resolutions do not expose (for example a torn bus that usually resolves consistently), or if the real failure rate exceeds the budget. Injection complements structural signoff; it never replaces it.