Skip to content

Commit

Permalink
lib.cdc: Implement BusSynchronizer and add smoke tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Wren6991 committed Mar 5, 2019
1 parent 26442d3 commit 502b138
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 2 deletions.
99 changes: 97 additions & 2 deletions nmigen/lib/cdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
from math import gcd


__all__ = ["MultiReg", "ResetSynchronizer", "PulseSynchronizer", "Gearbox"]
__all__ = [
"MultiReg",
"ResetSynchronizer",
"PulseSynchronizer",
"BusSynchronizer",
"Gearbox"
]


class MultiReg:
Expand Down Expand Up @@ -122,7 +128,6 @@ class PulseSynchronizer:
Parameters
----------
idomain : str
Name of input clock domain.
odomain : str
Expand Down Expand Up @@ -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.
Expand Down
46 changes: 46 additions & 0 deletions nmigen/test/test_lib_cdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 502b138

Please sign in to comment.