· FPGA  · 8 min read

Mastering External Signal Synchronization in FPGA Design

In the world of FPGA design, few challenges are as fundamental yet critical as handling external asynchronous signals.

In the world of FPGA design, few challenges are as fundamental yet critical as handling external asynchronous signals.

In the world of FPGA design, few challenges are as fundamental yet critical as handling external asynchronous signals. Whether it’s a simple push button, a sensor input, or a communication interface, external signals often arrive at unpredictable times relative to our internal clock domains. This unpredictability can lead to metastability—a condition where flip-flops enter an undefined state, potentially causing system failures.

Today, we’ll explore a comprehensive lab that demonstrates proper external signal synchronization techniques. This lab showcases real-world solutions for clock domain crossing (CDC) and debouncing, using a practical button-to-LED interface on the ARTY-Z7-20 development board.

The code for the lab is available on GitHub.

The Problem: Asynchronous External Signals

Why External Signals Are Problematic

External signals pose several challenges in digital design:

  1. Unknown Timing: External signals have no relationship to internal clock domains
  2. Metastability: When a signal changes near a clock edge, flip-flops can enter undefined states
  3. Mechanical Bounce: Physical switches create multiple transitions before settling
  4. Noise: External signals are susceptible to electromagnetic interference

The Metastability Challenge

When a signal changes near a clock edge, the flip-flop may enter a metastable state where the output is neither clearly high nor low. This can propagate through the system, causing unpredictable behavior.

The Solution: Multi-Stage Synchronization

Clock Domain Crossing (CDC) Synchronizer

The key to handling asynchronous signals is the multi-stage synchronizer:

module cdc_sync #(
    parameter integer N     = 2,
    parameter integer WIDTH = 1
) (
    input  logic             clk,
    input  logic [WIDTH-1:0] din,
    output logic [WIDTH-1:0] dout
);

  (* ASYNC_REG = "TRUE", SILISCALE_CDC = "TRUE" *)
  logic [WIDTH-1:0] sync_reg[N-1:0];

  always @(posedge clk) begin
    for (int i = 0; i < N; i++) begin
      if (i == 0) begin
        sync_reg[i] <= din;
      end else begin
        sync_reg[i] <= sync_reg[i-1];
      end
    end
  end

  assign dout = sync_reg[N-1];

endmodule

How the Synchronizer Works

The synchronizer works by registering the input signal on the clock edge. The number of stages determines the MTBF (Mean Time Between Failures) of the synchronizer. The more stages, the higher the MTBF. More than 3 stages is overkill for most applications.

The key aspect here is the ASYNC_REG attribute. This attribute tells the synthesis tool that these registers can handle an asynchronous input on the D input of the flop. For more info, visit the Xilinx UG901.

Key Features:

  • ASYNC_REG Attribute: Tells synthesis tools these are synchronization registers
  • Configurable Depth: More stages = higher MTBF (Mean Time Between Failures)
  • Configurable Width: Handles multi-bit signals

Mean Time Between Failures (MTBF)

The probability of metastability failure decreases exponentially with each synchronization stage:

MTBF ∝ e^(N × τ)

Where:

  • N = number of synchronization stages
  • τ = time constant related to flip-flop characteristics

The Lab Implementation

System Architecture

Clock Management

Internal Domain

External Domain

Button Input
125MHz Domain

CDC Synchronizer
3 Stages

Debouncer
100 Stages

LED Output

MMCM Clock Generator
125MHz → 100MHz

Reset Synchronizer

Top-Level Design

