Skip to content

Commit 502b138

Browse files
committed
lib.cdc: Implement BusSynchronizer and add smoke tests
1 parent 26442d3 commit 502b138

File tree

2 files changed

+143
-2
lines changed

2 files changed

+143
-2
lines changed

nmigen/lib/cdc.py

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
from math import gcd
33

44

5-
__all__ = ["MultiReg", "ResetSynchronizer", "PulseSynchronizer", "Gearbox"]
5+
__all__ = [
6+
"MultiReg",
7+
"ResetSynchronizer",
8+
"PulseSynchronizer",
9+
"BusSynchronizer",
10+
"Gearbox"
11+
]
612

713

814
class MultiReg:
@@ -122,7 +128,6 @@ class PulseSynchronizer:
122128
123129
Parameters
124130
----------
125-
126131
idomain : str
127132
Name of input clock domain.
128133
odomain : str
@@ -157,6 +162,96 @@ def elaborate(self, platform):
157162

158163
return m
159164

165+
class BusSynchronizer:
166+
"""Pass a multi-bit signal safely between clock domains.
167+
168+
Ensures that all bits presented at `o` form a single word
169+
that was present synchronously at `i` in the input clock
170+
domain (unlike direct use of MultiReg).
171+
172+
Parameters
173+
----------
174+
width : int > 0
175+
Width of the bus to be synchronised
176+
idomain : str
177+
Name of input clock domain
178+
odomain : str
179+
Name of output clock domain
180+
sync_stages : int > 1
181+
Number of synchronisation stages used in the req/ack
182+
pulse synchronisers. Lower than 2 is unsafe. Higher
183+
values increase safety for high-frequency designs,
184+
but increase latency too.
185+
timeout : int >= 0
186+
The request from idomain is re-sent if `timeout` cycles
187+
elapse without a response.
188+
`timeout` = 0 disables this feature.
189+
190+
Attributes
191+
----------
192+
i : Signal(width), in
193+
Input signal, sourced from `idomain`
194+
o : Signal(width), out
195+
Resynchronised version of `i`, driven to `odomain`
196+
"""
197+
def __init__(self, width, idomain, odomain, sync_stages=2, timeout = 127):
198+
if not isinstance(width, int) or width < 1:
199+
raise TypeError("width must be a positive integer, not '{!r}'".format(sync_stages))
200+
if not isinstance(sync_stages, int) or sync_stages < 2:
201+
raise TypeError("sync_stages must be an integer > 1, not '{!r}'".format(sync_stages))
202+
if not isinstance(timeout, int) or timeout < 0:
203+
raise TypeError("timeout must be a non-negative integer, not '{!r}'".format(sync_stages))
204+
205+
self.i = Signal(width)
206+
self.o = Signal(width, attrs={"no_retiming": True})
207+
self.width = width
208+
self.idomain = idomain
209+
self.odomain = odomain
210+
self.sync_stages = sync_stages
211+
self.timeout = timeout
212+
213+
def elaborate(self, platform):
214+
m = Module()
215+
if self.width == 1:
216+
m.submodules += MultiReg(self.i, self.o, odomain=self.odomain, n=self.sync_stages)
217+
return m
218+
219+
req = Signal()
220+
ack_o = Signal()
221+
ack_i = Signal()
222+
223+
sync_io = m.submodules.sync_io = \
224+
PulseSynchronizer(self.idomain, self.odomain, self.sync_stages)
225+
sync_oi = m.submodules.sync_oi = \
226+
PulseSynchronizer(self.odomain, self.idomain, self.sync_stages)
227+
228+
if self.timeout != 0:
229+
countdown = Signal(max=self.timeout, reset=self.timeout)
230+
with m.If(ack_i | req):
231+
m.d[self.idomain] += countdown.eq(self.timeout)
232+
with m.Else():
233+
m.d[self.idomain] += countdown.eq(countdown - countdown.bool())
234+
235+
start = Signal(reset=1)
236+
m.d[self.idomain] += start.eq(0)
237+
m.d.comb += [
238+
req.eq(start | ack_i | (self.timeout != 0 and countdown == 0)),
239+
sync_io.i.eq(req),
240+
ack_o.eq(sync_io.o),
241+
sync_oi.i.eq(ack_o),
242+
ack_i.eq(sync_oi.o)
243+
]
244+
245+
buf_i = Signal(self.width, attrs={"no_retiming": True})
246+
buf_o = Signal(self.width)
247+
with m.If(req):
248+
m.d[self.idomain] += buf_i.eq(self.i)
249+
sync_data = m.submodules.sync_data = \
250+
MultiReg(buf_i, buf_o, odomain=self.odomain, n=self.sync_stages - 1)
251+
with m.If(ack_o):
252+
m.d[self.odomain] += self.o.eq(buf_o)
253+
254+
return m
160255

161256
class Gearbox:
162257
"""Adapt the width of a continous datastream.

nmigen/test/test_lib_cdc.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,52 @@ def process():
155155
sim.add_process(process)
156156
sim.run()
157157

158+
class BusSynchronizerTestCase(FHDLTestCase):
159+
def test_paramcheck(self):
160+
with self.assertRaises(TypeError):
161+
bs = BusSynchronizer(0, "i", "o")
162+
with self.assertRaises(TypeError):
163+
bs = BusSynchronizer("x", "i", "o")
164+
165+
bs = BusSynchronizer(1, "i", "o")
166+
167+
with self.assertRaises(TypeError):
168+
bs = BusSynchronizer(1, "i", "o", sync_stages = 1)
169+
with self.assertRaises(TypeError):
170+
bs = BusSynchronizer(1, "i", "o", sync_stages = "a")
171+
with self.assertRaises(TypeError):
172+
bs = BusSynchronizer(1, "i", "o", timeout=-1)
173+
with self.assertRaises(TypeError):
174+
bs = BusSynchronizer(1, "i", "o", timeout="a")
175+
176+
bs = BusSynchronizer(1, "i", "o", timeout=0)
177+
178+
def test_smoke_w1(self):
179+
self.check_smoke(width=1, timeout=127)
180+
181+
def test_smoke_normalcase(self):
182+
self.check_smoke(width=8, timeout=127)
183+
184+
def test_smoke_notimeout(self):
185+
self.check_smoke(width=8, timeout=0)
186+
187+
def check_smoke(self, width, timeout):
188+
m = Module()
189+
m.domains += ClockDomain("sync")
190+
bs = m.submodules.dut = BusSynchronizer(width, "sync", "sync", timeout=timeout)
191+
192+
with Simulator(m, vcd_file = open("test.vcd", "w")) as sim:
193+
sim.add_clock(1e-6)
194+
def process():
195+
for i in range(10):
196+
testval = i % (2 ** width)
197+
yield bs.i.eq(testval)
198+
# 6-cycle round trip, and if one in progress, must complete first:
199+
for j in range(11):
200+
yield Tick()
201+
self.assertEqual((yield bs.o), testval)
202+
sim.add_process(process)
203+
sim.run()
158204

159205
# TODO: test with distinct clocks
160206
# (since we can currently only test symmetric aspect ratio)

0 commit comments

Comments
 (0)