Skip to content

Commit bad43d2

Browse files
committed
lib.cdc: Implement BusSynchronizer and add smoke tests
1 parent d56567c commit bad43d2

File tree

2 files changed

+139
-2
lines changed

2 files changed

+139
-2
lines changed

nmigen/lib/cdc.py

Lines changed: 93 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
def _incr(signal, modulo):
@@ -135,7 +141,6 @@ class PulseSynchronizer:
135141
136142
Parameters
137143
----------
138-
139144
idomain : str
140145
Name of input clock domain.
141146
odomain : str
@@ -169,6 +174,92 @@ def elaborate(self, platform):
169174

170175
return m
171176

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

173264
class Gearbox:
174265
"""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)