module fpga_top (
    input logic SYS_CLK,    // 125MHz system clock
    input  logic BTN0,      // External button
    output logic LD0        // LED output
);

  logic clk_100mhz;
  logic rstn_100mhz;
  logic btn0_sync;

  // Clock and reset subsystem
  clk_rst_subsys clk_rst_subsys_i (
      .SYS_CLK    (SYS_CLK),
      .clk_100mhz (clk_100mhz),
      .rstn_100mhz(rstn_100mhz)
  );

  // CDC Synchronizer (3 stages for high MTBF)
  cdc_sync #(
      .N(3),
      .WIDTH(1)
  ) cdc_sync_btn0_i (
      .clk (clk_100mhz),
      .din (BTN0),
      .dout(btn0_sync)
  );

  // Debouncer implementation
  localparam DEBOUNCE_FLOPS = 100;
  logic [DEBOUNCE_FLOPS-1:0] btn0_debounced;

  // 100-stage shift register for debouncing
  genvar i;
  for (i = 0; i < DEBOUNCE_FLOPS; i++) begin : gen_debounce_flops
    if (i == 0) begin
      register_sync_rstn #(.WIDTH(1)) register_sync_rstn_i (
          .clk (clk_100mhz),
          .rstn(rstn_100mhz),
          .din (btn0_sync),
          .dout(btn0_debounced[i])
      );
    end else begin
      register_sync_rstn #(.WIDTH(1)) register_sync_rstn_i (
          .clk (clk_100mhz),
          .rstn(rstn_100mhz),
          .din (btn0_debounced[i-1]),
          .dout(btn0_debounced[i])
      );
    end
  end

  // LED activates when all 100 stages are high
  assign ld0_i = &btn0_debounced;

  // Output buffer
  OBUF obuf_ld0_i (
      .I(ld0_i),
      .O(LD0)
  );

endmodule

Clock Management: The Heart of the System

MMCM Clock Generator

The clock subsystem converts the 125MHz system clock to a 100MHz internal clock:

module clk_rst_subsys (
    input  logic SYS_CLK,
    output logic clk_100mhz,
    output logic rstn_100mhz
);

  // MMCM configuration for 125MHz → 100MHz conversion
  MMCME2_BASE #(
      .BANDWIDTH("OPTIMIZED"),
      .CLKFBOUT_MULT_F(8),      // VCO = 125MHz × 8 = 1GHz
      .DIVCLK_DIVIDE(1),
      .CLKIN1_PERIOD(8.0),      // 125MHz input
      .CLKOUT0_DIVIDE_F(10),    // 1GHz ÷ 10 = 100MHz
      // ... other parameters
  ) MMCME2_BASE_i (
      .CLKIN1   (SYS_CLK),
      .CLKOUT0  (clk_100mhz_mmcm),
      .LOCKED   (locked_mmcm),
      .CLKFBOUT (clkfbout_mmcm),
      .CLKFBIN  (clkfbout_mmcm)
  );

  // Synchronize the MMCM lock signal
  cdc_sync #(.N(2), .WIDTH(1)) cdc_sync_locked_i (
      .clk (clk_100mhz),
      .din (locked_mmcm),
      .dout(rstn_100mhz)
  );

endmodule

Clock Domain Relationships

Internal Domain

MMCM

External Domain

125MHz System Clock

VCO: 1GHz

Feedback Loop

100MHz Internal Clock

Debouncing: Filtering Mechanical Noise

The Mechanical Bounce Problem

When a mechanical switch is pressed, it doesn’t make a clean transition:

Switch Press

Initial Contact

Bounce 1

Bounce 2

Bounce 3

Stable High

Shift Register Debouncing

Our implementation uses a 100-stage shift register:

Logic

100-Stage Shift Register

Stage 0

Stage 1

Stage 2

...

Stage 99

AND Reduction
All stages must be high

Why 100 Stages?

  • At 100MHz, each stage represents 10ns
  • 100 stages = 1μs debounce time
  • Mechanical bounce typically lasts 1-10ms
  • Conservative approach ensures reliable operation

Debouncing Logic

// LED activates only when ALL 100 stages are high
assign ld0_i = &btn0_debounced;

This AND reduction ensures the LED only illuminates when the button has been stably pressed for the full debounce period.

Implementation and Verification

Project Setup

# Clone the repository
git clone <this-repo>
cd <this-repo>

# Set environment variable
export REPO_TOP_DIR=$(pwd)

# Initialize SVLib submodule
git submodule update --init --recursive

# Create Vivado project
vivado -mode batch -source tcl/proj.tcl -notrace

Testbench Verification

module fpga_top_tb;

  logic SYS_CLK = 0;
  logic BTN0;
  logic LD0;

  fpga_top fpga_top_i (
      .SYS_CLK(SYS_CLK),
      .BTN0(BTN0),
      .LD0(LD0)
  );

  // Generate 125MHz clock (8ns period)
  always #4 SYS_CLK = ~SYS_CLK;

  initial begin
    BTN0 = 1'b0;

    // Wait for system to stabilize
    for (int i = 0; i < 100; i++) begin
        @(posedge SYS_CLK);
    end

    // Simulate button press
    BTN0 = 1'b1;

    // Run simulation
    #10000;
    $finish;
  end

endmodule

