Each bus bit has its own wire, delay, and synchroniser, and each synchroniser resolves independently. When several bits change at once, the destination can mix old and new bits.
Binary 0111 → 1000 changes four bits at once. A sample taken mid-change reads a value that was never on the bus.
How to read it: one slot is one destination clock cycle; each lane b3 to b0 is the output of one bit's synchroniser, and the bottom lane is those four bits read together as one word. The destination clock period is 1 ns and the per-bit wire delays are 0.0, 0.4, 0.8 and 1.2 ns.
Bits b1 and b0 fall one edge before b2 falls and b3 rises. The skew is smaller than one clock period, yet it is enough to split the change across two edges.
In the slot between them the word is 0100 (4). The source never held 4: it went from 7 straight to 8.
A receiver that reads the bus in that slot acts on a value nobody sent. Whether it happens depends on wire delay, not on logic, so a simulation without delays cannot show it.
How to read it: the top lane is the registered Gray word in the source domain and the bottom lane is the word seen after the per-bit synchronisers; both are shown in hex.
Each source step flips exactly one bit, so there is only one bit whose arrival time can differ.
The destination moves from 6 to 7 and from 7 to 5 a fixed synchroniser latency after the source, and never shows another value.
The same wire skew that produced the bad word 4 for binary has nothing to split here. This holds only while the source value changes one step at a time.
The safe structure depends on the value type.
Value type | Technique |
|---|---|
Counter or pointer that increments by 1 | Gray code (each step flips one bit); used by async FIFO pointers |
Arbitrary multi-bit data | Handshake: hold data stable, synchronise a request, capture, synchronise the acknowledge |
Stream of data | Asynchronous FIFO |
Slowly changing configuration | Bundled-data capture: hold data, transfer a captured/acknowledged load event, then sample under a timing contract |
Independent single bits (status flags with no relation) | Per-bit synchronisers are fine |
A stable bus alone is insufficient for a load pulse. Guarantee pulse capture and minimum event spacing (or use a request/acknowledge handshake), bound the latest data arrival before the earliest destination capture including setup margin, and hold the bus through capture until the returned acknowledgement. Reset both sides' event history consistently and prevent traffic during restart; otherwise a strobe can be lost or appear as a phantom event. See the canonical bundled-data timing contract and pulse capture and spacing.
The one-step condition applies to source updates, not consecutive destination observations. A slower destination may coherently see 4 then 6, skipping source value 5; those observed Gray words differ in two bits and that is legal. Gray pointer crossings convey snapshots, not lossless events. Use a handshake or FIFO for every event/data item. Require source-registered Gray and effective max-delay/bus-skew bounds that prevent successive source transitions overlapping at the receiving stage (normally bounded against the minimum source update interval, with implementation margin). Audit exception precedence and routed reports as in implementation hygiene; RTL simulations do not sign off those bounds.
One bit each time (bit 0, then bit 1). It is a legal Gray sequence of steps (these are the Gray codes of 4, 5 and 6).
`timescale 1ns/1ps
// Sampling uncertainty only: random digital capture at fixed TCQ after a window hit.
// No analogue state, late-resolution tail, or downstream disagreement is modelled.
module meta_ff #(parameter real TCQ = 0.3, TSU = 0.15, THD = 0.1) (
input logic clk, d,
output logic q
);
realtime t_d = -100.0, t_clk = -100.0;
int meta_events = 0;
initial q = 1'b0;
always @(d) begin
t_d = $realtime;
if ($realtime - t_clk < THD) begin // changed just after the edge (hold window)
meta_events++; q <= #TCQ $urandom_range(0, 1);
end
end
always @(posedge clk) begin
t_clk = $realtime;
if ($realtime - t_d < TSU) begin // changed just before the edge (setup window)
meta_events++; q <= #TCQ $urandom_range(0, 1);
end else q <= #TCQ d;
end
endmodule
// A W-bit bus crossing with one two-flop synchroniser per bit (what NOT to do for a counter)
module bus_sync_bits #(parameter int W = 4) (input logic clk, input logic [W-1:0] d, output logic [W-1:0] q);
for (genvar i = 0; i < W; i++) begin : g_bit
logic q1;
meta_ff f1 (.clk, .d(d[i]), .q(q1));
meta_ff f2 (.clk, .d(q1), .q(q[i]));
end
endmodule
Crossing | Anomalous steps (simulation run) | Why |
|---|---|---|
Binary counter, per-bit 2FF | 454 | Multi-bit transitions captured partly old, partly new |
Gray-coded counter, per-bit 2FF, decoded after the crossing | 0 | Only one bit changes per step, so the destination sees either the old or the new value |
Here an anomalous step means anything other than 0 or +1 between observations. The 454/0 counts are digital-model results, not physical failure rates; the bounded drain changes the sample total. Skips are legal when a destination undersamples a Gray pointer. Neither a zero anomaly count nor this rate-specific diagnostic proves physical CDC safety.
`timescale 1ns/1ps
module tb_gray_skip;
logic src_clk = 0, dst_clk = 0;
logic [3:0] count = 4, gray = 6;
logic [3:0] stage1 = 0, stage2 = 0;
logic [3:0] old_gray, source_delta;
always #5 src_clk = ~src_clk;
always #20 dst_clk = ~dst_clk;
always_ff @(posedge dst_clk) begin
stage1 <= gray;
stage2 <= stage1;
end
function automatic logic [3:0] decode(input logic [3:0] g);
logic [3:0] b;
b[3] = g[3];
for (int i = 2; i >= 0; i--) b[i] = b[i+1] ^ g[i];
return b;
endfunction
initial begin
$timeformat(-9, 0, " ns", 0); // print %t values in ns, not ps
// Source starts at 4; both subsequent source transitions are unit increments.
#30; #1;
old_gray = gray; count = 5; gray = count ^ (count >> 1);
source_delta = old_gray ^ gray;
if ($isunknown(source_delta) || !$onehot(source_delta)) $fatal(1, "4->5 source Gray step");
@(negedge src_clk); #1;
old_gray = gray; count = 6; gray = count ^ (count >> 1);
source_delta = old_gray ^ gray;
if ($isunknown(source_delta) || !$onehot(source_delta)) $fatal(1, "5->6 source Gray step");
@(posedge dst_clk); #1;
if (decode(stage2) !== 4'd4) $fatal(1, "expected snapshot 4");
$display("snapshot %0d at %0t", decode(stage2), $time);
@(posedge dst_clk); #1;
if (decode(stage2) !== 4'd6) $fatal(1, "expected coherent skipped snapshot 6");
$display("snapshot %0d at %0t", decode(stage2), $time);
$display("PASS: legal source unit steps, destination legitimately skips 5");
$finish;
end
initial begin #200; $fatal(1, "Gray skip watchdog"); end
endmoduleFinite source checker, run with Icarus Verilog 13.0 (2026-10-16): 454 anomalous binary steps against 0 Gray, then PASS. A binary counter and its registered Gray image cross bit by bit. Here the destination period (7.13 ns) is shorter than each source update interval (10 ns), with equal fixed-TCQ model stages and no added routing delay. The step <= 1 diagnostic is specific to this experiment; it is not a universal Gray crossing property. Window choices model sampling uncertainty only.
Directed checker, run with Icarus Verilog 13.0 (2026-10-16): prints snapshot 4 at 61 ns, then 6 at 101 ns. Source Gray updates 4 to 5 to 6 between destination captures; a 40 ns destination clock and two-register pipeline observe 4 then 6. The named delta vector checks the one-bit rule at the source only. This ideal digital example verifies the skip interpretation, not skew constraints or metastability reliability.