The Two-Flop Synchronizer as a Module
The previous article quantified why two flops is enough. This one writes the module.
We have built this circuit twice without treating it as one reusable thing: the reset synchronizer in Section 5.5 and the pointer synchronizers inside the asynchronous FIFO. Both are the same primitive, two flip-flops in series sampling an asynchronous signal and giving it two clock periods to resolve before anything downstream trusts it.
The general-purpose synchronizer
`timescale 1ns / 1ps
`default_nettype none
module sync_2ff #(parameter WIDTH = 1) (
input wire dest_clk,
input wire dest_rst_n,
input wire [WIDTH-1:0] async_in,
output wire [WIDTH-1:0] sync_out
);
// The attribute marks both flops as a synchronizer chain. See the note below.
(* ASYNC_REG = "TRUE" *) reg [WIDTH-1:0] stage1, stage2;
always @(posedge dest_clk or negedge dest_rst_n) begin
if (!dest_rst_n) begin
stage1 <= {WIDTH{1'b0}};
stage2 <= {WIDTH{1'b0}};
end else begin
stage1 <= async_in; // first stage: absorbs the metastable event
stage2 <= stage1; // second stage: safe to use downstream
end
end
assign sync_out = stage2;
endmodule
It is parameterized by WIDTH so the same module handles a single control bit or, per the Gray code article, a multi-bit pointer where every bit needs its own independent two-flop path. That is not the same as solving the multi-bit consistency problem, which is Gray code’s job. This module only solves metastability resolution; Gray code is what makes it safe to apply bit by bit to a multi-bit value in the first place.
The synthesis attribute matters
(* ASYNC_REG = "TRUE" *) is not decoration. It tells the synthesis and place-and-route tools to treat these registers differently:
- It stops the tool optimising the two flops together. A tool that does not know these registers are a synchronizer might see
stage1 <= async_in; stage2 <= stage1;and retime or merge them as an ordinary two-stage pipeline, where that is safe. It is not safe here: retiming a synchronizer changes its resolution time and therefore its real MTBF, in ways the tool cannot account for. - It tells place-and-route to put the two flops close together, minimising the wire delay between them and maximising the resolution time before the second flop’s setup window.
- It tells static timing analysis to treat the path specially. A normal timing path assumes a well-behaved input; a synchronizer’s first stage is, by definition, sometimes fed a signal that violates setup and hold. STA needs to know to exclude that path from ordinary violation reporting, or every synchronizer in the design shows up as a permanent, unfixable timing failure.
The exact attribute varies by vendor. Xilinx Vivado uses ASYNC_REG; other tools have their own spelling for the same instruction. Some form of it belongs on every synchronizer register pair. Leaving it out does not break simulation, since simulation does not retime anything, which is exactly why this is the kind of bug that passes simulation and only appears when a particular synthesis run decides to optimise your synchronizer.
Where it belongs, and where it does not
The two-flop synchronizer is correct for one job: a single control bit, or a value that is guaranteed by construction (Gray-coded, or changing far less often than the destination clock) not to have the multi-bit consistency problem. It is not sufficient on its own for an arbitrary multi-bit bus, a status register, a data word or an address, that changes on any source cycle. That case needs either Gray code, for counters, or the handshake in the next article.
The mistake I see is someone instantiating sync_2ff with WIDTH set to 8, feeding it a byte that changes on most source cycles, and calling the crossing solved because “it’s synchronized now”. Each of the 8 bits is correctly resolved out of metastability, but nothing guarantees all 8 were sampled from the same source value; they can each resolve on different destination edges relative to when the source changed. That is the multi-bit failure from Section 7.4 wearing a different disguise.
Practice Exercise
A design has a single-bit interrupt request generated asynchronously by an external peripheral, to be observed by a processor on an unrelated clock. Instantiate sync_2ff for it and explain why this is exactly the case the module is built for.
Solution
wire irq_sync;
sync_2ff #(.WIDTH(1)) u_irq_sync (
.dest_clk(proc_clk),
.dest_rst_n(proc_rst_n),
.async_in(irq_request),
.sync_out(irq_sync)
);
irq_request is a single bit, so there is no multi-bit consistency problem. A single bit resolving to either its old or new value, the only two possibilities after metastability settles, is by definition self-consistent. The processor can treat irq_sync as the interrupt request, safely visible in the proc_clk domain, with two cycles of latency, which is precisely the guarantee the module provides.
The two-flop synchronizer is worth having as one module: parameterized, tagged with the attribute that stops tools from quietly destroying its timing, and used only for the job it solves, single bits or values already made safe by construction. In the next article we handle the case where you need to move an arbitrary data word across clock domains and Gray code does not apply: the request/acknowledge handshake.