Mux recirculation is the simplest held-data crossing. The destination register keeps its old value through a feedback mux and loads the crossing bus only on a synchronised load pulse. Correctness rests on one promise: the source holds the data stable from before the pulse can be sampled until after the capture has settled.
Rule | Why |
|---|---|
Data stable before the earliest the pulse can be sampled | Skew across the data bits plus synchroniser latency moves the capture window earlier than the nominal pulse |
Data stable after the latest the capture can settle | The slowest data bit plus destination skew decides the hold side |
Pulse wide enough to be sampled | A pulse shorter than the destination period can fall between edges; stretch it or use a toggle (see CDC handshakes for the design) |
Pulses spaced wider than the synchroniser latency | Overlapping pulses merge into one capture |
The held-data promise belongs in a protocol assertion next to the RTL, so any source change that breaks it fails loudly:
// snippet: shorthand for the held-data promise (illustrative names). It samples the
// asynchronous source bus in the destination domain for ONE cycle, so it is only a
// shorthand: the real obligation is a stability WINDOW.
assert property (@(posedge clk_d) load_pulse |-> $stable(data_s));
// window form: data_s must be unchanged from N destination cycles before the earliest
// capture to M cycles after the latest, where N and M cover the bus skew plus the
// synchroniser latency on both sides of the pulse. Bind it in the source domain, qualified
// by the pulse, or check it with a counter that restarts whenever data_s changes.The one-line assertion states the letter of the law; the stability window, with the skew analysis in the timing rules, is its spirit and is what a reviewer asks for. In simulation the checker below plays the same role in plain code so it runs everywhere.
An 8-bit transfer with an explicit recirculation mux, a stretched 4-cycle load pulse into a faster destination clock, and a scoreboard that checks every capture against the launched word. The checker compares the settled capture one cycle after each pulse: the edge-detected pulse enables the register for the following edge, so the captured word is visible one cycle after the pulse fires.
`timescale 1ns/1ps
// Mux-recirculation capture: the destination reloads from the crossing bus on
// the synchronised load pulse and otherwise keeps its old value (the mux
// recirculates it). The source promises data_s stable around every pulse.
module mux_xfer (
input logic clk_s, clk_d, rst_n,
input logic [7:0] data_s,
input logic load_s, // held high 4 source cycles per transfer
output logic [7:0] data_d,
output logic load_pulse // one destination cycle per transfer
);
logic s1, s2, s2_d;
always_ff @(posedge clk_d or negedge rst_n)
if (!rst_n) begin s1 <= 0; s2 <= 0; s2_d <= 0; end
else begin s1 <= load_s; s2 <= s1; s2_d <= s2; end
assign load_pulse = s2 & ~s2_d; // edge detector
logic [7:0] data_q;
assign data_d = data_q;
always_ff @(posedge clk_d or negedge rst_n)
if (!rst_n) data_q <= '0;
else data_q <= load_pulse ? data_s : data_q; // the recirculation mux
endmodule
The pulse can fall between two destination edges and no capture happens; the scoreboard sees 7 captures instead of 8. Stretch the pulse past one destination period plus margin, or carry the event as a toggle with an edge detector.