Skip to content

Commit 3a6756e

Browse files
committed
lib.cdc: Add PulseSynchronizer and Gearbox modules, and smoke tests for these
1 parent dcc1815 commit 3a6756e

File tree

2 files changed

+217
-1
lines changed

2 files changed

+217
-1
lines changed

nmigen/lib/cdc.py

Lines changed: 138 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
from .. import *
2+
from math import gcd
23

34

4-
__all__ = ["MultiReg", "ResetSynchronizer"]
5+
__all__ = ["MultiReg", "ResetSynchronizer", "PulseSynchronizer", "Gearbox"]
6+
7+
8+
def _incr(signal, modulo):
9+
if modulo == 2 ** len(signal):
10+
return signal + 1
11+
else:
12+
return Mux(signal == modulo - 1, 0, signal + 1)
513

614

715
class MultiReg:
@@ -107,3 +115,132 @@ def elaborate(self, platform):
107115
ResetSignal(self.domain).eq(self._regs[-1])
108116
]
109117
return m
118+
119+
120+
class PulseSynchronizer:
121+
"""A one-clock pulse on the input produces a one-clock pulse on the output.
122+
123+
If the output clock is faster than the input clock, then the input
124+
may be safely asserted at 100% duty cycle; otherwise some level of
125+
sparsity is required at the input to avoid pulses being dropped.
126+
127+
Other than this there is no constraint on the relationship between
128+
input and output clock frequency.
129+
130+
Parameters
131+
----------
132+
133+
idomain : str
134+
Name of input clock domain.
135+
odomain : str
136+
Name of output clock domain.
137+
sync_stages : int
138+
Number of synchronisation flops between the two clock domains.
139+
2 is the default, and minimum safe value. High-frequency designs
140+
may choose to increase this.
141+
"""
142+
def __init__(self, idomain, odomain, sync_stages=2):
143+
if not isinstance(sync_stages, int) or sync_stages < 1:
144+
raise TypeError("sync_stages must be a positive integer, not '{!r}'".format(sync_stages))
145+
146+
self.i = Signal()
147+
self.o = Signal()
148+
self.idomain = idomain
149+
self.odomain = odomain
150+
self.sync_stages = sync_stages
151+
152+
def elaborate(self, platform):
153+
m = Module()
154+
155+
itoggle = Signal()
156+
otoggle = Signal()
157+
mreg = m.submodules.mreg = \
158+
MultiReg(itoggle, otoggle, odomain=self.odomain, n=self.sync_stages)
159+
otoggle_prev = Signal()
160+
161+
m.d[self.idomain] += itoggle.eq(itoggle ^ self.i)
162+
m.d[self.odomain] += otoggle_prev.eq(otoggle)
163+
m.d.comb += self.o.eq(otoggle ^ otoggle_prev)
164+
165+
return m
166+
167+
168+
class Gearbox:
169+
"""Adapt the width of a continous datastream.
170+
171+
Input: m bits wide, clock frequency f MHz.
172+
Output: n bits wide, clock frequency m / n * f MHz.
173+
174+
Used to adjust width of a datastream when interfacing
175+
system logic to a SerDes. The input and output clocks
176+
must be derived from the same frequency reference,
177+
to avoid long-term phase drift.
178+
179+
Parameters
180+
----------
181+
iwidth : int
182+
Bit width of the input
183+
idomain : str
184+
Name of input clock domain
185+
owidth : int
186+
Bit width of the output
187+
odomain : str
188+
Name of output clock domain
189+
190+
Attributes
191+
----------
192+
i : Signal(iwidth), in
193+
Input datastream. Sampled on every input clock.
194+
o : Signal(owidth), out
195+
Output datastream. Transitions on every output clock.
196+
"""
197+
def __init__(self, iwidth, idomain, owidth, odomain):
198+
if not isinstance(iwidth, int) or iwidth < 1:
199+
raise TypeError("iwidth must be a positive integer, not '{!r}'".format(iwidth))
200+
if not isinstance(owidth, int) or owidth < 1:
201+
raise TypeError("owidth must be a positive integer, not '{!r}'".format(owidth))
202+
203+
self.i = Signal(iwidth)
204+
self.o = Signal(owidth)
205+
self.iwidth = iwidth
206+
self.idomain = idomain
207+
self.owidth = owidth
208+
self.odomain = odomain
209+
210+
storagesize = iwidth * owidth // gcd(iwidth, owidth)
211+
while storagesize // iwidth < 4:
212+
storagesize *= 2
213+
while storagesize // owidth < 4:
214+
storagesize *= 2
215+
216+
self._storagesize = storagesize
217+
self._ichunks = storagesize // self.iwidth
218+
self._ochunks = storagesize // self.owidth
219+
assert(self._ichunks * self.iwidth == storagesize)
220+
assert(self._ochunks * self.owidth == storagesize)
221+
222+
def elaborate(self, platform):
223+
m = Module()
224+
225+
storage = Signal(self._storagesize)
226+
i_faster = self._ichunks > self._ochunks
227+
iptr = Signal(max=self._ichunks - 1, reset=(self._ichunks // 2 if i_faster else 0))
228+
optr = Signal(max=self._ochunks - 1, reset=(0 if i_faster else self._ochunks // 2))
229+
230+
231+
m.d[self.idomain] += iptr.eq(_incr(iptr, self._storagesize))
232+
m.d[self.odomain] += optr.eq(_incr(optr, self._storagesize))
233+
234+
with m.Switch(iptr):
235+
for n in range(self._ichunks):
236+
s = slice(n * self.iwidth, (n + 1) * self.iwidth)
237+
with m.Case(n):
238+
m.d[self.idomain] += storage[s].eq(self.i)
239+
240+
with m.Switch(optr):
241+
for n in range(self._ochunks):
242+
s = slice(n * self.owidth, (n + 1) * self.owidth)
243+
with m.Case(n):
244+
m.d[self.odomain] += self.o.eq(storage[s])
245+
246+
return m

nmigen/test/test_lib_cdc.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,82 @@ def process():
103103
yield Tick(); yield Delay(1e-8)
104104
sim.add_process(process)
105105
sim.run()
106+
107+
108+
# TODO: test with distinct clocks
109+
class PulseSynchronizerTestCase(FHDLTestCase):
110+
def test_paramcheck(self):
111+
with self.assertRaises(TypeError):
112+
ps = PulseSynchronizer("w", "r", sync_stages=0)
113+
with self.assertRaises(TypeError):
114+
ps = PulseSynchronizer("w", "r", sync_stages="abc")
115+
ps = PulseSynchronizer("w", "r", sync_stages = 1)
116+
117+
def test_smoke(self):
118+
m = Module()
119+
m.domains += ClockDomain("sync")
120+
ps = m.submodules.dut = PulseSynchronizer("sync", "sync")
121+
122+
with Simulator(m, vcd_file = open("test.vcd", "w")) as sim:
123+
sim.add_clock(1e-6)
124+
def process():
125+
yield ps.i.eq(0)
126+
# TODO: think about reset
127+
for n in range(5):
128+
yield Tick()
129+
# Make sure no pulses are generated in quiescent state
130+
for n in range(3):
131+
yield Tick()
132+
self.assertEqual((yield ps.o), 0)
133+
# Check conservation of pulses
134+
accum = 0
135+
for n in range(10):
136+
yield ps.i.eq(1 if n < 4 else 0)
137+
yield Tick()
138+
accum += yield ps.o
139+
self.assertEqual(accum, 4)
140+
sim.add_process(process)
141+
sim.run()
142+
143+
144+
# TODO: test with distinct clocks
145+
# (since we can currently only test symmetric aspect ratio)
146+
class GearboxTestCase(FHDLTestCase):
147+
def test_paramcheck(self):
148+
with self.assertRaises(TypeError):
149+
g = Gearbox(0, "i", 1, "o")
150+
with self.assertRaises(TypeError):
151+
g = Gearbox(1, "i", 0, "o")
152+
with self.assertRaises(TypeError):
153+
g = Gearbox("x", "i", 1, "o")
154+
with self.assertRaises(TypeError):
155+
g = Gearbox(1, "i", "x", "o")
156+
g = Gearbox(1, "i", 1, "o")
157+
g = Gearbox(7, "i", 1, "o")
158+
g = Gearbox(7, "i", 3, "o")
159+
g = Gearbox(7, "i", 7, "o")
160+
g = Gearbox(3, "i", 7, "o")
161+
162+
def test_smoke_symmetric(self):
163+
m = Module()
164+
m.domains += ClockDomain("sync")
165+
g = m.submodules.dut = Gearbox(8, "sync", 8, "sync")
166+
167+
with Simulator(m, vcd_file = open("test.vcd", "w")) as sim:
168+
sim.add_clock(1e-6)
169+
def process():
170+
pipeline_filled = False
171+
expected_out = 1
172+
yield Tick()
173+
for i in range(g._ichunks * 4):
174+
yield g.i.eq(i)
175+
if (yield g.o):
176+
pipeline_filled = True
177+
if pipeline_filled:
178+
self.assertEqual((yield g.o), expected_out)
179+
expected_out += 1
180+
yield Tick()
181+
self.assertEqual(pipeline_filled, True)
182+
self.assertEqual(expected_out > g._ichunks * 2, True)
183+
sim.add_process(process)
184+
sim.run()

0 commit comments

Comments
 (0)