Timing Analysis

The design must meet several timing constraints:

# Clock constraints
create_clock -name SYS_CLK_PIN -period 8.00 -waveform {0 4} [get_ports { SYS_CLK }]

# Pin assignments
set_property PACKAGE_PIN H16 [get_ports { SYS_CLK }]  # 125MHz system clock
set_property PACKAGE_PIN D19 [get_ports { BTN0 }]     # Button input
set_property PACKAGE_PIN R14 [get_ports { LD0 }]      # LED output

Best Practices Demonstrated

1. Proper CDC Implementation

  • Multi-stage synchronization: 3 stages for high MTBF
  • ASYNC_REG attributes: Proper synthesis guidance
  • Reset synchronization: Clean reset across domains

2. Robust Debouncing

  • Shift register approach: Simple, reliable, synthesizable
  • Conservative timing: 1μs debounce time
  • AND reduction logic: Ensures stable detection

3. Clock Management

  • MMCM usage: Professional clock generation
  • Feedback loops: Proper VCO configuration
  • Lock monitoring: Synchronized reset generation

4. Design Organization

  • Modular structure: Reusable components
  • Clear interfaces: Well-defined module boundaries
  • Comprehensive verification: Testbench coverage

Real-World Applications

This lab demonstrates techniques used in countless real-world applications:

Industrial Control Systems

  • Safety interlocks: Reliable button and switch handling
  • Sensor interfaces: Analog-to-digital conversion synchronization
  • Communication protocols: UART, SPI, I2C interface handling

Consumer Electronics

  • User interfaces: Button debouncing in remote controls
  • Touch sensors: Capacitive touch synchronization
  • Audio interfaces: Sample rate conversion

Automotive Systems

  • CAN bus interfaces: Multi-domain communication
  • Sensor fusion: Multiple sensor synchronization
  • Safety systems: Critical signal handling

Performance Analysis

Resource Utilization

  • LUTs: ~200 (primarily for debouncing shift register)
  • Flip-flops: ~105 (100 for debouncing + 5 for synchronization)
  • MMCM: 1 (clock generation)
  • Clock domains: 2 (125MHz external, 100MHz internal)

Timing Performance

  • Maximum frequency: 100MHz internal clock
  • Debounce time: 1μs (configurable via parameter)
  • CDC reliability: MTBF > 10^9 years with 3-stage synchronizer

Troubleshooting Guide

Common Issues and Solutions

1. CDC Violations

Problem: Vivado reports CDC violations Solution: Ensure ASYNC_REG attributes are set on synchronization registers

2. Timing Failures

Problem: Setup/hold timing violations Solution: Check MMCM configuration and clock constraints

3. Debouncing Issues

Problem: LED flickers or doesn’t respond Solution: Increase DEBOUNCE_FLOPS parameter for longer debounce time

4. Clock Lock Issues

Problem: System doesn’t start properly Solution: Verify MMCM configuration and input clock frequency

Advanced Topics

Multi-Bit CDC

For multi-bit signals, consider using:

  • Gray code encoding: Ensures only one bit changes at a time
  • Handshake protocols: For complex data transfers
  • FIFO-based synchronization: For streaming data

Alternative Debouncing Methods

  • Counter-based: More efficient for long debounce times
  • State machine: More complex but flexible
  • Analog filtering: RC circuits for simple applications

CDC Verification Tools

  • Vivado CDC Analysis: Built-in CDC checking
  • Formal verification: Mathematical proof of correctness
  • Simulation: Extensive testing with various timing scenarios

Conclusion

This lab demonstrates fundamental techniques for handling external asynchronous signals in FPGA designs. The combination of proper CDC synchronization, robust debouncing, and professional clock management creates a reliable foundation for any system that interfaces with the external world.

The key takeaways are:

  1. Always synchronize external signals using multi-stage synchronizers
  2. Debounce mechanical inputs to filter noise and bounce
  3. Use proper clock management with MMCM/PLL components
  4. Follow synthesis guidelines with appropriate attributes
  5. Verify thoroughly with simulation and timing analysis

These techniques form the backbone of reliable digital systems and are essential knowledge for any FPGA designer working with real-world interfaces.

Back to Blog

Related Posts

View All Posts »
Xilinx QDMA IP Core Tutorial

Xilinx QDMA IP Core Tutorial

In this article, we will explore how to use the Xilinx QDMA IP core to create a PCIe device and access it over Linux.