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.
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
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
- C. Cummings, “Clock Domain Crossing (CDC) Design and Verification Techniques Using SystemVerilog”, SNUG Boston 2008
- C. Cummings and P. Alfke, “Simulation and Synthesis Techniques for Asynchronous FIFO Design”, SNUG San Jose 2002
- C. Cummings and D. Mills, “Synchronous Resets? Asynchronous Resets? I am so confused!”, SNUG San Jose 2002
- C. Cummings, D. Mills and S. Golson, “Asynchronous and Synchronous Reset Design Techniques, Part Deux”, SNUG Boston 2003
- UG949: UltraFast Design Methodology Guide, clocking and reset chapters
- UG903: Vivado Design Suite User Guide, Using Constraints
- Quartus Prime Pro Edition User Guide: Timing Analyzer (document 683243)
- Related on this site: /reference/xdc-sdc-constraints/, /reference/timing-formulae/, /reference/glossary/#metastability