15 June 2022

Lib: clock/xd

Sometimes you need to send a single pulse from one clock domain to another. This is a simple case of clock domain crossing or CDC. This post uses the xd module from the Project F Library to handle such situations simply and safely.

Get in touch: GitHub Issues, 1BitSquared Discord, @WillFlux (Mastodon), @WillFlux (Twitter)

NB. A revised version of xd without reset signals is currently being rolled out across Project F. This post will be updated when this is complete.

Sponsor My Work
If you like what I do, consider sponsoring me on GitHub.
I love FPGAs and want to help more people discover and use them in their projects.
My hardware designs are open source, and my blog is advert free.

Crossing the Streams

The standard advice for safely handling an async signal is to pass it through a pair of flip-flops. This works well if your source signal is lower frequency than your destination, such as a button press or UART. However, this approach fails when the source signal has a similar or higher frequency than the destination.

A FIFO will work correctly for any frequencies, but this is relatively complex and logic-heavy. Instead, we can use a simple “toggle” trick for an isolated pulse to safely cross domains, irrespective of their frequencies.

Thanks to fpga4fun for this approach to CDC.

XD Module

Project F Library module [clock/xd.sv]:

module xd (
    input  wire logic clk_i,  //  input clock: source domain
    input  wire logic clk_o,  // output clock: destination domain
    input  wire logic rst_i,  //        reset: source domain
    input  wire logic rst_o,  //        reset: destination domain
    input  wire logic i,      //  input pulse: source domain
    output      logic o       // output pulse: destination domain
    );

    // toggle reg when pulse received in source domain
    logic toggle_i;
    always_ff @(posedge clk_i) begin
        toggle_i <= toggle_i ^ i;
        if (rst_i) toggle_i <= 1'b0;
    end

    // cross to destination domain via shift reg
    logic [3:0] shr_o;
    always_ff @(posedge clk_o) begin
        shr_o <= {shr_o[2:0], toggle_i};
        if (rst_o) shr_o <= 4'b0;
    end

    // output pulse when transition occurs
    always_comb begin
        o = shr_o[3] ^ shr_o[2];
    end
endmodule

On reflection, I could have named the module ports better—something I’ll fix in the next version.

There’s a Vivado test bench you can use to exercise the module with Vivado: [clock/xc7/xd_tb.sv]. Verilator test benches will be available later.

Diagram will be added shortly.

Example

My designs often have separate pixel and system clocks. When a new frame starts, we must pass the event from the pixel to the system clock domain.

Along with the signal you wish to bring into the new clock domain, you need the clock and reset signals for both domains.

    logic frame;      // start of new frame (pixel clock domain)
    logic frame_sys;  // start of new frame (system clock domain)
    xd xd_frame (
        .clk_i(clk_pix),  // source clock
        .clk_o(clk_sys),  // destination clock
        .rst_i(rst_pix),  // source reset
        .rst_o(rst_sys),  // destination reset
        .i(frame),        // source flag
        .o(frame_sys)     // destination flag
    );

You can see this module in action in many Project F designs, including Framebuffers and 2D Shapes.

Caution Advised

Before you rush to add new clock domains to your designs, you need to be aware of a significant limitation: this approach only works for isolated pulses of one clock cycle. The clock/xd module is ideal for sending well-spaced events but can’t be used to send arbitrary data from one clock domain to another: use dual-port BRAM or a proper FIFO instead.

The following simulation shows what happens if you abuse the module:

xd module simulation

Note how the final two-cycle pulse becomes two separate pulses when sent from slow to fast but disappears altogether when sent from fast to slow. You have been warned!

Check out the site map for more FPGA projects and tutorials.

©2022 Will Green, Project F