A single-bit synchronizer solves the problem of crossing a control signal between clock domains. But what about a 32-bit data word? You cannot simply run each bit through its own synchronizer — each bit settles at a different time, and the receiving domain captures a word that is half old data and half new. The solution is the request-acknowledge handshake: freeze the data, send a signal when it is ready, wait for the other domain to confirm receipt, then release.
Imagine you have a 32-bit configuration register in clock domain A. You want to read it safely in clock domain B. The naive approach — running each bit through a 2-FF synchronizer independently — is fatally wrong for one reason: the bits are not synchronized to each other in the destination domain.
Here is exactly what goes wrong. Suppose the data word changes from 0x0000_0000 to 0xFFFF_FFFF. Bit 0 transitions at time T. Clock domain B samples bit 0 and gets the new value 1. But bit 15 transitions 50 picoseconds later due to routing differences. Clock domain B's synchronizer for bit 15 samples just before that transition and captures the old value 0. The receiver reads 0x0000_7FFF — a word that never existed in domain A. This is called a data tear or multi-bit skew corruption.
Routing skew, process variation, and different metastability settling times guarantee that independently synchronized bits will be captured at different times. The resulting word in the destination domain is garbage — it never existed in the source domain and can corrupt your system state.
The safe solution requires a different approach: the handshake protocol. The core idea is simple — ensure data is frozen and stable before crossing, and use a single control signal (req) to tell the receiver that data is ready. Since req is a single bit, it can be safely synchronized with a 2-FF synchronizer. Only one req edge crosses the domain boundary, so there is no skew problem. The data lines themselves are held constant during the entire crossing window, so they carry no new transitions for the synchronizer to deal with.
The request-acknowledge (req/ack) handshake works as follows:
This round-trip handshake guarantees that data is stable on the bus for the entire time the receiver is sampling it. There is no skew problem because the data lines are not transitioning — only the single-bit req line transitions, and it is properly synchronized.
The 4-phase handshake is the standard form and the easiest to understand. It uses four signal transitions per transfer, each of which crosses a clock domain boundary through a synchronizer:
SENDER domain (clk_a) RECEIVER domain (clk_b)
data =========================================================
[ DATA STABLE -- hold throughout handshake ]
req ______|"""""""""""""""""""""""""""""|__________________
(1) assert req
req_sync __________|"""""""""""""""""""|__________________
2-FF sync delay (2) receiver sees req_sync
(3) receiver captures data
(4) receiver asserts ack
ack __________________|"""""""""|____________
ack in clk_b domain
ack_sync ____________________________|"""""""|______________
2-FF sync delay
(5) sender sees ack_sync
(6) sender deasserts req
req """""""""""""""""""""""""""""""""""""""""|______________
req falls
ack falls after req_sync falls in clk_b domain -- transfer done
The four transitions that give this protocol its name are:
Each transition crosses the domain boundary and takes approximately 2 synchronizer flip-flop delays. Total latency for one complete transfer is roughly 4 × 2 clock cycles — 2 clk_b cycles for req to synchronize, 2 clk_a cycles for ack to synchronize, and the same again for the deassert phase.
The 2-phase or toggle handshake reduces latency by half. Instead of requiring a level to go high then low, it simply toggles the req line for each new transfer. The receiver detects any edge on req_sync and captures data. It then toggles ack to confirm.
This means only two synchronizer crossings occur per transfer: req toggle (one crossing) and ack toggle (one crossing). The protocol is roughly twice as fast as 4-phase, but it is harder to implement correctly because the logic must detect edges rather than levels, and a missed toggle is catastrophic — the protocol enters an unrecoverable state with no way to tell sender and receiver apart.
The following implementation uses a 4-phase level-sensitive handshake. It has three modules: a sender FSM in clk_a, a receiver FSM in clk_b, and a top-level wrapper that instantiates both 2-FF synchronizers.
// Sender FSM -- clock domain A
// Drives req and holds data stable until ack is received
module handshake_sender #(
parameter DW = 32
) (
input wire clk_a,
input wire rst_a,
// Data interface (source side)
input wire [DW-1:0] src_data, // data word to transfer
input wire src_valid, // pulse: new data to send
output reg src_ready, // high when sender can accept new data
// Handshake signals (cross to clk_b domain)
output reg req, // request to receiver (single-bit, synchronizable)
output reg [DW-1:0] data_out, // frozen data bus (stable during entire handshake)
// Ack return (already synchronized into clk_a by 2-FF sync)
input wire ack_sync // ack from receiver, synchronized into clk_a
);
// FSM states
localparam IDLE = 2'd0;
localparam WAIT_ACK = 2'd1;
localparam WAIT_LOW = 2'd2;
reg [1:0] state;
always @(posedge clk_a or posedge rst_a) begin
if (rst_a) begin
state <= IDLE;
req <= 1'b0;
data_out <= {DW{1'b0}};
src_ready <= 1'b1;
end else begin
case (state)
IDLE: begin
src_ready <= 1'b1;
if (src_valid) begin
data_out <= src_data; // freeze data NOW
req <= 1'b1; // assert request
src_ready <= 1'b0; // block new data
state <= WAIT_ACK;
end
end
WAIT_ACK: begin
// Hold req and data_out stable
// Wait for receiver to acknowledge
if (ack_sync) begin
req <= 1'b0; // deassert req (phase 3 of 4-phase)
state <= WAIT_LOW;
end
end
WAIT_LOW: begin
// Wait for ack to fall after receiver sees req fall
if (!ack_sync) begin
src_ready <= 1'b1; // ready for next transfer
state <= IDLE;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
// Receiver FSM -- clock domain B
// Captures data when req_sync rises, asserts ack to confirm
module handshake_receiver #(
parameter DW = 32
) (
input wire clk_b,
input wire rst_b,
// Handshake signals (req already synchronized into clk_b)
input wire req_sync, // req from sender, synchronized into clk_b
output reg ack, // acknowledge back to sender (single-bit, synchronizable)
// Captured data output
output reg [DW-1:0] dst_data, // captured data word (valid when dst_valid high)
output reg dst_valid, // pulse: new captured data available
// Frozen data bus from sender (held stable by sender FSM)
input wire [DW-1:0] data_in
);
localparam IDLE = 2'd0;
localparam ACK_HIGH = 2'd1;
reg [1:0] state;
reg req_prev; // previous cycle req_sync (rising-edge detect)
always @(posedge clk_b or posedge rst_b) begin
if (rst_b) begin
state <= IDLE;
ack <= 1'b0;
dst_data <= {DW{1'b0}};
dst_valid <= 1'b0;
req_prev <= 1'b0;
end else begin
req_prev <= req_sync;
dst_valid <= 1'b0; // default: pulse for one cycle only
case (state)
IDLE: begin
// Detect rising edge of req_sync
if (req_sync && !req_prev) begin
dst_data <= data_in; // capture data (guaranteed stable by sender)
dst_valid <= 1'b1; // pulse valid for one cycle
ack <= 1'b1; // assert ack (phase 2 of 4-phase)
state <= ACK_HIGH;
end
end
ACK_HIGH: begin
// Hold ack high until sender deasserts req
if (!req_sync) begin
ack <= 1'b0; // deassert ack (phase 4 of 4-phase)
state <= IDLE;
end
end
default: state <= IDLE;
endcase
end
end
endmodule
// Top-level CDC handshake wrapper
// Instantiates sender, receiver, and both 2-FF synchronizers
module handshake_cdc_top #(
parameter DW = 32
) (
// Clock domain A (sender)
input wire clk_a,
input wire rst_a,
input wire [DW-1:0] src_data,
input wire src_valid,
output wire src_ready,
// Clock domain B (receiver)
input wire clk_b,
input wire rst_b,
output wire [DW-1:0] dst_data,
output wire dst_valid
);
wire req;
wire [DW-1:0] data_bus;
wire ack;
wire req_sync;
wire ack_sync;
// --- Sender FSM (clk_a domain) ---
handshake_sender #(.DW(DW)) u_sender (
.clk_a (clk_a),
.rst_a (rst_a),
.src_data (src_data),
.src_valid (src_valid),
.src_ready (src_ready),
.req (req),
.data_out (data_bus),
.ack_sync (ack_sync)
);
// --- 2-FF synchronizer: req clk_a -> clk_b ---
// synthesis attribute ASYNC_REG of req_ff1 is "true"
// synthesis attribute ASYNC_REG of req_ff2 is "true"
(* ASYNC_REG = "TRUE" *) reg req_ff1, req_ff2;
always @(posedge clk_b or posedge rst_b)
if (rst_b) {req_ff2, req_ff1} <= 2'b00;
else {req_ff2, req_ff1} <= {req_ff1, req};
assign req_sync = req_ff2;
// --- Receiver FSM (clk_b domain) ---
handshake_receiver #(.DW(DW)) u_receiver (
.clk_b (clk_b),
.rst_b (rst_b),
.req_sync (req_sync),
.ack (ack),
.dst_data (dst_data),
.dst_valid (dst_valid),
.data_in (data_bus)
);
// --- 2-FF synchronizer: ack clk_b -> clk_a ---
// synthesis attribute ASYNC_REG of ack_ff1 is "true"
// synthesis attribute ASYNC_REG of ack_ff2 is "true"
(* ASYNC_REG = "TRUE" *) reg ack_ff1, ack_ff2;
always @(posedge clk_a or posedge rst_a)
if (rst_a) {ack_ff2, ack_ff1} <= 2'b00;
else {ack_ff2, ack_ff1} <= {ack_ff1, ack};
assign ack_sync = ack_ff2;
endmodule
Always mark both flip-flops in each 2-FF synchronizer with (* ASYNC_REG = "TRUE" *) (Xilinx/Vivado) or the equivalent ASYNC_REG attribute. This tells the tool chain not to retime them and to place them in the same physical location for minimum clock-to-clock skew. Without these attributes, the synthesizer may separate the flip-flops and insert timing arcs that break CDC analysis tools.
For the handshake to work correctly, the sender must guarantee that data_out is valid for the entire period from when req is asserted to when ack_sync returns. More precisely:
clk_a cycle before req asserts, so the req synchronizer does not race with a data transitionclk_b cycles after req_sync rises, giving the synchronizer time to settle and the receiver FSM time to captureack_sync goes high in clk_a (enforced by the FSM remaining in WAIT_ACK state)| Phase | Signal | Required condition |
|---|---|---|
| T0 | data_out stable | At least 1 clk_a cycle before req asserts |
| T0 | req asserts | Sender FSM enters WAIT_ACK; data frozen |
| T0 + 2×Tclk_b | req_sync stable | Receiver FSM can safely read req_sync |
| T0 + 2×Tclk_b + 1 | dst_data captured | Receiver latches data on rising edge of clk_b |
| T0 + 2×Tclk_b + 2 | ack asserts in clk_b | Receiver enters ACK_HIGH state |
| T0 + 2×Tclk_b + 2×Tclk_a | ack_sync stable in clk_a | Sender FSM sees ack, deasserts req |
Let us calculate the minimum handshake latency for a realistic design.
Given: clk_a = 100 MHz (Tclk_a = 10 ns), clk_b = 200 MHz (Tclk_b = 5 ns)
The 4-phase handshake round-trip requires:
| Phase | Duration |
|---|---|
| req assert → req_sync stable in clk_b | 2 × 5 ns = 10 ns |
| Receiver captures data + asserts ack | 1 × 5 ns = 5 ns |
| ack → ack_sync stable in clk_a | 2 × 10 ns = 20 ns |
| Sender deasserts req | 1 × 10 ns = 10 ns |
| req fall → req_sync low in clk_b | 2 × 5 ns = 10 ns |
| ack fall → ack_sync low in clk_a (transfer complete) | 2 × 10 ns = 20 ns |
| Total minimum latency | 75 ns |
At 75 ns per transfer, the maximum throughput is approximately 13.3 million transfers per second. For a 32-bit word, this is about 425 Mbps — respectable for a control or configuration path, but completely unsuitable for video streaming or DMA (use a dual-clock FIFO for those).
For a 2-phase handshake with the same clocks, the deassert phases are eliminated. Only two synchronizer crossings occur per transfer: approximately 2×Tclk_b + 2×Tclk_a = 10 + 20 = 30 ns, plus FSM response time bringing the total to roughly 37–40 ns. This gives about 25–27 million transfers per second, nearly double the 4-phase rate.
When you need to transfer multiple data words back-to-back, you have two choices:
| Approach | Best for | Throughput | Complexity |
|---|---|---|---|
| Repeated handshake | Low-rate control, configuration registers, single-word status reads | Low (1 word per full handshake cycle) | Low |
| Dual-clock FIFO | Streaming data, bursts, video, audio, DMA | High (near wire-rate, limited by FIFO depth) | Medium-High |
The repeated handshake is ideal for single-word transfers: a configuration register update, a status word read, a one-shot command. Each transfer is independent and fully confirmed before the next begins. You can pipeline successive transfers by starting the next handshake as soon as the previous WAIT_LOW state completes, but you still incur one full round-trip latency per word.
For streaming data — audio samples, pixel values, DMA bursts — a dual-clock FIFO is always the right answer. The FIFO uses Gray-coded read and write pointers to safely cross the domain boundary and achieves near-wire-rate throughput. The handshake is embedded inside the FIFO's pointer crossing logic and you never see it explicitly. When a burst of N words arrives, all N words enter the FIFO quickly and are read out in the other domain without waiting for a round-trip handshake on each word.
The sender changes data_out before receiving ack_sync. The receiver may have sampled a transition rather than a stable value. Always use an FSM that explicitly holds data in a register from the moment req asserts until ack_sync goes high. Never use combinational logic on the data bus during the handshake window.
The sender asserts req for a new transfer before the previous ack has fully deasserted. The receiver sees req continuously high and never detects the second rising edge. Always ensure req returns to 0 and ack_sync returns to 0 before starting the next transfer. The WAIT_LOW state in the sender FSM enforces this correctly.
Without ASYNC_REG or equivalent attributes, the synthesizer may place the two synchronizer flip-flops in different logic slices, adding routing delay between them. This increases the risk that a metastable output from FF1 propagates to FF2 before settling. Always annotate your synchronizer flip-flops and verify placement in the post-implementation report.
A common misconception: "if I synchronize each data bit with its own 2-FF synchronizer, the data will be safe." Wrong. The two synchronizers are independent and resolve metastability at different times. The data bus must never be connected to synchronizer inputs. Only the single-bit req crosses through a synchronizer. Data lines are held static and sampled only after req_sync has cleanly settled.
ack is generated in clk_b and must cross back to clk_a through its own 2-FF synchronizer. Connecting ack directly to the sender FSM input creates a metastability hazard in the sender's flip-flops. Every control signal that crosses a clock boundary — in both directions — needs its own synchronizer chain.
A question that often comes up: if data lines are not synchronized, how does the timing tool know they are safe? The answer is that data lines in a handshake are quasi-static during the crossing window. They change only in the clk_a domain when the sender is in IDLE. During the entire window that req_sync is high in clk_b, the data lines are guaranteed not to change.
You must add a false path constraint to the data lines in your synthesis and implementation tool. Without this, the tool tries to time the data lines as if they must meet setup/hold to clk_b, which they cannot — they are registered by clk_a. You tell the tool: these data lines are stable when sampled, so skip timing analysis on this path.
# False path on req synchronizer input (metastability handled by 2-FF chain)
set_false_path -from [get_cells u_sender/req_reg] \
-to [get_cells req_ff1_reg]
# False path on ack synchronizer input
set_false_path -from [get_cells u_receiver/ack_reg] \
-to [get_cells ack_ff1_reg]
# False path on data bus (quasi-static during crossing window)
# Sender FSM guarantees data is held stable -- no timing constraint needed
set_false_path -from [get_cells {u_sender/data_out_reg[*]}] \
-to [get_cells {u_receiver/dst_data_reg[*]}]
Handshake CDC is a protocol for safely transferring a multi-bit data word between asynchronous clock domains by freezing data, crossing a single-bit req signal through a synchronizer, and waiting for an ack to confirm receipt. Use it for low-rate control and configuration transfers. For high-bandwidth streaming, use a dual-clock FIFO instead.
4-phase uses four signal transitions per transfer (req high, ack high, req low, ack low) and is simpler to implement. 2-phase uses toggle edges — req and ack simply toggle for each transfer — requiring only two synchronizer crossings per transfer and giving roughly twice the throughput, at the cost of increased implementation complexity and harder debugging.
The sender must hold data stable from before req asserts until ack_sync goes high in the sender domain. This covers the full round-trip: 2 receiver clock cycles for req to synchronize, receiver FSM response time, and 2 sender clock cycles for ack to synchronize back. In practice this is tens to hundreds of nanoseconds depending on clock frequencies. The sender FSM WAIT_ACK state enforces this automatically.