HomeCDCDay 4 Enhanced

Pulse Synchronizers

Synchronizing single-pulse signals and control edges across clock domains. Edge detection, pulse generation, handshake protocols, complete RTL design, timing analysis, formal verification, and production patterns.

By EcrioniX · Published June 13, 2026 · ~4500 words · 14 min read

1. The Problem: Single Pulses and Control Signals

From Days 1-3, we've covered:

But what about single-pulse control signals? Examples:

The problem: A simple dual-FF on a level signal will synchronize the level, not generate a pulse.

Pulse Synchronization Problem: Clock A: ┌─┐ Input_A: ├─┤ (single pulse, 1 Clock A cycle) └─┘ Naïve dual-FF attempt: Clock B: ┌─────────┐ FF1: ├─────────┤ (stretched across Clock B periods) FF2 (output): ┌─────────┐ (not a pulse, but a level!) └─────────┘ (multiple Clock B cycles) Problem: The input pulse is only 1 Clock A cycle long. But Clock B may be much slower. The pulse gets captured and held for many Clock B cycles. Downstream logic sees a held level, not a pulse. Real issue: We need to DETECT that a pulse occurred, then GENERATE a new pulse in the destination domain.

2. The Pulse Synchronizer Architecture

Strategy 1: Stretch and Detect

The most common pulse synchronization uses level transmission with pulse detection:

  1. Sender (Clock A): Convert the pulse into a toggleing level
  2. Synchronizer: Use a dual-FF to synchronize the level
  3. Receiver (Clock B): Detect when the level toggles, and generate a pulse
Pulse Synchronizer Architecture: Clock A Domain: input_pulse ──→ Toggle_Latch ──→ toggle_level (changes each pulse) Synchronization: toggle_level ──→ FF1 ──→ FF2 ──→ level_sync_b Clock B Domain: level_sync_b & (level_sync_b != level_sync_b_prev) ──→ output_pulse Timeline: Clock A Edge 1: input_pulse = 1 toggle_level toggles: 0 → 1 Clock A Edge 2: input_pulse = 0 toggle_level stays at 1 Clock B Edge 1: FF1 captures toggle_level ≈ 1 May be metastable Clock B Edge 2: FF2 captures FF1 output (resolved) level_sync_b_ff2 = 1 (metastability resolved) Clock B Edge 3: Detect change: level_sync_b_ff2 != level_sync_b_prev → Generate output_pulse for 1 Clock B cycle Clock B Edge 4: level_sync_b_prev updates to 1 No more pulse (until next toggle)

3. RTL Implementation

Complete Pulse Synchronizer Module

module pulse_synchronizer (
  input  clk_a,
  input  rst_a,
  input  pulse_a,      // Single-cycle pulse in Clock A domain

  input  clk_b,
  input  rst_b,
  output pulse_b       // Single-cycle pulse in Clock B domain
);

  // ============ Clock A Domain: Toggle Latch ============
  reg toggle_latch;

  always @(posedge clk_a or negedge rst_a) begin
    if (!rst_a) begin
      toggle_latch <= 1'b0;
    end else if (pulse_a) begin
      toggle_latch <= ~toggle_latch;  // Toggle on each input pulse
    end
  end

  // ============ Synchronization: Dual-FF on Level ============
  reg [1:0] level_sync;  // Two-stage synchronizer

  always @(posedge clk_b or negedge rst_b) begin
    if (!rst_b) begin
      level_sync <= 2'b00;
    end else begin
      level_sync <= {level_sync[0], toggle_latch};  // Shift register
    end
  end

  // ============ Clock B Domain: Edge Detection ============
  reg level_sync_prev;

  always @(posedge clk_b or negedge rst_b) begin
    if (!rst_b) begin
      level_sync_prev <= 1'b0;
    end else begin
      level_sync_prev <= level_sync[1];  // Track previous state
    end
  end

  // Generate output pulse when level changes
  assign pulse_b = (level_sync[1] != level_sync_prev);

endmodule

Key Implementation Points

4. Detailed Timing Analysis

Pulse Synchronizer Timing @ 1 GHz & 500 MHz

Scenario: Clock A = 1 GHz (1ns period), Clock B = 500 MHz (2ns period) Timeline: T=0ns (Clock A): pulse_a goes high T=0ns: toggle_latch toggles (0 → 1) immediately T=1ns (Clock A): pulse_a goes low T=1ns: toggle_latch stays at 1 T=2ns (Clock B): FF1 samples toggle_latch = 1 T=2ns: FF1 output may be metastable T=4ns (Clock B): FF2 samples FF1 output T=4ns: level_sync[1] = 1 (metastability resolved) T=4ns: level_sync[1] != level_sync_prev (0 ≠ 1) T=4ns: pulse_b = 1 for exactly 1 Clock B cycle T=6ns (Clock B): level_sync_prev updates to 1 T=6ns: pulse_b = 0 (no more pulse)

5. Real-World Use Cases

Interrupt Across Domains

System-on-Chip with multiple clock domains: Peripheral (Clock A) signals interrupt to processor (Clock B).

Reset Distribution

Asynchronous system reset must be synchronized into all clock domains. A pulse synchronizer ensures each domain gets a single reset pulse.

Hand-Shake Protocols

Two subsystems in different clock domains need to coordinate:

6. Advanced Variant: Multi-Pulse Handling

What if multiple pulses arrive before the previous pulse is synchronized across?

The toggle latch naturally handles this: Each new pulse toggles the level again. The edge detector in Clock B will see another toggle and generate another pulse (when it arrives).

However, if pulses arrive faster than the synchronization latency (2-3 Clock B cycles), some toggling might not be detected.

Maximum pulse frequency = f_clk_B / (2 + synchronization_latency) For Clock B = 1 GHz, latency = 3 cycles: Max pulse frequency = 1 GHz / 5 = 200 MHz Practical rule: Keep pulse input frequency << Clock B / 4 to avoid missing pulses

7. Comparison: Three Synchronization Techniques

Technique Use Case Input Type Output Type Design Complexity
Dual-FF Single-bit level (req, ack, etc.) Level (0 or 1) Level (0 or 1) Simple (2 FFs)
Gray Code Multi-bit counters Binary counter Binary counter Medium (encode/decode)
Pulse Sync Single pulses, interrupts Pulse (1 clock cycle) Pulse (1 clock cycle) Medium (toggle + edge detect)

8. Common Mistakes

9. Summary & Checklist

Next (Day 5): Dual-clock FIFO — integrating Gray code synchronization with FIFO control logic.