A configuration word must move from a block on one clock to a block on another, unrelated clock. Valid/ready cannot be used directly: each side samples the other's signal with its own clock, so a single-cycle valid might be missed, or seen for a different number of cycles than it lasted. Every CDC course teaches the same answer, and it predates valid/ready by decades.
How do two sides agree that a word moved when they do not even share a clock, and what does that agreement cost?
The obvious answer is valid/ready with synchronisers on both signals. But valid/ready's rule is "a transfer happens on an edge where both are high", and there is no shared edge across two clocks. One side sees the other's signal a few cycles late, and a pulse shorter than the receiver's sampling period may be missed entirely. A handshake across clocks must use signals that stay put until the other side has clearly seen them, and must never depend on a shared edge.
In a request/acknowledge handshake, each side changes its signal only in answer to a change it has seen from the other. Because each signal holds until answered, it does not matter how many cycles the other side takes to notice. There are two variants:
Variant | What means "a new word" | Steps per word | Trade-off |
|---|---|---|---|
Four-phase (return-to-zero) | req rising | req up, ack up, req down, ack down | Simple to reason about (levels); twice the round trips |
Two-phase (transition) | any change of req | req toggles, ack toggles | Half the round trips; each side must remember the last level it saw |
The data never crosses through a synchroniser. The sender sets the data before raising (or toggling) req and holds it until it sees ack, so the receiver always samples a settled bus. Only the single-bit req and ack signals need synchronisers, which is why this handshake is the standard way to move occasional words between clocks.
The testbench runs both variants on one clock, with each side reacting one cycle after it sees a change, as if one register separated them. Real crossings add a synchroniser's two or three cycles to each step, but the ratio between the variants stays the same.
// Four-phase and two-phase request/acknowledge handshakes carrying four words each,
// side by side on one clock. Each side reacts one cycle after it sees the other's
// change (as if a one-flop delay separated them), so the cycle counts show the
// protocols' cost per word. Four-phase: req up, ack up, req down, ack down per word.
// Two-phase: every toggle of req is a word, every toggle of ack answers it.
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
localparam int N = 4;
// ---- Four-phase ----
logic req4 = 0, ack4 = 0;
int sent4 = 0, got4 = 0, done4_at = -1, cyc = 0;
always @(posedge clk) if (rst_n) cyc <= cyc + 1;
always @(posedge clk) if (rst_n) begin // sender
if (!req4 && !ack4 && sent4 < N) begin req4 <= 1; sent4 <= sent4 + 1; end
else if (req4 && ack4) req4 <= 0;
end
always @(posedge clk) if (rst_n) begin // receiver
if (req4 && !ack4) begin ack4 <= 1; got4 <= got4 + 1; end
else if (!req4 && ack4) begin
ack4 <= 0;
if (got4 == N) done4_at <= cyc + 1;
end
end
// ---- Two-phase ----
logic req2 = 0, ack2 = 0;
int sent2 = 0, got2 = 0, done2_at = -1;
always @(posedge clk) if (rst_n) begin // sender: toggle when ack has caught up
if (req2 == ack2 && sent2 < N) begin req2 <= ~req2; sent2 <= sent2 + 1; end
end
always @(posedge clk) if (rst_n) begin // receiver: answer each toggle
if (req2 != ack2) begin
ack2 <= req2; got2 <= got2 + 1;
if (got2 + 1 == N) done2_at <= cyc + 1;
end
end
initial begin
#300;
if (got4 == N && got2 == N && done4_at == 2 * done2_at)
$display("PASS: four-phase took %0d cycles for %0d words, two-phase %0d: half the round trips", done4_at, N, done2_at);
else $display("FAIL: four-phase %0d words in %0d cycles, two-phase %0d words in %0d cycles", got4, done4_at, got2, done2_at);
$finish;
end
endmoduleHow to read it, slot by slot:
Four-phase, slots 1 to 4: req4 rises (slot 1), ack4 answers (slot 2), req4 falls (slot 3), ack4 falls (slot 4). Four steps, four cycles, one word. The next word starts in slot 5.
Two-phase, slots 1 to 2: req2 toggles (slot 1) and ack2 toggles to match (slot 2): one word in two cycles. The next toggle comes in slot 3.
By slot 8: two-phase has moved all four words; four-phase has moved two. Four-phase finishes its fourth word at slot 16.
Across a clock boundary each step waits for a synchroniser. With illustrative numbers, two-flop synchronisers, a 100 MHz sender and a 50 MHz receiver:
Each step = the other side's synchroniser (2 of its cycles) + 1 cycle to react
step seen by receiver: 2 x 20 ns + 20 ns = 60 ns
step seen by sender: 2 x 10 ns + 10 ns = 30 ns
Four-phase: 2 steps each way per word = 2 x 60 + 2 x 30 = 180 ns per word (about 5.6 M words/s)
Two-phase: 1 step each way per word = 60 + 30 = 90 ns per word (about 11 M words/s)Fine for configuration writes and status words; far too slow for a data stream, which is why streams cross clocks through an async FIFO instead (CDC course).
Option | What it costs | When it wins |
|---|---|---|
Four-phase req/ack | Two round trips per word | Occasional words across clocks; easiest to verify |
Two-phase req/ack | One round trip per word; edge-detection logic on each side | Higher event rates across clocks |
Async FIFO | Memory, Gray-coded pointers | Streams across clocks |
valid/ready | Requires a shared clock edge | Within one clock domain |
Where | What is used |
|---|---|
Configuration and status between clock domains | Four-phase or two-phase request/acknowledge, with synchronisers on req and ack and the data bus held stable across the handshake. |
Self-timed (clockless) links | The same four-phase protocol with no clock at all (lesson 1.8). |
Single events across clocks (pulses, interrupts) | A toggle synchroniser, which is a two-phase signal without the acknowledge. |
A sender updates its data bus on the same cycle it raises req, from a register that also changes on the next cycle (illustrative). The receiver captures when its synchronised copy of req arrives, two cycles later, by which time the bus holds the following value. The fix is the rule the protocol depends on: data changes only before req rises (or toggles), and holds until ack is seen. A handshake carries the timing; the data must obey it.
After it has seen ack high and lowered req. Changing it earlier risks the receiver capturing while the bus is changing.
Each two-phase word needs one req change and one ack change; each four-phase word needs two of each (up and back down).
Each signal holds its value until the other side answers, so it does not matter when, or in which clock, the other side samples it.
Request/acknowledge changes each signal only in answer to the other, so it works whatever the relative speeds or clocks.
Four-phase costs two round trips per word, two-phase one; across clocks each step waits for a synchroniser.
Only req and ack are synchronised; the data is held stable from before req until ack is seen.