Skip to content

Commit ca4bc09

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

File tree

2 files changed

+213
-1
lines changed

2 files changed

+213
-1
lines changed

nmigen/lib/cdc.py

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