HomeCDC GuideDay 4
DAY 4 · CDC FUNDAMENTALS

Pulse Synchronizers & Handshake Protocols

By EcrioniX · Updated Jun 13, 2026

The 2-FF synchronizer is great for stable signals, but what about narrow pulses? A pulse might last only 1 or 2 clock cycles in the source domain — the receiving clock might miss it entirely. Pulse synchronizers and handshake protocols solve this by stretching events and coordinating transfers across clock boundaries.

1. The problem: narrow pulses and the 2-FF synchronizer

Imagine a 1-cycle pulse on event_A in clock domain A. When it arrives at a 2-FF synchronizer in domain B:

This is catastrophic for control signals: a request that never arrives will hang the system.

⚠️ Never use a raw 2-FF sync on pulses

If your signal is a 1-cycle pulse, you must use a pulse synchronizer or handshake protocol. The 2-FF synchronizer alone cannot guarantee narrow pulses will be captured.

2. Pulse synchronizer: stretching the pulse

A simple pulse synchronizer stretches a narrow pulse into a wide pulse (typically several clock cycles) before crossing, so the receiving clock definitely sees it:

Pulse Synchronizer: Stretching 1-Cycle Pulse to Multi-Cycle pulse_in (clk_a) pulse stretched (still clk_a) wide pulse pulse_sync (clk_b) captured!
Figure — Pulse is widened in source domain, ensuring receiving clock captures it across the 2-FF sync.
pulse_sync.sv
// Pulse Synchronizer
// Input: 1-cycle pulse on pulse_in (clk_a)
// Output: Synchronized pulse on pulse_out (clk_b)
module pulse_sync (
  input  clk_a, rst_n_a,
  input  pulse_in,       // 1-cycle pulse in domain A
  input  clk_b, rst_n_b,
  output pulse_out       // Synchronized pulse in domain B
);

  wire pulse_stretched;  // Stretched version in domain A
  reg  stretch_en;       // Stretch enable
  reg  [2:0] stretch_cnt;

  // Stretch pulse in source domain (3 cycles)
  always @(posedge clk_a or negedge rst_n_a) begin
    if (!rst_n_a) begin
      stretch_cnt <= 0;
      stretch_en  <= 0;
    end else if (pulse_in) begin
      stretch_cnt <= 3;  // Stretch for 3 cycles
      stretch_en  <= 1;
    end else if (stretch_cnt > 0) begin
      stretch_cnt <= stretch_cnt - 1;
      stretch_en  <= 1;
    end else begin
      stretch_en  <= 0;
    end
  end

  assign pulse_stretched = stretch_en;

  // Sync stretched pulse to domain B with 2-FF sync
  wire pulse_sync_ff1, pulse_sync_ff2;
  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b)
      pulse_sync_ff1 <= 0;
    else
      pulse_sync_ff1 <= pulse_stretched;
  end

  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b)
      pulse_sync_ff2 <= 0;
    else
      pulse_sync_ff2 <= pulse_sync_ff1;
  end

  // Detect rising edge in domain B
  reg pulse_sync_ff2_r;
  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b)
      pulse_sync_ff2_r <= 0;
    else
      pulse_sync_ff2_r <= pulse_sync_ff2;
  end

  assign pulse_out = pulse_sync_ff2 & ~pulse_sync_ff2_r;

endmodule

3. Handshake protocols: req-ack and valid-ready

For more complex transactions (especially when data must cross), handshake protocols provide reliable flow control:

Request-Acknowledge (req-ack) protocol:

  1. Sender asserts req and places data on output bus
  2. Receiver samples req, captures data, and asserts ack
  3. Sender sees ack and deasserts req
  4. Receiver sees req deassert and deasserts ack

The handshake ensures the receiver has captured data before the sender releases it. This is atomic: no data can be lost or duplicated.

handshake_req_ack.sv
// Handshake: Sender (req-side, domain A)
module handshake_sender #(parameter DWIDTH = 8) (
  input clk_a, rst_n_a,
  input [DWIDTH-1:0] data_out,
  input data_valid,     // User says: send this data
  output reg req_a,     // Request signal
  input ack_sync        // Ack from receiver (synced to domain A)
);

  always @(posedge clk_a or negedge rst_n_a) begin
    if (!rst_n_a)
      req_a <= 0;
    else if (data_valid)
      req_a <= 1;          // Assert request
    else if (ack_sync)
      req_a <= 0;          // Deassert when ack received
  end
endmodule

// Handshake: Receiver (ack-side, domain B)
module handshake_receiver #(parameter DWIDTH = 8) (
  input clk_b, rst_n_b,
  input [DWIDTH-1:0] data_in,
  input req_sync,       // Request (synced to domain B)
  output reg ack_b,     // Acknowledge signal
  output reg [DWIDTH-1:0] data_captured,
  output data_valid_b   // Output: data is ready
);

  reg req_sync_r;
  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b) begin
      req_sync_r <= 0;
      ack_b <= 0;
      data_captured <= 0;
    end else begin
      req_sync_r <= req_sync;

      // Detect rising edge on req_sync
      if (req_sync & ~req_sync_r) begin
        data_captured <= data_in;  // Capture data
        ack_b <= 1;                // Assert ack
      end else if (~req_sync & req_sync_r) begin
        ack_b <= 0;                // Deassert ack when req goes low
      end
    end
  end

  assign data_valid_b = ack_b;
