Implementing a Clock Boundary Synchronizer in Verilog

In this post we will explore a Verilog description of a clock synchronizer. From your introduction to digital electronics, you may recall the concept of metastability. This is an undesirable condition where a digital signal enters an indeterminate state; neither one nor zero. The purpose of the clock boundary synchronizer is to mitigate this metastability.

To better understand the synchronizer logic, let’s start by answering a few common questions:

What is a clock boundary?

Many logic designs are designed to be synchronous which implies that all logic moves with the same heartbeat. In an FPGA this is implemented as a series of registers. Data is often transferred between registers on the rising edge of the clock. All logic that is synchronous with this single clock is said to be in the same clock domain. There may be multiple domains within an FPGA. There may also be domains outside the FPGA such as an external microcontroller.

What is Metastability?

One of the primary reasons to maintain clear clock boundaries is to prevent a metastable condition; a condition where a register (flip-flop) enters an intermediate state. This is mitigated for a single clock domain as all elements transition at the same time such as on rising edge of clock. At this point, all elements have a period of time to settle before the next rising edge occurs. Provided the gate propagation delays aren’t excessive, the chance of metastability is reduced.

What is a Synchronizer?

A challenging situation arises when signals cross clock boundaries. These boundaries are like two musical instruments that are slightly out of tune. The edges slide relative to each other. Sometimes the rising edges are coincident and other times, they are far apart. The metastability problem occurs when the signals between boundaries are close to each other. This is a hazard because a register in the receiving clock domain does not have time to stabilize before it is read. In extreme cases, this instability can cascade across the clock domain causing unpredictable operation.

Block diagram of a synchronizer

A block diagram representation of the Verilog implementation of a synchronizer is included as Figure 1. This is a classic design where the asynchronous signal flows through the registers. Each register is clocked by the master clock for the new domain. Stated another way, the asynchronous signal is made synchronous in the new domain. Here the number of registers is a design compromise. It’s a question of statistics. More synchronizing registers will reduce the chance of metastability. However, an overly complicated design will consume precious FPGA fabric and reduce speed.

Figure 1: Synchronizing chain conditions a signal as it crosses a clock boundary.

Verilog parameters for the synchronizer module

At this point we will explore some of the highlights of the Verilog code which is attached to the end of this note. The first thing you will notice an ASCII art representation of the Synchronizer included here as Figure 2. This is followed by a sample instantiation and a header that provides a brief description of all input and output signals.

Figure 2: ASCII art representation of the synchronizer.

In the middle of Figure 2, you will notice a parameters and defaults section. Parameters are one of the great hidden gems of Verilog. They allow you to modify the operation of a (Register Transfer Level) RTL design as they are instantiated. For every parameter, a default value is defined. This may be overridden if desired. In this example, the parameter determines the number of flip-flops included in the synchronizer. It has a default of two and as seen in this sample instantiation, it remains at two.

//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip-flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//

This line of Verilog code declares and initializes the synchronous flip-flops:

reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}};  // init with zeros

This is a single register with a width determined by [SYNC_STAGES-1:0]. For example, with the SYNC_STAGES parameter is set to 2 the vector is understood as [1:0]. The {SYNC_STAGES{0’b0}}; statement is Verilog replication operator. It is a clever way of initializing each element of the sync_reg flip-flops with a binary zero.

This may not seem like a flip-flop. However, the FPGA synthesis tools will certainly interpret the description as a cascade of flip-flops as show in the VIVADO schematic representation of the synthesizer captured in Figure 3.

In this particular example, defining a register and then using a shift operation as seen in the next code snippet allows easy expansion of the circuit. It uses a parameter to determine the number of flip-flops.

Figure 3: VIVADO generated schematic of the synchronizer circuit.

The synchronizing chain and the rise / fall detectors are implemented inside an always @(posedge clk) block. Note the use of non-blocking code. Everything in this always block occurs at the same time. This is synchronous operation is a critical consideration for this design.

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];
        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
    end

The operation (sensitivity) is controlled by the positive edge of the master clock associated with the receiving domain. A Verilog concatenation operator performs the equivalent of a microcontroller’s shift left operation. The left most bit is discarded, and the latest asynchronous input is placed into the lowest bit position.

Observe that each vector and bit operation is dependent on the value of the SYNC_STAGES parameter. Consequently, the number of synchronizing flip-flops is easily changed with no modification to the module. These changes are made as the module is instantiated. As designed, they cannot be changed at run time.

The sync_out for the module is the derived from the leftmost bit in the shift register. Again, notice that this is non-blocking code. In this example, there are two synchronizers and an output register. The sync_out value is locked in on the rising edge of the 3rd clock pulse.

