The 2-FF synchronizer is great for stable signals, but what about narrow pulses? A pulse might last only 1 or 2 clock cycles in the source domain — the receiving clock might miss it entirely. Pulse synchronizers and handshake protocols solve this by stretching events and coordinating transfers across clock boundaries.
Imagine a 1-cycle pulse on event_A in clock domain A. When it arrives at a 2-FF synchronizer in domain B:
This is catastrophic for control signals: a request that never arrives will hang the system.
If your signal is a 1-cycle pulse, you must use a pulse synchronizer or handshake protocol. The 2-FF synchronizer alone cannot guarantee narrow pulses will be captured.
A simple pulse synchronizer stretches a narrow pulse into a wide pulse (typically several clock cycles) before crossing, so the receiving clock definitely sees it:
// Pulse Synchronizer
// Input: 1-cycle pulse on pulse_in (clk_a)
// Output: Synchronized pulse on pulse_out (clk_b)
module pulse_sync (
input clk_a, rst_n_a,
input pulse_in, // 1-cycle pulse in domain A
input clk_b, rst_n_b,
output pulse_out // Synchronized pulse in domain B
);
wire pulse_stretched; // Stretched version in domain A
reg stretch_en; // Stretch enable
reg [2:0] stretch_cnt;
// Stretch pulse in source domain (3 cycles)
always @(posedge clk_a or negedge rst_n_a) begin
if (!rst_n_a) begin
stretch_cnt <= 0;
stretch_en <= 0;
end else if (pulse_in) begin
stretch_cnt <= 3; // Stretch for 3 cycles
stretch_en <= 1;
end else if (stretch_cnt > 0) begin
stretch_cnt <= stretch_cnt - 1;
stretch_en <= 1;
end else begin
stretch_en <= 0;
end
end
assign pulse_stretched = stretch_en;
// Sync stretched pulse to domain B with 2-FF sync
wire pulse_sync_ff1, pulse_sync_ff2;
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b)
pulse_sync_ff1 <= 0;
else
pulse_sync_ff1 <= pulse_stretched;
end
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b)
pulse_sync_ff2 <= 0;
else
pulse_sync_ff2 <= pulse_sync_ff1;
end
// Detect rising edge in domain B
reg pulse_sync_ff2_r;
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b)
pulse_sync_ff2_r <= 0;
else
pulse_sync_ff2_r <= pulse_sync_ff2;
end
assign pulse_out = pulse_sync_ff2 & ~pulse_sync_ff2_r;
endmodule
For more complex transactions (especially when data must cross), handshake protocols provide reliable flow control:
Request-Acknowledge (req-ack) protocol:
req and places data on output busreq, captures data, and asserts ackack and deasserts reqreq deassert and deasserts ackThe handshake ensures the receiver has captured data before the sender releases it. This is atomic: no data can be lost or duplicated.
// Handshake: Sender (req-side, domain A)
module handshake_sender #(parameter DWIDTH = 8) (
input clk_a, rst_n_a,
input [DWIDTH-1:0] data_out,
input data_valid, // User says: send this data
output reg req_a, // Request signal
input ack_sync // Ack from receiver (synced to domain A)
);
always @(posedge clk_a or negedge rst_n_a) begin
if (!rst_n_a)
req_a <= 0;
else if (data_valid)
req_a <= 1; // Assert request
else if (ack_sync)
req_a <= 0; // Deassert when ack received
end
endmodule
// Handshake: Receiver (ack-side, domain B)
module handshake_receiver #(parameter DWIDTH = 8) (
input clk_b, rst_n_b,
input [DWIDTH-1:0] data_in,
input req_sync, // Request (synced to domain B)
output reg ack_b, // Acknowledge signal
output reg [DWIDTH-1:0] data_captured,
output data_valid_b // Output: data is ready
);
reg req_sync_r;
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b) begin
req_sync_r <= 0;
ack_b <= 0;
data_captured <= 0;
end else begin
req_sync_r <= req_sync;
// Detect rising edge on req_sync
if (req_sync & ~req_sync_r) begin
data_captured <= data_in; // Capture data
ack_b <= 1; // Assert ack
end else if (~req_sync & req_sync_r) begin
ack_b <= 0; // Deassert ack when req goes low
end
end
end
assign data_valid_b = ack_b;
endmodule
// Typical instantiation:
// - req_a synced to domain B with 2-FF: req_sync_b
// - ack_b synced to domain A with 2-FF: ack_sync_a
// - data_out tied to same bus in both domains during handshake
A toggle synchronizer is another way to safely cross pulses. Instead of stretching, it toggles a bit on each event, then detects the toggle in the receiving domain:
This is more elegant than stretching and works well when clock frequencies are very different.
// Toggle Synchronizer
module toggle_sync (
input clk_a, rst_n_a,
input event_in, // Pulse or event in domain A
input clk_b, rst_n_b,
output pulse_out // Synchronized pulse in domain B
);
// Toggle on each event (domain A)
reg toggle_a;
always @(posedge clk_a or negedge rst_n_a) begin
if (!rst_n_a)
toggle_a <= 0;
else if (event_in)
toggle_a <= ~toggle_a; // Toggle
end
// Sync toggle to domain B (2-FF)
reg toggle_sync_ff1, toggle_sync_ff2;
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b) begin
toggle_sync_ff1 <= 0;
toggle_sync_ff2 <= 0;
end else begin
toggle_sync_ff1 <= toggle_a;
toggle_sync_ff2 <= toggle_sync_ff1;
end
end
// Detect edge in domain B
reg toggle_prev;
always @(posedge clk_b or negedge rst_n_b) begin
if (!rst_n_b)
toggle_prev <= 0;
else
toggle_prev <= toggle_sync_ff2;
end
// Output pulse = edge detected
assign pulse_out = toggle_sync_ff2 ^ toggle_prev;
endmodule
| Technique | Best for | Pros | Cons |
|---|---|---|---|
| 2-FF Sync | Stable signals, low-frequency changes | Simple, low latency | Loses narrow pulses |
| Pulse Stretcher | 1-cycle pulses, events | Easy to understand, stretches before sync | Adds latency, fixed stretch duration |
| Toggle Sync | Pulses, events, variable clock rates | Elegant, handles any pulse width | Slightly more logic |
| Req-Ack Handshake | Data transactions, flow control | Atomic, works with multi-bit data | Slower (roundtrip latency) |
| Valid-Ready | Streaming data, pipelined crossings | Flexible, supports backpressure | More complex to implement |
Single-bit pulse? Use toggle synchronizer or pulse stretcher. Data + flow control? Use req-ack handshake. Streaming data? Use valid-ready or dual-clock FIFO (Day 5).
A circuit that stretches a narrow pulse into a wide pulse before crossing clock domains. The wide pulse is guaranteed to be seen by the receiving clock, preventing event loss.
Pulse sync: for simple events that don't require acknowledgment or feedback. Handshake: for transactions where sender must know the receiver captured the data.
A synchronizer that toggles a bit on each event, then syncs the toggled bit to the other domain. The receiving clock detects edges via XOR. It's more elegant and flexible than pulse stretching.
The handshake creates a guaranteed sequence: sender puts data, asserts req; receiver captures, asserts ack; sender deasserts req only after seeing ack. This prevents race conditions and duplicates.
Pulse sync is best for single-bit events. For multi-bit data, use a handshake (req-ack) or dual-clock FIFO (Day 5).
Valid-ready is for unidirectional pipelined flow (source says valid=1, sink must be ready). Req-ack is bidirectional handshake (initiator req, responder ack). Both are CDC-safe when properly synchronized.