Why a Simple Button Press Can Crash Your FPGA System (and How to Fix It)

4 months ago 7

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.

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

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 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

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

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

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

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

Our implementation uses a 100-stage shift register:

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

// 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.

# 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 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

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
  • Multi-stage synchronization: 3 stages for high MTBF

  • ASYNC_REG attributes: Proper synthesis guidance

  • Reset synchronization: Clean reset across domains

  • Shift register approach: Simple, reliable, synthesizable

  • Conservative timing: 1μs debounce time

  • AND reduction logic: Ensures stable detection

  • MMCM usage: Professional clock generation

  • Feedback loops: Proper VCO configuration

  • Lock monitoring: Synchronized reset generation

  • Modular structure: Reusable components

  • Clear interfaces: Well-defined module boundaries

  • Comprehensive verification: Testbench coverage

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

  • Safety interlocks: Reliable button and switch handling

  • Sensor interfaces: Analog-to-digital conversion synchronization

  • Communication protocols: UART, SPI, I2C interface handling

  • User interfaces: Button debouncing in remote controls

  • Touch sensors: Capacitive touch synchronization

  • Audio interfaces: Sample rate conversion

  • CAN bus interfaces: Multi-domain communication

  • Sensor fusion: Multiple sensor synchronization

  • Safety systems: Critical signal handling

  • 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)

  • Maximum frequency: 100MHz internal clock

  • Debounce time: 1μs (configurable via parameter)

  • CDC reliability: MTBF > 10^9 years with 3-stage synchronizer

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

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

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

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

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

  • Counter-based: More efficient for long debounce times

  • State machine: More complex but flexible

  • Analog filtering: RC circuits for simple applications

  • Vivado CDC Analysis: Built-in CDC checking

  • Formal verification: Mathematical proof of correctness

  • Simulation: Extensive testing with various timing scenarios

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.

Original post published here https://siliscale.com/ext-sig-sync

Discussion about this post

Read Entire Article