Reference guide

Reset and clock-domain-crossing patterns

The reset synchroniser, two-flop synchroniser, pulse and handshake synchronisers, Gray-pointer FIFO and the constraints that make them safe, with compiled Verilog.

AMDAlteraLast verified 13 Nov 2026· Verilog checked with Icarus Verilog 13.0, constraints for Vivado 2026.1 and Quartus Prime Pro 26.3

This guide is for engineers who have a signal that must cross between clock domains, or a reset that must reach every flop safely, and want the pattern that is known to work. Each pattern gives the circuit, when it applies, and the constraint that stops the timing tool from failing it or silently ignoring it. The Verilog was compiled with Icarus Verilog and the pulse, handshake and Gray-pointer modules were simulated.

Synchronous or asynchronous reset

Synchronous reset Asynchronous reset
Behaviour Reset is sampled on the clock edge like any data input Flop clears the instant reset asserts, no clock needed
Needs a running clock to assert yes no
Timing Reset is a normal data path; must meet setup and hold Assertion is free; release must meet recovery and removal
Glitch sensitivity Filtered by the clock A glitch resets the flop
Fabric cost Uses the flop’s synchronous clear, or a LUT input on some families Uses the dedicated async clear pin
Common on AMD (recommended in UG949), Lattice Altera, Microchip, ASIC-style code

The house default is synchronous reset, active high, because it keeps every path a normal timed path. Whichever you choose, the release of reset must be synchronous to the clock, which is what the first pattern provides.

Reset synchroniser

Asserts asynchronously (the system is held in reset even if the clock is stopped) and releases synchronously (every flop leaves reset in the same cycle).

module reset_sync (
  input  wire clk,
  input  wire arst_n,   // asynchronous, active-low, from pin or PLL lock
  output wire rst_n     // asserts asynchronously, releases on clk
);
  (* ASYNC_REG = "TRUE" *) reg [1:0] sync_n;
  always @(posedge clk or negedge arst_n) begin
    if (!arst_n) sync_n <= 2'b00;
    else         sync_n <= {sync_n[0], 1'b1};
  end
  assign rst_n = sync_n[1];
endmodule

One of these per clock domain, fed from the same asynchronous source. Follow it with a fan-out tree of registers if the domain is large: the tool will replicate the final flop, but only if you let it (do not mark it KEEP). The output can drive synchronous resets or asynchronous clear pins. What matters is that release is aligned to the clock.

Two-flop synchroniser

For a single bit whose level stays stable for at least two destination clock periods.

module sync2 (
  input  wire clk_dst,
  input  wire d_src,     // a level that stays stable for several clk_dst cycles
  output wire d_dst
);
  (* ASYNC_REG = "TRUE" *) reg [1:0] ff;
  always @(posedge clk_dst) ff <= {ff[0], d_src};
  assign d_dst = ff[1];
endmodule
Metastability on the first flop. d_src changes near edge 3, ff0 is unresolved for part of that cycle, then settles. ff1 only ever sees a clean level, here one cycle later than it would have with an early input.clk_dstd_srcff0ff1
Metastability on the first flop. d_src changes near edge 3, ff0 is unresolved for part of that cycle, then settles. ff1 only ever sees a clean level, here one cycle later than it would have with an early input.

Rules that follow from the diagram:

Rule Reason
Never put logic between the two flops Every gate eats into the time the first flop has to resolve
Never synchronise a multi-bit bus bit by bit Each bit resolves on its own cycle, so the bus is incoherent
The source must be a register, not combinational logic Glitches on the input are sampled as real transitions
Use three flops above about 400 MHz or for very long MTBF targets See /reference/timing-formulae/ for the MTBF arithmetic
Pulses shorter than two destination periods are lost Use the pulse synchroniser below

Pulse (toggle) synchroniser

Turns a single-cycle pulse in the source domain into a single-cycle pulse in the destination domain, for any ratio of clock frequencies as long as pulses are spaced by at least three destination periods.

module pulse_sync (
  input  wire clk_src,
  input  wire pulse_src,   // single-cycle pulse in clk_src
  input  wire clk_dst,
  output wire pulse_dst    // single-cycle pulse in clk_dst
);
  reg tog_src = 1'b0;
  always @(posedge clk_src)
    if (pulse_src) tog_src <= ~tog_src;

  (* ASYNC_REG = "TRUE" *) reg [1:0] ff;
  reg tog_prev;
  always @(posedge clk_dst) begin
    ff       <= {ff[0], tog_src};
    tog_prev <= ff[1];
  end
  assign pulse_dst = ff[1] ^ tog_prev;
endmodule

The toggle register converts each pulse into a level change, the two flops synchronise the level, and the XOR with a delayed copy makes the edge back into a pulse. If pulses can arrive faster than the destination can absorb them, use the handshake below or a FIFO.

Handshake synchroniser

Moves a word of data across with a request and acknowledge, so the source knows when it may change the data. Throughput is low (about six cycles per word across both domains) but it is always correct.

module handshake_sync #(parameter W = 16) (
  input  wire         clk_src,
  input  wire         req_src,     // pulse when data_src is valid
  input  wire [W-1:0] data_src,
  output wire         busy_src,    // high until the far side has taken the data
  input  wire         clk_dst,
  output reg          valid_dst = 1'b0,
  output reg  [W-1:0] data_dst
);
  reg         req_tog = 1'b0;
  reg [W-1:0] data_hold;
  always @(posedge clk_src)
    if (req_src && !busy_src) begin
      req_tog   <= ~req_tog;
      data_hold <= data_src;
    end

  (* ASYNC_REG = "TRUE" *) reg [1:0] req_ff = 2'b00;
  reg req_prev = 1'b0;
  always @(posedge clk_dst) begin
    req_ff    <= {req_ff[0], req_tog};
    req_prev  <= req_ff[1];
    valid_dst <= req_ff[1] ^ req_prev;
    if (req_ff[1] ^ req_prev) data_dst <= data_hold;
  end

  (* ASYNC_REG = "TRUE" *) reg [1:0] ack_ff = 2'b00;
  always @(posedge clk_src) ack_ff <= {ack_ff[0], req_ff[1]};
  assign busy_src = req_tog ^ ack_ff[1];
