Source locks data onto the bus and asserts req_a ↑. Data is now stable and will not change until the full handshake completes.
After 2-FF sync delay, req_sync_b ↑ is seen in CLK_B domain. Destination captures data into its local register, then asserts ack_b ↑.
After 2-FF sync delay, ack_sync_a ↑ is seen in CLK_A domain. Transfer is confirmed. Source de-asserts req_a ↓. Data may now change.
After 2-FF sync delay, req_sync_b ↓ is seen. Destination de-asserts ack_b ↓. System is now reset — ready for the next transfer.
| Port | Width | Domain | Dir | Description |
|---|---|---|---|---|
| clk_a | 1 | CLK_A | IN | Source clock — rising-edge active |
| rst_a_n | 1 | CLK_A | IN | Active-low synchronous reset in source domain |
| send_req | 1 | CLK_A | IN | Pulse high for 1 cycle to initiate a transfer. Ignored while busy |
| data_a | N | CLK_A | IN | Data to transfer — must be stable from send_req until transfer_done |
| transfer_done | 1 | CLK_A | OUT | High for 1 CLK_A cycle when ACK handshake completes |
| busy | 1 | CLK_A | OUT | High while a transfer is in progress |
| clk_b | 1 | CLK_B | IN | Destination clock — rising-edge active |
| rst_b_n | 1 | CLK_B | IN | Active-low synchronous reset in destination domain |
| data_b | N | CLK_B | OUT | Captured data in destination domain — valid when data_valid_b is high |
| data_valid_b | 1 | CLK_B | OUT | High for 1 CLK_B cycle when data_b has been captured from source |
module req_ack_sync #( parameter DATA_W = 8 ) ( // Source domain (CLK_A) input wire clk_a, rst_a_n, input wire send_req, input wire [DATA_W-1:0] data_a, output reg transfer_done, output wire busy, // Destination domain (CLK_B) input wire clk_b, rst_b_n, output reg [DATA_W-1:0] data_b, output reg data_valid_b ); // ── Source FSM states ─────────────────────────────────────────── localparam [1:0] S_IDLE=2'd0, S_ASSERT=2'd1, S_WAIT=2'd2, S_DEASSERT=2'd3; reg [1:0] src_state; // ── Destination FSM states ────────────────────────────────────── localparam [1:0] D_IDLE=2'd0, D_CAP=2'd1, D_ACK=2'd2, D_DEACK=2'd3; reg [1:0] dst_state; // ── Raw handshake signals ─────────────────────────────────────── reg req_a; // driven by source FSM reg ack_b; // driven by dest FSM reg [DATA_W-1:0] data_latch; // locked at send_req, released after done // ── 2-FF synchronizer: req_a → CLK_B domain ──────────────────── reg ff1_req_b, ff2_req_b; // synthesis attribute: async_reg always @(posedge clk_b or negedge rst_b_n) if (!rst_b_n) {ff1_req_b, ff2_req_b} <= 2'b0; else {ff2_req_b, ff1_req_b} <= {ff1_req_b, req_a}; wire req_sync_b = ff2_req_b; // ── 2-FF synchronizer: ack_b → CLK_A domain ──────────────────── reg ff1_ack_a, ff2_ack_a; // synthesis attribute: async_reg always @(posedge clk_a or negedge rst_a_n) if (!rst_a_n) {ff1_ack_a, ff2_ack_a} <= 2'b0; else {ff2_ack_a, ff1_ack_a} <= {ff1_ack_a, ack_b}; wire ack_sync_a = ff2_ack_a; // ── Source FSM (CLK_A) ────────────────────────────────────────── assign busy = (src_state != S_IDLE); always @(posedge clk_a or negedge rst_a_n) begin if (!rst_a_n) begin src_state <= S_IDLE; req_a <= 0; data_latch <= 0; transfer_done <= 0; end else begin transfer_done <= 0; case (src_state) S_IDLE: if (send_req) begin data_latch <= data_a; req_a <= 1; src_state <= S_ASSERT; end S_ASSERT: src_state <= S_WAIT; // let req_a propagate 1 cycle S_WAIT: if (ack_sync_a) begin req_a <= 0; src_state <= S_DEASSERT; end S_DEASSERT: if (!ack_sync_a) begin // wait for ACK to fall transfer_done <= 1; src_state <= S_IDLE; end endcase end end // ── Destination FSM (CLK_B) ───────────────────────────────────── always @(posedge clk_b or negedge rst_b_n) begin if (!rst_b_n) begin dst_state <= D_IDLE; ack_b <= 0; data_b <= 0; data_valid_b <= 0; end else begin data_valid_b <= 0; case (dst_state) D_IDLE: if (req_sync_b) begin data_b <= data_latch; // data_latch is stable here dst_state <= D_CAP; end D_CAP: begin data_valid_b <= 1; ack_b <= 1; dst_state <= D_ACK; end D_ACK: if (!req_sync_b) begin // REQ has fallen (source de-asserted) dst_state <= D_DEACK; end D_DEACK: begin ack_b <= 0; dst_state <= D_IDLE; end endcase end end endmodule
Each bit resolves independently — you can capture a half-old, half-new bus. Only sync single-bit REQ and ACK.
Data must be stable on the bus before req_a goes high and must not change until transfer_done is received.
Never reduce to a 1-FF chain to save area. One extra cycle is the MTBF insurance — removing it causes failures at scale.
Annotate the 2-FF chain registers with ASYNC_REG / async_reg so the tool keeps them close in placement and doesn't retime them.
One word every ~10+ cycles. For bulk streaming data, use a Gray-coded async FIFO — Req-Ack is not designed for continuous flow.
Req-Ack works correctly regardless of CLK_A vs CLK_B frequency ratio — including when one is much faster or completely asynchronous.