The rising and falling edge detector are constructed using the last two lines of code. Here we use the ternary operator ? : . This is a Verilog shorthand way of implementing an if else statement. The statements may be understood by referring to Figure 1. Observe that the edge trigger block monitors the output reg (last out) and the soon to be output. On the rising edge of clock, the block determines if the value of the output register has changed. It they are different, and the new output is positive, there is a rising edge. Same thought for falling edge.

Testbench

Let’s shift focus and explore the performance of the RTL. This included the testbench results as well as real world performance. This hardware was developed on the VIVADO platform for a Xilinx Artix FPGA as installed on a Digilent Basys 3. More on that topic another day.

The testbench results are captured in this testbench included as Figure 4. Clock 1 is considered the synchronous clock and clock 2 is the asynchronous clock. They have simulated frequencies of 100 MHz and 59 MHz respectively. Observe that sync_out follows the in signal by three rising edges of clk_1. It’s as if sync_out is a replica; albeit a delayed replica that is stretched to by 3 rising edges of clk_1. Also observe that rise_edge_tick and fall_edge_tick are synchronous with the rising edge of clk_1. The term “tick” refers to a pulsed signal that is high for one clock cycle. This rising edge trigger or the falling edge trigger may be for downstream edge triggered circuits. The sync_out signal may be used for level sensitive circuits.

Figure 4: Testbench results for the synchronizer circuit.

Figure 5 is an attempt to capture the real-world signals using a Digilent Analog Discovery (legacy) instrument. It’s difficult to see as the 100 MHz signals are fleeting and hard to capture. However, the various delays and pulsed signals can be seen. You can’t see in this still picture but know that there was an observable jitter between the signals. This is true because the Analog Discovery’s signal generator that produced the test pulse was not in sync with the BASYS 3 board’s 100 MHz clock. But that’s the point, we crossed clock boundaries – the synchronizer did its job.

Figure 5: Real-world operation of the synchronizer on a Digilent BASYS 3 FPGA.

Parting thoughts

Modern FPGA “compilers” are designed to optimize your code with user assigned weights for power consumption, speed, and fabric consumption. It’s not uncommon for the synthesis tools to trim away parts of your logic. For example, you may need the rise_edge_tick and fall_edge_tick outputs. If you instantiate the synchronizer without connecting these outputs, they will automatically be trimmed. You will see this as an unused logic warning.

You can generally disregard this type of warning. However, when developing your logic, you should make all efforts to identify and eliminate the warnings. I ran headlong into this problem while constructing the rise and fall edge detector for this synchronizer. Originally, I had used a register to remember the last value of sync_out. This is a perfectly natural way of coding in the microcontroller world:

If (present != last) do something

That’s a good solution and it worked but the synthesizing tool constantly presented the logic trimmed warning. Turns out I was thinking about this from the wrong perspective. To understand why, look back at Figure 1. Notice that the last flip-flop is special. It’s not part of the synchronizer per say. Instead, it’s the output register for the module. It is the source of synchronous stability presented to the other FPGA logic. Here stable implies that it is a register, under control of the master clock wrapped in a always@(posedge clk) block.

When you think about it, that register I has used to capture the “last output” was redundant. The signal of interest was held in the sync_out register. The synthesizer tools observed this and warned me about the redundancy.

This observation may help explain the positioning of the rise and fall block inputs shown in Figure 1. Notice that monitors the input and output of the last flip-flop. If the signals are different, the block sends the appropriate pulse.

Best Wishes,

APDahlen

About the Author

Aaron Dahlen, LCDR USCG (Ret.), serves as an application engineer at DigiKey. He has a unique electronics and automation foundation built over a 27-year military career as a technician and engineer which was further enhanced by 12 years of teaching (interwoven). With an MSEE degree from Minnesota State University, Mankato, Dahlen has taught in an ABET accredited EE program, served as the program coordinator for an EET program, and taught component-level repair to military electronics technicians. Dahlen has returned to his Northern Minnesota home and thoroughly enjoys researching and writing articles such as this. LinkedIn | Aaron Dahlen - Application Engineer - DigiKey




synchronizer.v