endmodule

The data bus data_hold crosses the domains without a synchroniser. That is safe because it is guaranteed stable from before the request toggles until after the acknowledge returns, and the constraint in the last section tells the tool so.

Asynchronous FIFO with Gray pointers

For streaming data, an asynchronous FIFO is the right tool. Dual-port block RAM holds the data. Each side keeps a write or read pointer in its own domain, and each pointer is passed to the other side through a two-flop synchroniser to compute full and empty.

Binary pointers cannot be synchronised, because several bits can change at once and each bit might resolve on a different cycle. Gray code changes exactly one bit per increment, so a synchronised Gray pointer is always either the old value or the new value, never a mixture. The pointer is one bit wider than the address so that full and empty can be told apart.

module gray_ptr #(parameter AW = 4) (
  input  wire          clk,
  input  wire          rst,
  input  wire          inc,
  output reg  [AW:0]   bin,      // one extra bit to tell full from empty
  output reg  [AW:0]   gray
);
  wire [AW:0] bin_next  = bin + {{AW{1'b0}}, inc};
  wire [AW:0] gray_next = bin_next ^ (bin_next >> 1);
  always @(posedge clk) begin
    if (rst) begin
      bin  <= {(AW+1){1'b0}};
      gray <= {(AW+1){1'b0}};
    end else begin
      bin  <= bin_next;
      gray <= gray_next;
    end
  end
endmodule

module gray_to_bin #(parameter AW = 4) (
  input  wire [AW:0] gray,
  output wire [AW:0] bin
);
  genvar i;
  generate
    for (i = 0; i <= AW; i = i + 1) begin : g_xor
      assign bin[i] = ^(gray >> i);
    end
  endgenerate
endmodule

The flags compare the local Gray pointer with the synchronised remote one:

module fifo_flags #(parameter AW = 4) (
  input  wire [AW:0] wr_gray,        // write pointer, write domain
  input  wire [AW:0] rd_gray_sync,   // read pointer after two flops in write domain
  input  wire [AW:0] rd_gray,        // read pointer, read domain
  input  wire [AW:0] wr_gray_sync,   // write pointer after two flops in read domain
  output wire        full,
  output wire        empty
);
  assign empty = (rd_gray == wr_gray_sync);
  assign full  = (wr_gray == {~rd_gray_sync[AW:AW-1], rd_gray_sync[AW-2:0]});
endmodule

Empty means the pointers are equal. Full means they are equal except for the top two bits inverted, which is how Gray code expresses “one wrap ahead”. Both flags are pessimistic because the remote pointer is two cycles stale, which is safe: the FIFO may report full slightly early, but it never overflows. Keep the binary pointer for addressing the RAM and the Gray pointer for crossing. Vendor FIFO generators (AMD FIFO Generator and XPM FIFO, Altera DCFIFO, Lattice FIFO_DC) implement exactly this and should be preferred over hand-written FIFOs in production.

Reset tree

Step Why
One asynchronous source (power-on, external pin, PLL lock) A single point of truth for “the system is up”
One reset synchroniser per clock domain Release aligned to each clock
Hold reset until every PLL reports lock Clocks must be stable before release
Release the slowest domain last, or sequence resets by a counter Avoids a fast domain reading an uninitialised slow one
Register the reset at the top of each large block Breaks the fan-out so that timing closes
Reset only what needs it: control state, counters, valid flags Data path registers and RAM do not need reset and cost routing when they get one

AMD’s UG949 recommends few resets, synchronous and active high. Altera and Microchip fabrics have cheap asynchronous clears and their guides accept either. Do not mix the two styles on the same signal.

What to constrain

Pattern Vivado XDC Quartus SDC Why
Any synchroniser flops set_property ASYNC_REG TRUE [get_cells u_s/ff_reg*] (or the attribute in RTL) set_instance_assignment -name SYNCHRONIZER_IDENTIFICATION FORCED -to u_s/ff* in the QSF Keeps the two flops adjacent, stops the tool from inserting logic, enables MTBF reports
Single-bit crossing into a synchroniser set_false_path -to [get_cells u_s/ff_reg[0]] or set_max_delay -datapath_only set_false_path -to [get_registers u_s/ff[0]] The path is asynchronous by design; without this the tool fails it or, worse, “fixes” it
Handshake data bus, Gray pointer bus set_max_delay -datapath_only T_dst -from [get_cells src_reg*] -to [get_cells dst_reg*] and set_bus_skew for Gray buses set_max_delay plus set_net_delay -max on the bus Bounds the delay so that the stable-window assumption holds; a false path would let the tool route the bus arbitrarily slowly
Unrelated clocks overall set_clock_groups -asynchronous same Declares that no synchronous path exists between the groups
Reset synchroniser input set_false_path -from [get_ports arst_n] same The asynchronous assert is not a timed path; the release is already synchronous

Prefer set_max_delay -datapath_only over set_false_path for anything carrying data. A false path means “never check this”, a bounded delay means “check this, loosely”, and only the second one catches a crossing that a later change made slow. Run the vendor CDC report (report_cdc in Vivado, the Timing Analyzer CDC viewer in Quartus Prime Pro) after every significant change.

Sources