-
Notifications
You must be signed in to change notification settings - Fork 57
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Expand and document lib.cdc #40
Open
Wren6991
wants to merge
9
commits into
m-labs:master
Choose a base branch
from
Wren6991:cdc_lib
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
8af1873
lib.cdc: Add docstrings and basic parameter checking to MultiReg and …
Wren6991 4608d9f
lib.cdc: Add PulseSynchronizer and Gearbox modules, and smoke tests f…
Wren6991 41ca5ac
test_lib_cdc: test platform dependency injection
Wren6991 e76c187
lib.cdc: Implement BusSynchronizer and add smoke tests
Wren6991 d4a6f28
lib.cdc: Add ElasticBuffer and smoke test
Wren6991 1ea1a9d
BusSynchronizer: lengthen request path, rather than shortening data p…
Wren6991 8357fbc
lib.cdc: Fix description of ResetSynchronizer
Wren6991 c84cd8d
lib.cdc: update clock domain naming conventions according to #97 (bik…
Wren6991 2e6cf3c
lib.cdc: consistent naming conventions for Gearbox constructor arguments
Wren6991 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,22 @@ | ||
from .. import * | ||
from math import gcd | ||
|
||
|
||
__all__ = ["MultiReg", "ResetSynchronizer"] | ||
__all__ = [ | ||
"MultiReg", | ||
"ResetSynchronizer", | ||
"PulseSynchronizer", | ||
"BusSynchronizer", | ||
"ElasticBuffer", | ||
"Gearbox" | ||
] | ||
|
||
|
||
def _incr(signal, modulo): | ||
if modulo == 2 ** len(signal): | ||
return signal + 1 | ||
else: | ||
return Mux(signal == modulo - 1, 0, signal + 1) | ||
|
||
|
||
class MultiReg(Elaboratable): | ||
|
@@ -16,15 +31,15 @@ class MultiReg(Elaboratable): | |
Signal to be resynchronised | ||
o : Signal(), out | ||
Signal connected to synchroniser output | ||
odomain : str | ||
Name of output clock domain | ||
cd_o : str | ||
Name of output (capturing) clock domain | ||
n : int | ||
Number of flops between input and output. | ||
reset : int | ||
Reset value of the flip-flops. On FPGAs, even if ``reset_less`` is True, the MultiReg is | ||
still set to this value during initialization. | ||
reset_less : bool | ||
If True (the default), this MultiReg is unaffected by ``odomain`` reset. | ||
If True (the default), this MultiReg is unaffected by ``cd_o`` reset. | ||
See "Note on Reset" below. | ||
|
||
Platform override | ||
|
@@ -42,17 +57,19 @@ class MultiReg(Elaboratable): | |
consider setting ``reset_less`` to False if any of the following is true: | ||
|
||
- You are targeting an ASIC, or an FPGA that does not allow arbitrary initial flip-flop states; | ||
- Your design features warm (non-power-on) resets of ``odomain``, so the one-time | ||
- Your design features warm (non-power-on) resets of ``cd_o``, so the one-time | ||
initialization at power on is insufficient; | ||
- Your design features a sequenced reset, and the MultiReg must maintain its reset value until | ||
``odomain`` reset specifically is deasserted. | ||
``cd_o`` reset specifically is deasserted. | ||
|
||
MultiReg is reset by the ``odomain`` reset only. | ||
MultiReg is reset by the ``cd_o`` reset only. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
""" | ||
def __init__(self, i, o, odomain="sync", n=2, reset=0, reset_less=True): | ||
def __init__(self, i, o, cd_o="sync", n=2, reset=0, reset_less=True): | ||
if not isinstance(n, int) or n < 1: | ||
raise TypeError("n must be a positive integer, not '{!r}'".format(n)) | ||
self.i = i | ||
self.o = o | ||
self.odomain = odomain | ||
self.cd_o = cd_o | ||
|
||
self._regs = [Signal(self.i.shape(), name="cdc{}".format(i), | ||
reset=reset, reset_less=reset_less, attrs={"no_retiming": True}) | ||
|
@@ -64,15 +81,35 @@ def elaborate(self, platform): | |
|
||
m = Module() | ||
for i, o in zip((self.i, *self._regs), self._regs): | ||
m.d[self.odomain] += o.eq(i) | ||
m.d[self.cd_o] += o.eq(i) | ||
m.d.comb += self.o.eq(self._regs[-1]) | ||
return m | ||
|
||
|
||
class ResetSynchronizer(Elaboratable): | ||
def __init__(self, arst, domain="sync", n=2): | ||
"""Synchronize the deassertion of a reset to a local clock. | ||
|
||
Output `assertion` is asynchronous, so the local clock need not be free-running. | ||
|
||
Parameters | ||
---------- | ||
arst : Signal(1), out | ||
Asynchronous reset signal, to be synchronized. | ||
cd : str | ||
Name of clock domain to synchronize reset to. | ||
n : int, >=1 | ||
Number of metastability flops between input and output | ||
|
||
Override | ||
-------- | ||
Define the ``get_reset_sync`` platform attribute to override the implementation of | ||
ResetSynchronizer, e.g. to instantiate library cells directly. | ||
""" | ||
def __init__(self, arst, cd="sync", n=2): | ||
if not isinstance(n, int) or n < 1: | ||
raise TypeError("n must be a positive integer, not '{!r}'".format(n)) | ||
self.arst = arst | ||
self.domain = domain | ||
self.cd = cd | ||
|
||
self._regs = [Signal(name="arst{}".format(i), reset=1, | ||
attrs={"no_retiming": True}) | ||
|
@@ -87,8 +124,279 @@ def elaborate(self, platform): | |
for i, o in zip((0, *self._regs), self._regs): | ||
m.d._reset_sync += o.eq(i) | ||
m.d.comb += [ | ||
ClockSignal("_reset_sync").eq(ClockSignal(self.domain)), | ||
ClockSignal("_reset_sync").eq(ClockSignal(self.cd)), | ||
ResetSignal("_reset_sync").eq(self.arst), | ||
ResetSignal(self.domain).eq(self._regs[-1]) | ||
ResetSignal(self.cd).eq(self._regs[-1]) | ||
] | ||
return m | ||
|
||
|
||
class PulseSynchronizer(Elaboratable): | ||
"""A one-clock pulse on the input produces a one-clock pulse on the output. | ||
|
||
If the output clock is faster than the input clock, then the input may be safely asserted at | ||
100% duty cycle. Otherwise, if the clock ratio is n : 1, the input may be asserted at most once | ||
in every n input clocks, else pulses may be dropped. | ||
|
||
Other than this there is no constraint on the ratio of input and output clock frequency. | ||
|
||
Parameters | ||
---------- | ||
cd_i : str | ||
Name of input clock domain. | ||
cd_o : str | ||
Name of output clock domain. | ||
sync_stages : int | ||
Number of synchronisation flops between the two clock domains. 2 is the default, and | ||
minimum safe value. High-frequency designs may choose to increase this. | ||
""" | ||
def __init__(self, cd_i, cd_o, sync_stages=2): | ||
if not isinstance(sync_stages, int) or sync_stages < 1: | ||
raise TypeError("sync_stages must be a positive integer, not '{!r}'".format(sync_stages)) | ||
|
||
self.i = Signal() | ||
self.o = Signal() | ||
self.cd_i = cd_i | ||
self.cd_o = cd_o | ||
self.sync_stages = sync_stages | ||
|
||
def elaborate(self, platform): | ||
m = Module() | ||
|
||
itoggle = Signal() | ||
otoggle = Signal() | ||
mreg = m.submodules.mreg = \ | ||
MultiReg(itoggle, otoggle, cd_o=self.cd_o, n=self.sync_stages) | ||
otoggle_prev = Signal() | ||
|
||
m.d[self.cd_i] += itoggle.eq(itoggle ^ self.i) | ||
m.d[self.cd_o] += otoggle_prev.eq(otoggle) | ||
m.d.comb += self.o.eq(otoggle ^ otoggle_prev) | ||
|
||
return m | ||
|
||
class BusSynchronizer(Elaboratable): | ||
"""Pass a multi-bit signal safely between clock domains. | ||
|
||
Ensures that all bits presented at ``o`` form a single word that was present synchronously at | ||
``i`` in the input clock domain (unlike direct use of MultiReg). | ||
|
||
Parameters | ||
---------- | ||
width : int > 0 | ||
Width of the bus to be synchronized | ||
cd_i : str | ||
Name of input clock domain | ||
cd_o : str | ||
Name of output clock domain | ||
sync_stages : int >= 2 | ||
Number of synchronisation stages used in the req/ack pulse synchronizers. Lower than 2 is | ||
unsafe. Higher values increase safety for high-frequency designs, but increase latency too. | ||
timeout : int >= 0 | ||
The request from cd_i is re-sent if ``timeout`` cycles elapse without a response. | ||
``timeout`` = 0 disables this feature. | ||
|
||
Attributes | ||
---------- | ||
i : Signal(width), in | ||
Input signal, sourced from ``cd_i`` | ||
o : Signal(width), out | ||
Resynchronized version of ``i``, driven to ``cd_o`` | ||
""" | ||
def __init__(self, width, cd_i, cd_o, sync_stages=2, timeout = 127): | ||
if not isinstance(width, int) or width < 1: | ||
raise TypeError("width must be a positive integer, not '{!r}'".format(width)) | ||
if not isinstance(sync_stages, int) or sync_stages < 2: | ||
raise TypeError("sync_stages must be an integer > 1, not '{!r}'".format(sync_stages)) | ||
if not isinstance(timeout, int) or timeout < 0: | ||
raise TypeError("timeout must be a non-negative integer, not '{!r}'".format(timeout)) | ||
|
||
self.i = Signal(width) | ||
self.o = Signal(width, attrs={"no_retiming": True}) | ||
self.width = width | ||
self.cd_i = cd_i | ||
self.cd_o = cd_o | ||
self.sync_stages = sync_stages | ||
self.timeout = timeout | ||
|
||
def elaborate(self, platform): | ||
m = Module() | ||
if self.width == 1: | ||
m.submodules += MultiReg(self.i, self.o, cd_o=self.cd_o, n=self.sync_stages) | ||
return m | ||
|
||
req = Signal() | ||
ack_o = Signal() | ||
ack_i = Signal() | ||
|
||
# Extra flop on i->o to avoid race between data and request | ||
sync_io = m.submodules.sync_io = \ | ||
PulseSynchronizer(self.cd_i, self.cd_o, self.sync_stages + 1) | ||
sync_oi = m.submodules.sync_oi = \ | ||
PulseSynchronizer(self.cd_o, self.cd_i, self.sync_stages) | ||
|
||
if self.timeout != 0: | ||
countdown = Signal(max=self.timeout, reset=self.timeout) | ||
with m.If(ack_i | req): | ||
m.d[self.cd_i] += countdown.eq(self.timeout) | ||
with m.Else(): | ||
m.d[self.cd_i] += countdown.eq(countdown - countdown.bool()) | ||
|
||
start = Signal(reset=1) | ||
m.d[self.cd_i] += start.eq(0) | ||
m.d.comb += [ | ||
req.eq(start | ack_i | (self.timeout != 0 and countdown == 0)), | ||
sync_io.i.eq(req), | ||
ack_o.eq(sync_io.o), | ||
sync_oi.i.eq(ack_o), | ||
ack_i.eq(sync_oi.o) | ||
] | ||
|
||
buf_i = Signal(self.width, attrs={"no_retiming": True}) | ||
buf_o = Signal(self.width) | ||
with m.If(ack_i): | ||
m.d[self.cd_i] += buf_i.eq(self.i) | ||
sync_data = m.submodules.sync_data = \ | ||
MultiReg(buf_i, buf_o, cd_o=self.cd_o, n=self.sync_stages) | ||
with m.If(ack_o): | ||
m.d[self.cd_o] += self.o.eq(buf_o) | ||
|
||
return m | ||
|
||
class ElasticBuffer(Elaboratable): | ||
"""Pass data between two clock domains with the same frequency, and bounded phase difference. | ||
|
||
Increasing the storage depth increases tolerance for clock wander and jitter, but still within | ||
some bound. For less-well-behaved clocks, consider AsyncFIFO. | ||
|
||
Parameters | ||
---------- | ||
width : int > 0 | ||
Width of databus to be resynchronized | ||
depth : int > 1 | ||
Number of storage elements in buffer | ||
cd_i : str | ||
Name of input clock domain | ||
cd_o : str | ||
Name of output clock domain | ||
|
||
Attributes | ||
---------- | ||
i : Signal(width) | ||
Input data bus | ||
o : Signal(width) | ||
Output data bus | ||
""" | ||
def __init__(self, width, depth, cd_i, cd_o): | ||
if not isinstance(width, int) or width < 1: | ||
raise TypeError("width must be a positive integer, not '{!r}'".format(width)) | ||
if not isinstance(depth, int) or depth <= 1: | ||
raise TypeError("depth must be an integer > 1, not '{!r}'".format(depth)) | ||
|
||
self.i = Signal(width) | ||
self.o = Signal(width) | ||
self.width = width | ||
self.depth = depth | ||
self.cd_i = cd_i | ||
self.cd_o = cd_o | ||
|
||
def elaborate(self, platform): | ||
m = Module() | ||
|
||
wptr = Signal(max=self.depth, reset=self.depth // 2) | ||
rptr = Signal(max=self.depth) | ||
m.d[self.cd_i] += wptr.eq(_incr(wptr, self.depth)) | ||
m.d[self.cd_o] += rptr.eq(_incr(rptr, self.depth)) | ||
|
||
storage = Memory(self.width, self.depth) | ||
wport = m.submodules.wport = storage.write_port(domain=self.cd_i) | ||
rport = m.submodules.rport = storage.read_port(domain=self.cd_o) | ||
|
||
m.d.comb += [ | ||
wport.en.eq(1), | ||
wport.addr.eq(wptr), | ||
wport.data.eq(self.i), | ||
rport.addr.eq(rptr), | ||
self.o.eq(rport.data) | ||
] | ||
|
||
return m | ||
|
||
|
||
class Gearbox(Elaboratable): | ||
"""Adapt the width of a continous datastream. | ||
|
||
Input: m bits wide, clock frequency f MHz. | ||
Output: n bits wide, clock frequency m / n * f MHz. | ||
|
||
Used to adjust width of a datastream when interfacing system logic to a SerDes. The input and | ||
output clocks must be derived from the same reference clock, to maintain distance between | ||
read and write pointers. | ||
|
||
Parameters | ||
---------- | ||
width_i : int | ||
Bit width of the input | ||
cd_i : str | ||
Name of input clock domain | ||
width_o : int | ||
Bit width of the output | ||
cd_o : str | ||
Name of output clock domain | ||
|
||
Attributes | ||
---------- | ||
i : Signal(width_i), in | ||
Input datastream. Sampled on every input clock. | ||
o : Signal(width_o), out | ||
Output datastream. Transitions on every output clock. | ||
""" | ||
def __init__(self, width_i, cd_i, width_o, cd_o): | ||
if not isinstance(width_i, int) or width_i < 1: | ||
raise TypeError("width_i must be a positive integer, not '{!r}'".format(width_i)) | ||
if not isinstance(width_o, int) or width_o < 1: | ||
raise TypeError("width_o must be a positive integer, not '{!r}'".format(width_o)) | ||
|
||
self.i = Signal(width_i) | ||
self.o = Signal(width_o) | ||
self.width_i = width_i | ||
self.cd_i = cd_i | ||
self.width_o = width_o | ||
self.cd_o = cd_o | ||
|
||
storagesize = width_i * width_o // gcd(width_i, width_o) | ||
while storagesize // width_i < 4: | ||
storagesize *= 2 | ||
while storagesize // width_o < 4: | ||
storagesize *= 2 | ||
|
||
self._storagesize = storagesize | ||
self._ichunks = storagesize // self.width_i | ||
self._ochunks = storagesize // self.width_o | ||
assert(self._ichunks * self.width_i == storagesize) | ||
assert(self._ochunks * self.width_o == storagesize) | ||
|
||
def elaborate(self, platform): | ||
m = Module() | ||
|
||
storage = Signal(self._storagesize, attrs={"no_retiming": True}) | ||
i_faster = self._ichunks > self._ochunks | ||
iptr = Signal(max=self._ichunks - 1, reset=(self._ichunks // 2 if i_faster else 0)) | ||
optr = Signal(max=self._ochunks - 1, reset=(0 if i_faster else self._ochunks // 2)) | ||
|
||
m.d[self.cd_i] += iptr.eq(_incr(iptr, self._storagesize)) | ||
m.d[self.cd_o] += optr.eq(_incr(optr, self._storagesize)) | ||
|
||
with m.Switch(iptr): | ||
for n in range(self._ichunks): | ||
s = slice(n * self.width_i, (n + 1) * self.width_i) | ||
with m.Case(n): | ||
m.d[self.cd_i] += storage[s].eq(self.i) | ||
|
||
with m.Switch(optr): | ||
for n in range(self._ochunks): | ||
s = slice(n * self.width_o, (n + 1) * self.width_o) | ||
with m.Case(n): | ||
m.d[self.cd_o] += self.o.eq(storage[s]) | ||
|
||
return m |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's already one in
lib.fifo
and this is getting ridiculous.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, it should be either inlined at my callsites, or preferably factored out. Wanted to keep the scope of this PR well-defined though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you think of a nicer place to put this? It's a common pattern (as is saturating increment/decrement)