//******************************************************************************
//
// Module: synchronizer
//
//  This code is derived from Xilinx UltraFast Design Methodology Guide for
//  the Vivado Design Suite UG949 (v2014.1) April 2, 2014
//
//  This RTL is subject to Term and Conditions associated with the 
//  DigiKey TechForum. Refer to:
//  https://www.digikey.com/en/terms-and-conditions?_ga=2.92992980.817147340.1696698685-139567995.1695066003
//
//  Should you find an error, please leave a comment in the forum space below. 
//  If you are able, please provide a recommendation for improvement.
//
//******************************************************************************
//           ______________________________________________
//          |                                              |
//          | synchronizer                                 |
//          |______________________________________________|
//          |                                              |
//          |    Parameters and defaults                   |
//          |        SYNC_STAGES = 2                       |
//          |                                              |
//      ----| async_in                            sync_out |----
//          |                               rise_edge_tick |----
//      ----| clk                           fall_edge_tick |----
//          |______________________________________________|
//
//** Description ***************************************************************
//
//  A synchronizer used to safely transition a signal across a clock domain.
//
//** Sample Instantiation ******************************************************
//
//    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
//    synchronizer(                        // Instance name
//        .clk(clk),                       // Your clock signal
//        .async_in(async_in),             // Asynchronous input signal
//        .sync_out(sync_out),             // Safe synchronized signal
//        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
//        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
//    );
//
//** Parameters ****************************************************************
//
//  Parameter SYNC_STAGES identifies the number of Flip-Flops to be used in
//  the synchronizer. The default value is set to 2 which is generally 
//  considered adequate for most applications.
//
//** Signal Inputs: ************************************************************
//
//  1) clk: high speed clock (typically 100 MHz) for synchronous operation
//
//  2) async_in: The input signal to be synchronized to clk
//
//** Signal Outputs ************************************************************
//
//  1) sync_out: this is a "safe" signal synchronized to clk
//
//  2) rise_edge_tick: pulse on rising edge of synchronized signal
//
//  3) fall_edge_tick: pulse on falling edge of synchronized signal
//
//** Comments ******************************************************************
//
//  N/A
//
//******************************************************************************

module synchronizer #(parameter SYNC_STAGES = 2)(
    input async_in,
    input clk,
    output reg sync_out,
    output reg rise_edge_tick,
    output reg fall_edge_tick
);

//** CONSTANT DECLARATIONS *****************************************************

    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;
        
//** BODY **********************************************************************

    reg [SYNC_STAGES-1:0] sync_regs = {SYNC_STAGES{1'b0}};  // init with zeros

    always @(posedge clk) begin
        sync_regs <= {sync_regs[SYNC_STAGES-2:0], async_in};    // shift left
        sync_out <= sync_regs[SYNC_STAGES-1];

        rise_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == T) ? T : F; 
        fall_edge_tick <= (sync_out != sync_regs[SYNC_STAGES-1]) & (sync_regs[SYNC_STAGES - 1] == F) ? T : F; 
  
    end
endmodule

tb_synchronizer.v

//`timescale 1ns / 1ps
//*************************************************************
//
// Description:
//
//    This testbench provides a stimulus to the synchronizer module.
//    The graphical signal timing diagram serves as the main
//    debugging tool.
//
//*************************************************************
module tb_synchronizer();

    /* Module Inputs */
        reg clk_1;
        reg clk_2;
        reg in;

    /* Module Outputs */
        wire sync_out;
        wire rise_edge_tick;
        wire fall_edge_tick;

    /* Testbench Specific */


//** CONSTANT DECLARATION ************************************

    /* Local */

    /* Clock simulation */
        localparam clock_1_T_ns = 10;     // 100 MHz
        localparam clock_2_T_ns = 17;     // 59 MHz
    /* General shortcuts */
        localparam T = 1'b1;
        localparam F = 1'b0;

//** SYMBOLIC STATE DECLARATIONS ******************************

//** SIGNAL DECLARATIONS **************************************

//** INSTANTIATE THE UNIT UNDER TEST (UUT)*********************

    synchronizer #( .SYNC_STAGES(2) )    // Example with 2 stage flip flop
    synchronizer(                        // Instance name
        .clk(clk_1),                     // Your clock signal
        .async_in(in),                   // Asynchronous input signal
        .sync_out(sync_out),             // Safe synchronized signal
        .rise_edge_tick(rise_edge_tick), // Pulse on rise edge of safe signal
        .fall_edge_tick(fall_edge_tick)  // Pulse on fall edge of safe signal
    );


//** ASSIGN STATEMENTS ****************************************

//** CLOCK ****************************************************

    always begin
        clk_1 = T;
        #(clock_1_T_ns/2);
        clk_1 = F;
        #(clock_1_T_ns/2);
    end

    always begin
        clk_2 = T;
        #(clock_2_T_ns/2);
        clk_2 = F;
        #(clock_2_T_ns/2);
    end

//** UUT Tests ************************************************ 

    initial begin

        initial_conditions();

    /* Begin tests */
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        in = T;
        delay_N_clk_2_cycles(5);
        in = F;
        delay_N_clk_2_cycles(5);
        
//        $monitor($realtime, " count = %d", cnt);

        $finish;
    end

//** FUNCTIONS ************************************************ 

//** Tasks **************************************************** 

    task initial_conditions; begin
        repeat(5) @(posedge clk_1)
        in = F;
        end
    endtask


    task delay_N_clk_1_cycles(input integer N); begin
        repeat(N) @(posedge clk_1);
        end
    endtask
    
       task delay_N_clk_2_cycles(input integer N); begin
        repeat(N) @(posedge clk_2);
        end
    endtask
    
endmodule

1 Like