The symptom is a count that never adds up: interrupts or credits sent from a fast domain arrive fewer times than sent, rarely and irreproducibly. Simulation passes because the test never places two events close together; the lab fails whenever real traffic does. The mechanism is almost always spacing: a toggle scheme re-toggled before the destination could absorb the first event.
The simulation output below drives three source events through a toggle synchroniser into a domain four times slower. The first two events are 20 ns apart, inside one 40 ns destination window; the third stands alone.
How to read it: pulse_s carries three one-cycle source events and toggle_s flips once per event. q1 and sync_t are the destination synchroniser stages and pulse_d is the edge-detected output. The flips at the first two events cancel inside a single destination sampling window, so sync_t never moves and pulse_d stays silent: two events in, nothing out. The third flip stands alone in its window, sync_t rises two slow edges later, and pulse_d fires once.
A toggle scheme carries each event as a level change and needs each level sampled at least once before the next change. Re-toggling inside one destination sampling window returns the line to its old value before any sampling edge sees the new one, and the destination cannot distinguish that from no event at all. The general rule is generous spacing: each toggle level must stand for enough destination cycles to cross the synchroniser and be consumed, so the source must rate-limit events or hold off on a busy indication. Short pulses without any scheme are worse: a pulse narrower than the destination sampling window may never overlap a sampling edge at all.
Structural analysis flags pulse crossings without a recognised scheme or without a spacing constraint, and the spacing lesson of the pulse topic states the minimum-gap rule the review should have demanded. A report entry of unspaced pulse crossing plus a testbench that never emits back-to-back events is the full signature of this bug waiting to happen.
Fix it at the source: enforce the minimum gap with a busy flag or acknowledge, or replace the toggle with a handshake when the event rate cannot be bounded. The guard has two halves that last: an assertion bounding the minimum spacing between source events, and a self-checking test that emits events at the minimum legal gap and counts receipts. The testbench below is that second half for an honestly spaced design, and it prints PASS:
module sync2 (
input logic clk,
input logic d,
output logic q
);
logic q1 = 1'b0, q2 = 1'b0;
always @(posedge clk) begin
q1 <= d;
q2 <= q1;
end
assign q = q2;
endmodule
module tb;
reg clk_s = 0;
always #5 clk_s = ~clk_s;
reg clk_d = 0;
always #15 clk_d = ~clk_d;
reg toggle_s = 1'b0;
wire sync_t;
logic sync_t_d = 1'b0;
wire pulse_d;
integer received = 0;
sync2 u_tog (.clk(clk_d), .d(toggle_s), .q(sync_t));
always @(posedge clk_d) sync_t_d <= sync_t;
assign pulse_d = sync_t ^ sync_t_d;
always @(posedge clk_d) if (pulse_d) received = received + 1;
initial begin
#26 toggle_s = 1'b1;
#120 toggle_s = 1'b0;
#120 toggle_s = 1'b1;
#150;
if (received == 3) $display("PASS: 3 sent, 3 received");
else $display("FAIL: 3 sent, %0d received", received);
$finish;
end
endmoduleZero: both flips can fall inside one sampling window and cancel, so the destination may see no change at all.
A spacing assertion proves the source cannot emit events closer than the rule allows; a counting testbench proves legally spaced events are all received exactly once.