The four-phase request/acknowledge handshake moves a running word sequence between domains. A reset event arrives every 100 to 400 ns on the destination only, the source only, or both.
The bench tracks accepted words in an epoch-tagged queue, explicitly cancels pending words at reset, and drains the final epoch. Single-sided modes intentionally report protocol damage; the coordinated-reset mode requires exact accounting.
Kind | Mechanism |
|---|---|
Reset-value leakage | A flop in reset drives its reset value across the boundary, which the other side may take for real data (the 271 zeros). |
Protocol mismatch | One side forgets the state of the handshake while the other remembers it (the 3 duplicates). |
Asynchronous reset edge | The reset assertion itself changes outputs at an arbitrary time, so even a single-bit control signal can change inside the receiving flop's window. |
Protocol mismatch is the easiest to see. The destination is reset while the source still holds req high, so after reset it treats the old request as a new one.
Each slot is one clock cycle, numbered from 0, showing the values just after the edge that starts it. For a short picture both sides share one 10 ns clock and the two-flop synchronisers are ideal flops; the protocol logic is the listing's.
Slots 0 to 2: the source has sent word 0x05 and holds req high while the request passes through the two synchroniser flops.
Slot 3: the destination captures 0x05. ack and out_valid go high: this is the real delivery.
Slot 4: rst_d_n is low. The destination forgets the transfer: ack and out_data return to 0, while req is still high.
Slot 6: out of reset, the destination still sees the old request (its synchronised copy req_sync is high, although req itself fell at this edge) with ack low, so it captures 0x05 again. The source counted one transfer; the consumer received two.
Reset-value leakage: the reset value of the data register is visible across the crossing and can be captured as a valid word. The fix is to not reset data buses (reset only control), or to mark them invalid while in reset.
The handshake with random resets on either side or both.
`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
Reset applied to | Words delivered | Duplicates | Stale words | Why |
|---|---|---|---|---|
Destination only | 4,532 | 3 | 0 | After reset the destination sees |
Source only | 4,587 | 2 | 271 | The source's data register resets to 0 while |
Both together | 4,197 | 0 | 0 | Both protocol states restart together; the word in flight is cancelled |
In the coordinated-reset mode (MODE=2) the bench requires accepted = accounted deliveries + explicit cancellations, no unexpected delivery and a final drain: accepted=4606, delivered=4197, cancelled=409, errors=0. MODE=0/1 report diagnostics only, because their damage is intentional. Queue epochs belong to this testbench oracle; a real exactly-once protocol needs transmitted transaction identity, replay and deduplication.
The results table is from the listing above, run with Icarus Verilog 13.0 (2026-10-16), default seed, one run per MODE. MODE=2 prints ACCOUNT accepted=4606 delivered=4197 cancelled=409 errors=0, then PASS. MODE=0 and MODE=1 keep diagnostic RESULT output because their damage is intentional.
The original eight-bit modular monitor checks duplicates and backward jumps only when the true sequence gap is less than 128. It cannot establish transaction conservation across arbitrary reset epochs or distinguish wraparound from a large gap, so the revised bench adds an epoch-tagged queue.