The source holds the data stable while the request is high. The destination captures it and answers with an acknowledge.
Source puts the word on the data bus and raises req. The data stays stable from now on.
Destination sees req (after a synchroniser), captures the data, and raises ack.
Source sees ack (after a synchroniser) and lowers req. The data may change from now on.
Destination sees req low and lowers ack. The source sees ack low and is ready for the next word.
Each slot is one source clock cycle, sampled just after its rising edge. The destination clock is slower and unrelated, so every crossing takes a slot or two. This run uses plain two-flop synchronisers, with no metastability outcome.
Slot 2: the source loads the word and raises req on the same edge, so data A5 and req appear together.
Slots 3 and 4: req_sync rises after the destination synchroniser, and the destination captures the data and raises ack.
Slot 5: ack_sync rises in the source domain. The source lowers req on the next edge, in slot 6.
Slots 7 to 9: the low req crosses, ack falls, and the low ack crosses back. After slot 9 the source can accept the next word.
The data lane stays at A5 from slot 2 on, so it is stable the whole time req is high and while the destination reads it.
A digital old/new perturbation model on four flops, with offers stopped and a full drain required, moves 2,077 words at about 9 source cycles per word.
Check | Result |
|---|---|
Historical fixed-time cutoff | 2,075 delivered / 2,076 accepted; unfinished or lost final word was not distinguished |
Revised required result (executed: PASS, 2,077 words) | Accepted = delivered; queue empty; source ready and request/ack history idle under a watchdog |
Source clock / destination clock | 10 ns / 7.13 ns, unrelated |
Metastability model on all four synchroniser flops | Yes ( |
Throughput | About 9 source cycles per word |
Throughput is low by design: each word waits for the request to cross (2 to 3 destination cycles) and the acknowledge to cross back (2 to 3 source cycles).
The return-to-zero repeats both crossings. The scheme suits control and configuration, not data streams.
Holding the source register prevents overwrite; it does not bound wire delay. In this listing, req and data launch together, two destination flops synchronise req, and hs_dst consumes the previous synchroniser output on a third edge. With a request launched just before the first sampling edge, the earliest capture is about 2 destination periods after launch. Every data bit, including source clock-to-Q, routing and destination setup, must fit before that earliest capture with margin. A slower control arrival only increases the budget; do not size it from average handshake latency.
# Illustrative held-data constraint for the 7.13 ns destination period below.
# Earliest capture budget: 2 * 7.13 = 14.26 ns, less setup and margin.
# Conservative example: clock-to-Q + data route + setup <= 12.0 ns.
# Adapt object names and confirm what the tool includes in its path delay.
set_max_delay 12.0 -datapath_only -from [get_cells s/data_reg*] -to [get_cells d/out_data_reg*]
# Do not override this bound with clock groups or a false path on the bus.
# Require each bit in the effective max-delay report; keep req/ack stage-to-stage paths timed.No. The destination can capture that bit before it arrives and then acknowledge corrupted data. Bound the data arrival below the earliest capture opportunity, or delay capture with a documented, constrained control pipeline.
After it has seen ack high (the destination has captured the data) and has lowered req. In this design, the source only updates data in IDLE, when both req and ack are low.
The full handshake with a digital perturbation model and an order-checking testbench. The drain requires progressing clocks, no reset during the run, and an always-accepting pulse consumer.
`timescale 1ns/1ps
module meta_ff #(parameter real TCQ = 0.25, TSU = 0.3, THD = 0.15) (input logic clk, d, output logic q);
realtime t_d = -100.0, t_clk = -100.0;
initial q = 1'b0;
always @(d) begin
t_d = $realtime;
if ($realtime - t_clk < THD) q <= #TCQ $urandom_range(0, 1);
end
always @(posedge clk) begin
t_clk = $realtime;
if ($realtime - t_d < TSU) q <= #TCQ $urandom_range(0, 1);
else q <= #TCQ d;
end
endmodule
module sync2 (input logic clk, d, output logic q);
logic q1;
meta_ff f1 (.clk, .d, .q(q1));
meta_ff f2 (.clk, .d(q1), .q);
endmodule
// ---------------- 4-phase request/acknowledge transfer of one word ----------------
module hs_src #(parameter int W = 8) (
input logic clk, rst_n,
input logic send_valid, // producer offers a word
output logic send_ready, // high only when a new word can be accepted
input logic [W-1:0] send_data,
output logic req, // request to the destination
output logic [W-1:0] data, // held STABLE while req is high
input logic ack_sync // acknowledge, already synchronised into this domain
);
typedef enum logic [1:0] {IDLE, WAIT_ACK, WAIT_NACK} state_t;
state_t state;
assign send_ready = (state == IDLE);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin state <= IDLE; req <= 1'b0; data <= '0; end
else unique case (state)
IDLE: if (send_valid) begin data <= send_data; req <= 1'b1; state <= WAIT_ACK; end
WAIT_ACK: if (ack_sync) begin req <= 1'b0; state <= WAIT_NACK; end // destination has the data
WAIT_NACK: if (!ack_sync) state <= IDLE; // destination released: ready again
default: state <= IDLE;
endcase
endmodule
module hs_dst #(parameter int W = 8) (
input logic clk, rst_n,
input logic req_sync, // request, already synchronised into this domain
input logic [W-1:0] data, // crosses WITHOUT a synchroniser: legal because it is stable while req is high
output logic ack,
output logic out_valid, // one-cycle pulse with new data
output logic [W-1:0] out_data
);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin ack <= 1'b0; out_valid <= 1'b0; out_data <= '0; end
else begin
out_valid <= 1'b0;
if (req_sync && !ack) begin ack <= 1'b1; out_data <= data; out_valid <= 1'b1; end // capture, then acknowledge
else if (!req_sync) ack <= 1'b0;
end
endmodule
module hs_cdc #(parameter int W = 8) (
input logic clk_s, rst_s_n, clk_d, rst_d_n,
input logic send_valid, input logic [W-1:0] send_data, output logic send_ready,
output logic out_valid, output logic [W-1:0] out_data
);
logic req, ack, req_sync, ack_sync;
logic [W-1:0] data;
hs_src #(W) s (.clk(clk_s), .rst_n(rst_s_n), .send_valid, .send_ready, .send_data, .req, .data, .ack_sync);
sync2 u_req (.clk(clk_d), .d(req), .q(req_sync)); // request crosses to the destination clock
hs_dst #(W) d (.clk(clk_d), .rst_n(rst_d_n), .req_sync, .data, .ack, .out_valid, .out_data);
sync2 u_ack (.clk(clk_s), .d(ack), .q(ack_sync)); // acknowledge crosses back
endmodule
Revised bench, run with Icarus Verilog 13.0 (2026-10-16): prints PASS with 2,077 words at about 9 source cycles per word. The original run, with a digital old/new perturbation model on four flops, also averaged about 9 source cycles per word but stopped with one accepted word unaccounted for; the revised source stops offers, preserves a pending offer and requires full drain. The model neither simulates analog resolution nor establishes MTBF.
Full testbench, run with Icarus Verilog 13.0 (2026-10-16): prints PASS. It is finite functional evidence, not a physical reliability guarantee.