from amaranth import * from amaranth.sim import Simulator class SyncFF(Elaboratable): """Width-N multi-flop synchronizer from `src_domain` to `dst_domain`. Use when the source is a level signal that may be stable for multiple destination cycles. Not suitable for single-cycle pulses (use TogglePulseSync instead). """ def __init__(self, width=1, src_domain="src", dst_domain="dst"): self.width = width self.src_domain = src_domain self.dst_domain = dst_domain self.src = Signal(self.width) self.dst = Signal(self.width) def elaborate(self, platform): m = Module() reg_src = Signal(self.width) ff0 = Signal(self.width) ff1 = Signal(self.width) m.d[self.src_domain] += reg_src.eq(self.src) m.d[self.dst_domain] += ff0.eq(reg_src) m.d[self.dst_domain] += ff1.eq(ff0) m.d.comb += self.dst.eq(ff1) return m class TogglePulseSync(Elaboratable): """Reliable pulse transfer from `src_domain` into `dst_domain`. - Source toggles `toggle` whenever an event occurs. - Destination synchronizes the toggle and detects edges. Guarantees ordering and no lost pulses for single-bit events. """ def __init__(self, src_domain="src", dst_domain="dst"): self.src_domain = src_domain self.dst_domain = dst_domain self.src_pulse = Signal() self.dst_pulse = Signal() def elaborate(self, platform): m = Module() toggle = Signal() sync0 = Signal() sync1 = Signal() prev = Signal() edge = Signal() # Source domain: flip the toggle when a pulse arrives m.d[self.src_domain] += If(self.src_pulse, toggle.eq(~toggle)) # Destination domain: two-flop synchronize the toggle m.d[self.dst_domain] += sync0.eq(toggle) m.d[self.dst_domain] += sync1.eq(sync0) # Detect the change in the destination domain m.d[self.dst_domain] += edge.eq(sync1 ^ prev) m.d[self.dst_domain] += prev.eq(sync1) m.d.comb += self.dst_pulse.eq(edge) return m def _sim_toggle_pulse(): """Simple simulation that drives pulses on the source domain and prints detections on the destination domain.""" top = Module() t = TogglePulseSync(src_domain="src", dst_domain="dst") top.submodules.t = t sim = Simulator(top) # Create two asynchronous clocks (periods chosen arbitrarily for the sim) sim.add_clock(1e-6, domain="src") sim.add_clock(1.5e-6, domain="dst") def process(): # Wait a little, then generate three source pulses at different phases for _ in range(5): yield for i in range(3): yield t.src_pulse.eq(1) yield yield t.src_pulse.eq(0) # let the domains run for a few cycles for _ in range(10): dp = (yield t.dst_pulse) if dp: print(f"dst detected pulse at sim tick") yield # run a bit longer to observe behavior for _ in range(20): yield sim.add_sync_process(process, domain="src") sim.run_until(100e-6) if __name__ == "__main__": _sim_toggle_pulse()