endmodule

// Typical instantiation:
// - req_a synced to domain B with 2-FF: req_sync_b
// - ack_b synced to domain A with 2-FF: ack_sync_a
// - data_out tied to same bus in both domains during handshake

4. Toggle synchronizer (alternative to pulse sync)

A toggle synchronizer is another way to safely cross pulses. Instead of stretching, it toggles a bit on each event, then detects the toggle in the receiving domain:

  • Event in domain A → toggle a bit
  • Sync toggled bit to domain B (2-FF)
  • Domain B detects edge via XOR with previous value

This is more elegant than stretching and works well when clock frequencies are very different.

toggle_sync.sv
// Toggle Synchronizer
module toggle_sync (
  input clk_a, rst_n_a,
  input event_in,        // Pulse or event in domain A
  input clk_b, rst_n_b,
  output pulse_out       // Synchronized pulse in domain B
);

  // Toggle on each event (domain A)
  reg toggle_a;
  always @(posedge clk_a or negedge rst_n_a) begin
    if (!rst_n_a)
      toggle_a <= 0;
    else if (event_in)
      toggle_a <= ~toggle_a;  // Toggle
  end

  // Sync toggle to domain B (2-FF)
  reg toggle_sync_ff1, toggle_sync_ff2;
  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b) begin
      toggle_sync_ff1 <= 0;
      toggle_sync_ff2 <= 0;
    end else begin
      toggle_sync_ff1 <= toggle_a;
      toggle_sync_ff2 <= toggle_sync_ff1;
    end
  end

  // Detect edge in domain B
  reg toggle_prev;
  always @(posedge clk_b or negedge rst_n_b) begin
    if (!rst_n_b)
      toggle_prev <= 0;
    else
      toggle_prev <= toggle_sync_ff2;
  end

  // Output pulse = edge detected
  assign pulse_out = toggle_sync_ff2 ^ toggle_prev;

endmodule

5. When to use each technique

TechniqueBest forProsCons
2-FF SyncStable signals, low-frequency changesSimple, low latencyLoses narrow pulses
Pulse Stretcher1-cycle pulses, eventsEasy to understand, stretches before syncAdds latency, fixed stretch duration
Toggle SyncPulses, events, variable clock ratesElegant, handles any pulse widthSlightly more logic
Req-Ack HandshakeData transactions, flow controlAtomic, works with multi-bit dataSlower (roundtrip latency)
Valid-ReadyStreaming data, pipelined crossingsFlexible, supports backpressureMore complex to implement

✅ Rule of thumb

Single-bit pulse? Use toggle synchronizer or pulse stretcher. Data + flow control? Use req-ack handshake. Streaming data? Use valid-ready or dual-clock FIFO (Day 5).

🎯 Day 4 takeaways

  • Pulse problem: 2-FF synchronizer can miss 1-cycle pulses if clock B samples between edges
  • Pulse stretcher: Widen pulse in source domain before sync, detect edge in receiving domain
  • Toggle synchronizer: Toggle a bit on each event, sync the toggled value, detect edges via XOR
  • Handshake (req-ack): Sender asserts req, receiver acks, sender deasserts req when ack seen
  • Atomic transactions: Handshakes ensure data is captured before sender releases it
  • Latency trade-off: Pulse sync: low latency, Handshake: higher latency but coordinated
  • Implementation: Use library CDC modules when available (Synopsys DesignWare, foundry cells)

FAQ

What is a pulse synchronizer?

A circuit that stretches a narrow pulse into a wide pulse before crossing clock domains. The wide pulse is guaranteed to be seen by the receiving clock, preventing event loss.

When should I use pulse synchronizer vs handshake?

Pulse sync: for simple events that don't require acknowledgment or feedback. Handshake: for transactions where sender must know the receiver captured the data.

What is a toggle synchronizer?

A synchronizer that toggles a bit on each event, then syncs the toggled bit to the other domain. The receiving clock detects edges via XOR. It's more elegant and flexible than pulse stretching.

How do req-ack handshakes ensure atomicity?

The handshake creates a guaranteed sequence: sender puts data, asserts req; receiver captures, asserts ack; sender deasserts req only after seeing ack. This prevents race conditions and duplicates.

Can I use pulse synchronizer for multi-bit data?

Pulse sync is best for single-bit events. For multi-bit data, use a handshake (req-ack) or dual-clock FIFO (Day 5).

What's the difference between valid-ready and req-ack?

Valid-ready is for unidirectional pipelined flow (source says valid=1, sink must be ready). Req-ack is bidirectional handshake (initiator req, responder ack). Both are CDC-safe when properly synchronized.