Pulse synchronizer
A one-cycle pulse carried into another clock domain. A toggle flop makes it a level, two flops carry it across, and a flop and an XOR make it a pulse again.
- Level
- rtl
- Code
- pulse-synchronizer
- Checked
- Verilator 5.052, Icarus Verilog 13.0, Yosys 0.66
- Published
- Authorship
- Drafted with AI assistance
- Status
- In progress — published so it can be reviewed, not finished.
A pulse synchronizer carries a one-cycle pulse from one clock domain into another, where it arrives
as a one-cycle pulse of the receiving clock. It cannot carry the pulse itself, because a pulse one
clk_a period wide can fall entirely between two edges of clk_b. So a toggle flop turns each
pulse into a change of level, two flops carry the level across, and a third flop and an XOR turn
each change back into a pulse.
Run plays this scenario from the start; driving an input continues from here.
A circuit that runs: a schematic and its waveform on one time axis. Inputs: pulse_a, rst_a_n, rst_b_n. Outputs: pulse_b. clk_a runs at 8 ticks a period. clk_b runs at 10 ticks a period. It is built from 2 XOR gates, 4 D flip-flops. The waveform shows clk_a, rst_a_n, pulse_a, toggle, clk_b, meta, sync, sync_d, pulse_b.
Figure 1. Press Run. pulse_a is high for one clk_a period, between two clk_b edges; toggle keeps the change as a level, and pulse_b is high for one clk_b period from the second clk_b edge after the change.
How it works, scenario by scenario
clk_a's domain is one flop, at the top of the drawing: its output, toggle, comes back through
an XOR with pulse_a, so it flips at every clk_a edge that finds pulse_a high. The rest is
clk_b's: three flops, meta, sync and sync_d, and an XOR of the last two that drives the
lamp pulse_b. The waveform's grid lines are clk_a's rising edges, at 4, 12, 20 and 28; clk_b
rises at 3, 13, 23 and 33, never on a grid line, so follow its row for its edges. Both resets are
released at tick 1; rst_b_n has no row, and its pad shows it high.
In One pulse, the default scenario, pulse_a is high from 4 to 12, one clk_a period. Look
along the clk_b row: it rises at 3, just before, and at 13, just after, and nowhere in between.
A flop on clk_b that sampled pulse_a itself would find 0 at 3 and 0 at 13, and to that flop
the pulse would never have happened. So the pulse becomes a level first. The clk_a edge at 4
still samples the 0 that pulse_a held just before it, so toggle stays 0. The edge at 12
samples its 1 and flips toggle from 0 to 1, and toggle holds that 1 until the next pulse,
however long clk_b takes to see it.
clk_b's edge at 13 copies the new 1 into meta, and its edge at 23 copies it into sync. From
23, sync is 1 while sync_d, which is sync one clk_b period ago, still holds 0, so their
XOR, pulse_b, goes high. At 33 sync_d catches up and pulse_b falls. The window ends at tick
33, so sync_d's rise and pulse_b's fall show only as the last tick of their rows: read the
rows as pulse_b high from 23 to 33, exactly one clk_b period. pulse_b rises at the second
clk_b edge after the clk_a edge that sent the pulse.
The clk_b edge at 13 comes one tick after toggle changed. The two clocks are unrelated, so in
silicon that change can land inside meta's setup-and-hold window. meta may then go metastable
and settle either way. meta and sync are the
two-flop synchronizer, which gives meta a clk_b period to
settle. The toggle scheme survives either outcome, because what crosses is a level and the level
stays. A meta that misses the change at 13 takes it at 23, and pulse_b arrives one clk_b
period later, at 33 instead of 23: late, never lost.
Too close sends the same pulse, then holds pulse_a high from 36 to 52, two clk_a periods.
To the toggle flop that is two pulses back to back, since the edges at 44 and 52 both find it high.
toggle falls to 0 at 44, one tick after clk_b's edge at 43, and rises to 1 again at 52, one
tick before the edge at 53. Both changes fall between the same two clk_b edges, both edges see a
1, and neither pulse crosses: meta holds its 1 and pulse_b its 0 to the end.
Reset one side sends the same pulse, then pulls rst_a_n low from 38 to 40, after the pulse
has crossed and while toggle is 1. The reset is asynchronous, so toggle drops to 0 at 38
without waiting for clk_a. To clk_b's flops that fall is a change of level like any other:
meta takes it at 43, sync at 53, and pulse_b is high from 53 to 63, one clk_b period, for a
pulse nobody sent on pulse_a.
The code
// A one-cycle pulse in clk_a's domain, carried into clk_b's as a one-cycle
// pulse there. A toggle flop turns each pulse into a change of level, two
// flops carry that level into clk_b's domain, and a third flop and an XOR
// turn each change back into a pulse.
//
// Reset style for this module: asynchronous, active low, one reset a
// domain: rst_a_n clears the toggle, rst_b_n the three flops after it. Each
// is released through its own domain's reset synchronizer, which this module
// leaves out.
module pulse_synchronizer (
input logic clk_a,
input logic rst_a_n,
input logic pulse_a,
input logic clk_b,
input logic rst_b_n,
output logic pulse_b
);
// Flips at every clk_a edge that finds pulse_a high. It is the only signal
// that crosses, and it comes straight from a flop, so it changes once a
// pulse and never glitches.
logic toggle;
always_ff @(posedge clk_a or negedge rst_a_n) begin
if (!rst_a_n) begin
toggle <= 1'b0;
end else begin
toggle <= toggle ^ pulse_a;
end
end
// meta may go metastable and has a clk_b period to settle; sync_d is the
// value sync had one clk_b period ago.
logic meta, sync, sync_d;
always_ff @(posedge clk_b or negedge rst_b_n) begin
if (!rst_b_n) begin
meta <= 1'b0;
sync <= 1'b0;
sync_d <= 1'b0;
end else begin
meta <= toggle;
sync <= meta;
sync_d <= sync;
end
end
// High for one clk_b period after every change of the synchronized level.
assign pulse_b = sync ^ sync_d;
endmoduleThe reset style is stated at the top of the file: asynchronous, active low, and one reset a domain,
rst_a_n for the toggle and rst_b_n for the three flops after it. Each is released through its
own domain's reset synchronizer, which the module leaves out, and
so does the drawing. The always_ff on clk_a is the flop at the top of the drawing, and
toggle <= toggle ^ pulse_a is that flop and the XOR in front of it. The always_ff on clk_b is
the three flops along the bottom, meta, sync and sync_d, each a line in the reset branch and
a line after else, and the assign is the XOR that drives pulse_b. Only toggle crosses
between the domains, and it comes straight from a flop, so it changes once a pulse, or at a reset,
and never glitches.
Synthesis result
Yosys's generic synthesis turns pulse_synchronizer.sv into 6 cells, 4 of them flip-flops. The cells are Yosys's own gate library, not an FPGA's or a standard-cell library's: the count measures the logic, not the area of any chip.
| Cell | What it is | Count |
|---|---|---|
$_DFF_PN0_ | D flip-flop on the rising edge, active-low asynchronous reset to 0 | 4 |
$_XOR_ | 2-input XOR | 2 |
| Total | 6 | |
Lint by Verilator 5.052, the testbench by Icarus Verilog 13.0, synthesis by Yosys 0.66.
Icarus replayed this page's three scenarios on this code, and all 1,584 values the waveform draws match.
The netlist before synthesis
The netlist of pulse_synchronizer.sv, drawn by netlistsvg 1.0.2, is 802 × 299 px, too large to read inline: open it on its own, where the browser can zoom.
Pitfalls
Sampling the pulse directly. One pulse is the reason the circuit exists: no clk_b edge
falls while pulse_a is high. Here pulse_a is high for one clk_a period, shorter than a
clk_b period, so a flop on clk_b fed with pulse_a catches the pulse only when a clk_b edge
happens to land inside it, and nothing relates the phases of the two clocks. A two-flop
synchronizer on the raw pulse is the same mistake with one more flop.
Pulses closer than two clk_b periods. Too close shows the limit: two changes of toggle
between the same two clk_b edges, here 43 and 53, cancel, since clk_b sees only the level at
its edges. At most other phases of clk_b an edge would fall between them and catch the 0, and the
two pulses would merge into one pulse_b two clk_b periods long. Two pulses one clk_a cycle
apart, as here, never arrive as two while clk_b is no faster than clk_a. They always do, at
every phase, when the clk_a edges that sample them are at least two clk_b periods apart, three
clk_a cycles at this page's clocks. Both are zero-delay results, as in the figure. In silicon a
metastable meta can move a capture by one clk_b edge, so the spacing needs a margin on top.
A sender that may pulse more often needs more than this circuit, such as a handshake that tells it when it may send again.
Resetting one side alone. Reset one side pulls rst_a_n while toggle is 1, and pulse_b
delivers a pulse nobody sent. Pulling rst_b_n alone, low over the same ticks, does it too: its
three flops go to 0 while toggle still holds 1, meta takes that 1 again at 43, and pulse_b
rises at 53 for one clk_b period. Reset both domains together, as the testbench does: it pulls
both resets while toggle is 1 and checks that nothing comes out.
A rising-edge detector instead of the XOR. The last stage looks like the rising-edge detector,
and it is tempting to write it as one, pulse_b = sync & ~sync_d. But toggle rises for one pulse
and falls for the next, so every second pulse arrives as a falling edge of sync, and the AND
drops it. The XOR is high after either edge. In the figure sync falls only in Reset one side,
after a reset rather than a pulse, so no scenario shows the loss. The testbench does: it sends 151
pulses, and a copy with that line lost 75 of them, every second one.
What the figure does not model. The figure is zero-delay, and meta never goes metastable in
it: meta takes each change of toggle at the first clk_b edge after the change. In silicon a
change just before a clk_b edge may be taken one edge later than drawn, as the walk-through
says, and one just after an edge may be taken by that edge, one edge earlier. Metastability itself
is the two-flop synchronizer page's lesson.
Check yourself
- Why is pulse_b the XOR of sync and sync_d, and not sync & ~sync_d as in a rising-edge detector?
- Because every pulse on pulse_a is a change of toggle, and the changes alternate. The first pulse makes toggle rise, the next makes it fall, and sync follows it two clk_b edges later. An AND with ~sync_d is high only after a rise, so it drops every second pulse; the XOR is high for one clk_b period after a change either way.
- Why does pulse_a not go straight into meta?
- Because a pulse one clk_a period wide can begin and end between two clk_b edges, as it does in One pulse, high from 4 to 12 with clk_b rising at 3 and 13. A flop on clk_b that sampled pulse_a would find 0 at both edges. The toggle flop turns it into a level that stays until the next pulse, however long clk_b takes to look.
- How far apart must two pulses on pulse_a be to arrive as two pulses on pulse_b?
- To arrive as two at every phase of clk_b in simulation, the clk_a edges that sample them have to be at least two clk_b periods apart, which at this page's clocks is three clk_a cycles, and silicon needs a margin on top. Closer than two periods, a single clk_b edge can fall between the two changes of toggle, and the two pulses merge into one pulse_b two clk_b periods long. Closer than one clk_b period, it can happen that no edge falls between them at all, and then they cancel, as in Too close.