Skip to content

Commit 8b9f855

Browse files
committed
lib.cdc: Add PulseSynchronizer and Gearbox modules, and smoke tests for these
1 parent a27c13e commit 8b9f855

File tree

2 files changed

+210
-1
lines changed

2 files changed

+210
-1
lines changed

nmigen/lib/cdc.py

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

34

4-
__all__ = ["MultiReg", "ResetSynchronizer"]
5+
__all__ = ["MultiReg", "ResetSynchronizer", "PulseSynchronizer", "Gearbox"]
56

67

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