Coordinated resets are simplest, but some blocks cannot reset together. Three rules then make a crossing survive the other side's reset: keep the crossing data, arm the receiver, and block new traffic until recovery is complete.
Data retention and an armed receiver reduce reset damage, but they do not start a new handshake epoch by themselves. A stale ack can survive a source reset: a source that returns to IDLE and accepts another word lets that old ack complete the new request. The source below therefore also needs link_ready_s from the reset controller before it accepts any word.
Problem | Rule | Change |
|---|---|---|
Source reset puts 0 on the data bus (stale word 0) | Don't reset data that crosses; reset only control state | Remove |
Destination reset leaves a pending request that gets captured again (duplicate) | Acknowledge an old request without capturing it | A destination |
Stale request/ack history survives a reset | Block acceptance until recovery completes | link_ready_s is low throughout reset/recovery, and high only after both sides and synchroniser pipelines are idle |
link_ready_s is synchronous to clk_s. The controller that drives it follows five steps for every reset:
Clear link_ready_s before a planned reset. An unexpected reset must also clear it before any new acceptance.
Stop new traffic. Hold any destination reset until the pending req has reached its synchroniser (at least three clean destination edges in this model).
Release the resets synchronously, each in its own domain.
Let the old handshake finish or cancel.
Raise link_ready_s only when req, req_sync, ack and ack_sync are low, the source is IDLE and the destination is armed.
In hardware this needs synchronised recovery status, or an epoch handshake, from an always-on controller. Seeing ack low once, or a fixed timer in the source domain, is not enough. If either clock stops, keep the link closed. The listing below implements only the acceptance interlock; the recovery-controller pilot at the end of the page sketches the controller itself.
An isolation cell is a gate (AND or OR) on each output of a block held in reset or powered down. An isolate signal clamps the outputs to a safe constant.
Isolate asserts before the block resets and releases after it is stable again.
assign out_to_neighbour = block_out & ~isolate; // clamp to 0 while isolated
// isolate asserted before the block resets, released after it has left reset and is quiescentThe source is still waiting for an acknowledge to complete its handshake. If the destination ignored the request, the source would wait forever (deadlock). Acknowledging without capturing completes the handshake and cancels the word.
The protected handshake under the same random reset experiment. The task reopen_link stands in for the controller: it raises link_ready_s only after three qualified idle samples.
`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_recover #(parameter int W = 8) (
input logic clk, rst_n, link_ready_s,
input logic send_valid,
output logic send_ready,
input logic [W-1:0] send_data,
output logic req,
output logic [W-1:0] data,
input logic ack_sync
);
typedef enum logic [1:0] {IDLE, WAIT_ACK, WAIT_NACK} state_t;
state_t state;
assign send_ready = rst_n && link_ready_s && (state == IDLE);
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin state <= IDLE; req <= 1'b0; end // retain data during recovery
else unique case (state)
IDLE: if (send_valid && send_ready) begin data <= send_data; req <= 1'b1; state <= WAIT_ACK; end
WAIT_ACK: if (ack_sync) begin req <= 1'b0; state <= WAIT_NACK; end
WAIT_NACK: if (!ack_sync) state <= IDLE;
default: state <= IDLE;
endcase
endmodule
module hs_dst #(parameter int W = 8) (
input logic clk, rst_n,
input logic req_sync,
input logic [W-1:0] data,
output logic ack,
output logic out_valid,
output logic [W-1:0] out_data
);
logic armed; // 0 after reset: a request that was already pending is acknowledged but NOT captured again
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin ack <= 1'b0; out_valid <= 1'b0; out_data <= '0; armed <= 1'b0; end
else begin
out_valid <= 1'b0;
if (req_sync && !ack) begin
ack <= 1'b1;
if (armed) begin out_data <= data; out_valid <= 1'b1; end // normal: capture and deliver
end else if (!req_sync) begin
ack <= 1'b0;
armed <= 1'b1; // the old handshake has completed: new requests are real
end
end
endmodule
module hs_cdc_recover #(parameter int W = 8) (
input logic clk_s, rst_s_n, clk_d, rst_d_n, link_ready_s,
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_recover #(W) s (.clk(clk_s), .rst_n(rst_s_n), .link_ready_s, .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
Directed / random case | Required result (all PASS) |
|---|---|
Source reset with old ack high | No acceptance until qualified idle; subsequent traffic resumes |
Destination only (MODE = 0) | No duplicate or stale word under the controller contract |
Source only (MODE = 1) | No duplicate or stale word under the controller contract |
Both together (MODE = 2) | No duplicate or stale word under the controller contract |
Run each MODE separately. MODE 0, 1 and 2 each pass over 1,500 resets (4,276, 4,699 and 4,371 words delivered). This experiment permits cancellation of an interrupted word; it checks no duplication/stale data and eventual progress, not lossless delivery across reset. An exactly-once contract requires transaction identity and replay/deduplication above this link.
The random bench above uses an oracle for the controller. This separate pilot supplies a small always-on controller, recovery_supervisor, and a clocked reporter in each endpoint, recovery_endpoint. One recovery is a fresh low-high-low flush barrier: close both sides, raise flush and wait for both acknowledgements, drop flush and wait for both to fall, then reopen. Neither a stale low acknowledgement nor a reused timer can open a new epoch.
Read the diagram from the top: each arrow is one clock of clk_aon on which the condition, seen through the controller's two-flop synchronisers, is true.
The barrier needs both edges. WAIT_HIGH proves each endpoint saw this flush; WAIT_LOW proves each has finished it. A low ack left over from before the reset satisfies neither.
The timeout counts clk_aon cycles from CLOSE (LIMIT = 100). It covers the whole barrier, so a stopped endpoint clock ends in FAULT.
FAULT never reopens by itself: only recover_i leaves it, even after the stopped clock returns.
A synchronised fault_hold forces CLOSE from every phase (not drawn), and its unsynchronised copy closes each endpoint at once.
Phase | Required evidence |
|---|---|
CLOSE | Both synchronised local open flags low, both old acknowledgements low |
WAIT_HIGH | Each endpoint has observed flush and held its protocol block reset for four local edges |
WAIT_LOW | Flush released; each reporter waits four local edges with idle/armed true before dropping acknowledgement |
OPEN | Fresh barrier complete; open request synchronised into each endpoint |
FAULT | Always-on watchdog expired; keep acceptance closed and require explicit retry after clocks recover |
The bench tb_recovery_barrier checks four cases: the first opening, a planned recovery that closes both sides before a fresh barrier, a stopped destination clock that must end in a closed FAULT, and an explicit retry that reopens once the clock is back.
module recovery_endpoint (
input logic clk, por_n, fault_hold, open_async, flush_async,
input logic local_idle, local_armed,
output logic link_open, block_reset, flush_ack
);
timeunit 1ns; timeprecision 1ps;
(* ASYNC_REG = "TRUE" *) logic [1:0] open_sync, flush_sync;
int clean_edges;
// A held, approved fault-control path closes acceptance even with this clock stopped.
always_ff @(posedge clk or negedge por_n or posedge fault_hold)
if (!por_n || fault_hold) open_sync <= '0;
else open_sync <= {open_sync[0], open_async};
assign link_open = open_sync[1] && !fault_hold;
always_ff @(posedge clk or negedge por_n)
if (!por_n) flush_sync <= '0;
else flush_sync <= {flush_sync[0], flush_async};
// Reporter remains powered/clocked while its protocol block is reset.
always_ff @(posedge clk or negedge por_n) begin
if (!por_n) begin block_reset <= 1; flush_ack <= 0; clean_edges <= 0; end
else if (flush_sync[1]) begin
block_reset <= 1;
if (!flush_ack) begin
if (clean_edges == 3) begin flush_ack <= 1; clean_edges <= 0; end
else clean_edges <= clean_edges + 1;
end
end else begin
block_reset <= 0;
if (flush_ack) begin
if (!local_idle || !local_armed) clean_edges <= 0;
else if (clean_edges == 3) begin flush_ack <= 0; clean_edges <= 0; end
else clean_edges <= clean_edges + 1;
end else clean_edges <= 0;
end
end
endmodule
module recovery_supervisor #(parameter int LIMIT = 100) (
input logic clk_aon, por_n, fault_hold, recover_i,
input logic open_s, open_d, ack_s, ack_d,
output logic flush_req, open_request, faulted
);
timeunit 1ns; timeprecision 1ps;
typedef enum logic [2:0] {CLOSE, WAIT_HIGH, WAIT_LOW, OPEN, FAULT} phase_t;
phase_t phase;
(* ASYNC_REG = "TRUE" *) logic [1:0] os, od, as_q, ad_q, fault_q;
int waited;
initial if (LIMIT < 1) $fatal(1, "Recovery LIMIT must be positive");
always_ff @(posedge clk_aon or negedge por_n) begin
if (!por_n) begin os<=0; od<=0; as_q<=0; ad_q<=0; fault_q<=0; end
else begin
os<={os[0],open_s}; od<={od[0],open_d};
as_q<={as_q[0],ack_s}; ad_q<={ad_q[0],ack_d}; fault_q<={fault_q[0],fault_hold};
end
end
assign flush_req = phase == WAIT_HIGH || phase == FAULT;
assign open_request = phase == OPEN && !fault_hold;
assign faulted = phase == FAULT;
always_ff @(posedge clk_aon or negedge por_n) begin
if (!por_n) begin phase <= CLOSE; waited <= 0; end
else if (fault_q[1]) begin phase <= CLOSE; waited <= 0; end
else if (recover_i && (phase == OPEN || phase == FAULT)) begin phase <= CLOSE; waited <= 0; end
else if (phase != OPEN && phase != FAULT && waited >= LIMIT) phase <= FAULT;
else begin
if (phase == OPEN || phase == FAULT) waited <= 0;
else waited <= waited + 1;
case (phase)
CLOSE: if (!os[1] && !od[1] && !as_q[1] && !ad_q[1]) phase <= WAIT_HIGH;
WAIT_HIGH: if (as_q[1] && ad_q[1]) phase <= WAIT_LOW;
WAIT_LOW: if (!as_q[1] && !ad_q[1]) phase <= OPEN;
OPEN: begin end
FAULT: begin end // no timer-based reopening
default: phase <= FAULT;
endcase
end
end
endmodule
Protected handshake under the random reset experiment, run with Icarus Verilog 13.0 (2026-10-16): MODE 0, 1 and 2 each print PASS over 1,500 resets (4,276, 4,699 and 4,371 words delivered). The experiment permits cancellation of an interrupted word. It checks no duplication, no stale data and eventual progress, not lossless delivery across reset.
The recovery-controller pilot is a standalone bench with abstract idle and armed endpoints. It does not connect the full handshake or prove cancellation or exactly-once delivery. No simulation or physical signoff is claimed for this source.