From 502b1383dc16d8c6d17b3a90146b305fa17b0283 Mon Sep 17 00:00:00 2001 From: Luke Wren Date: Tue, 5 Mar 2019 08:52:20 +0000 Subject: [PATCH] lib.cdc: Implement BusSynchronizer and add smoke tests --- nmigen/lib/cdc.py | 99 ++++++++++++++++++++++++++++++++++++- nmigen/test/test_lib_cdc.py | 46 +++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) diff --git a/nmigen/lib/cdc.py b/nmigen/lib/cdc.py index ae6b9c6f..fc5250bb 100644 --- a/nmigen/lib/cdc.py +++ b/nmigen/lib/cdc.py @@ -2,7 +2,13 @@ from math import gcd -__all__ = ["MultiReg", "ResetSynchronizer", "PulseSynchronizer", "Gearbox"] +__all__ = [ + "MultiReg", + "ResetSynchronizer", + "PulseSynchronizer", + "BusSynchronizer", + "Gearbox" +] class MultiReg: @@ -122,7 +128,6 @@ class PulseSynchronizer: Parameters ---------- - idomain : str Name of input clock domain. odomain : str @@ -157,6 +162,96 @@ def elaborate(self, platform): return m +class BusSynchronizer: + """Pass a multi-bit signal safely between clock domains. + + Ensures that all bits presented at `o` form a single word + that was present synchronously at `i` in the input clock + domain (unlike direct use of MultiReg). + + Parameters + ---------- + width : int > 0 + Width of the bus to be synchronised + idomain : str + Name of input clock domain + odomain : str + Name of output clock domain + sync_stages : int > 1 + Number of synchronisation stages used in the req/ack + pulse synchronisers. Lower than 2 is unsafe. Higher + values increase safety for high-frequency designs, + but increase latency too. + timeout : int >= 0 + The request from idomain is re-sent if `timeout` cycles + elapse without a response. + `timeout` = 0 disables this feature. + + Attributes + ---------- + i : Signal(width), in + Input signal, sourced from `idomain` + o : Signal(width), out + Resynchronised version of `i`, driven to `odomain` + """ + def __init__(self, width, idomain, odomain, sync_stages=2, timeout = 127): + if not isinstance(width, int) or width < 1: + raise TypeError("width must be a positive integer, not '{!r}'".format(sync_stages)) + if not isinstance(sync_stages, int) or sync_stages < 2: + raise TypeError("sync_stages must be an integer > 1, not '{!r}'".format(sync_stages)) + if not isinstance(timeout, int) or timeout < 0: + raise TypeError("timeout must be a non-negative integer, not '{!r}'".format(sync_stages)) + + self.i = Signal(width) + self.o = Signal(width, attrs={"no_retiming": True}) + self.width = width + self.idomain = idomain + self.odomain = odomain + self.sync_stages = sync_stages + self.timeout = timeout + + def elaborate(self, platform): + m = Module() + if self.width == 1: + m.submodules += MultiReg(self.i, self.o, odomain=self.odomain, n=self.sync_stages) + return m + + req = Signal() + ack_o = Signal() + ack_i = Signal() + + sync_io = m.submodules.sync_io = \ + PulseSynchronizer(self.idomain, self.odomain, self.sync_stages) + sync_oi = m.submodules.sync_oi = \ + PulseSynchronizer(self.odomain, self.idomain, self.sync_stages) + + if self.timeout != 0: + countdown = Signal(max=self.timeout, reset=self.timeout) + with m.If(ack_i | req): + m.d[self.idomain] += countdown.eq(self.timeout) + with m.Else(): + m.d[self.idomain] += countdown.eq(countdown - countdown.bool()) + + start = Signal(reset=1) + m.d[self.idomain] += start.eq(0) + m.d.comb += [ + req.eq(start | ack_i | (self.timeout != 0 and countdown == 0)), + sync_io.i.eq(req), + ack_o.eq(sync_io.o), + sync_oi.i.eq(ack_o), + ack_i.eq(sync_oi.o) + ] + + buf_i = Signal(self.width, attrs={"no_retiming": True}) + buf_o = Signal(self.width) + with m.If(req): + m.d[self.idomain] += buf_i.eq(self.i) + sync_data = m.submodules.sync_data = \ + MultiReg(buf_i, buf_o, odomain=self.odomain, n=self.sync_stages - 1) + with m.If(ack_o): + m.d[self.odomain] += self.o.eq(buf_o) + + return m class Gearbox: """Adapt the width of a continous datastream. diff --git a/nmigen/test/test_lib_cdc.py b/nmigen/test/test_lib_cdc.py index 44e8a353..322bf800 100644 --- a/nmigen/test/test_lib_cdc.py +++ b/nmigen/test/test_lib_cdc.py @@ -155,6 +155,52 @@ def process(): sim.add_process(process) sim.run() +class BusSynchronizerTestCase(FHDLTestCase): + def test_paramcheck(self): + with self.assertRaises(TypeError): + bs = BusSynchronizer(0, "i", "o") + with self.assertRaises(TypeError): + bs = BusSynchronizer("x", "i", "o") + + bs = BusSynchronizer(1, "i", "o") + + with self.assertRaises(TypeError): + bs = BusSynchronizer(1, "i", "o", sync_stages = 1) + with self.assertRaises(TypeError): + bs = BusSynchronizer(1, "i", "o", sync_stages = "a") + with self.assertRaises(TypeError): + bs = BusSynchronizer(1, "i", "o", timeout=-1) + with self.assertRaises(TypeError): + bs = BusSynchronizer(1, "i", "o", timeout="a") + + bs = BusSynchronizer(1, "i", "o", timeout=0) + + def test_smoke_w1(self): + self.check_smoke(width=1, timeout=127) + + def test_smoke_normalcase(self): + self.check_smoke(width=8, timeout=127) + + def test_smoke_notimeout(self): + self.check_smoke(width=8, timeout=0) + + def check_smoke(self, width, timeout): + m = Module() + m.domains += ClockDomain("sync") + bs = m.submodules.dut = BusSynchronizer(width, "sync", "sync", timeout=timeout) + + with Simulator(m, vcd_file = open("test.vcd", "w")) as sim: + sim.add_clock(1e-6) + def process(): + for i in range(10): + testval = i % (2 ** width) + yield bs.i.eq(testval) + # 6-cycle round trip, and if one in progress, must complete first: + for j in range(11): + yield Tick() + self.assertEqual((yield bs.o), testval) + sim.add_process(process) + sim.run() # TODO: test with distinct clocks # (since we can currently only test symmetric aspect ratio)