Skip to content

Commit 393b14d

Browse files
committed
lib.cdc: Implement BusSynchronizer and add smoke tests
1 parent 68186d5 commit 393b14d

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
def _incr(signal, modulo):
@@ -129,7 +135,6 @@ class PulseSynchronizer:
129135
130136
Parameters
131137
----------
132-
133138
idomain : str
134139
Name of input clock domain.
135140
odomain : str
@@ -164,6 +169,96 @@ def elaborate(self, platform):
164169

165170
return m
166171

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

168263
class Gearbox:
169264
"""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)