Compare commits
10 commits
66ad0a207e
...
02f63ba874
Author | SHA1 | Date | |
---|---|---|---|
![]() |
02f63ba874 | ||
![]() |
bf4f22d2d5 | ||
![]() |
de685ca026 | ||
![]() |
a0750b89c6 | ||
![]() |
3d2cd15435 | ||
![]() |
f3bcdf4782 | ||
![]() |
64809bea99 | ||
![]() |
2c0265d827 | ||
![]() |
3c64c66b5c | ||
![]() |
f5a8c07d54 |
|
@ -76,16 +76,6 @@ def _ignore_deprecated(f=None):
|
|||
return decorator_like
|
||||
|
||||
|
||||
def extend(cls):
|
||||
def decorator(f):
|
||||
if isinstance(f, property):
|
||||
name = f.fget.__name__
|
||||
else:
|
||||
name = f.__name__
|
||||
setattr(cls, name, f)
|
||||
return decorator
|
||||
|
||||
|
||||
def get_linter_options(filename):
|
||||
first_line = linecache.getline(filename, 1)
|
||||
if first_line:
|
||||
|
|
|
@ -23,8 +23,8 @@ def _convert_rtlil_text(rtlil_text, black_boxes, *, src_loc_at=0):
|
|||
script = []
|
||||
if black_boxes is not None:
|
||||
for box_name, box_source in black_boxes.items():
|
||||
script.append(f"read_ilang <<rtlil\n{box_source}\nrtlil")
|
||||
script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil")
|
||||
script.append(f"read_rtlil <<rtlil\n{box_source}\nrtlil")
|
||||
script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil")
|
||||
script.append("write_cxxrtl")
|
||||
|
||||
return yosys.run(["-q", "-"], "\n".join(script), src_loc_at=1 + src_loc_at)
|
||||
|
|
|
@ -12,7 +12,7 @@ def _convert_rtlil_text(rtlil_text, *, strip_internal_attrs=False, write_verilog
|
|||
yosys = find_yosys(lambda ver: ver >= (0, 40))
|
||||
|
||||
script = []
|
||||
script.append(f"read_ilang <<rtlil\n{rtlil_text}\nrtlil")
|
||||
script.append(f"read_rtlil <<rtlil\n{rtlil_text}\nrtlil")
|
||||
script.append("proc -nomux -norom")
|
||||
script.append("memory_collect")
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ from ..asserts import Initial
|
|||
from ..utils import ceil_log2
|
||||
from .cdc import FFSynchronizer, AsyncFFSynchronizer
|
||||
from .memory import Memory
|
||||
from . import stream
|
||||
|
||||
|
||||
__all__ = ["FIFOInterface", "SyncFIFO", "SyncFIFOBuffered", "AsyncFIFO", "AsyncFIFOBuffered"]
|
||||
|
@ -93,6 +94,22 @@ class FIFOInterface:
|
|||
self.r_en = Signal()
|
||||
self.r_level = Signal(range(depth + 1))
|
||||
|
||||
@property
|
||||
def w_stream(self):
|
||||
w_stream = stream.Signature(self.width).flip().create()
|
||||
w_stream.payload = self.w_data
|
||||
w_stream.valid = self.w_en
|
||||
w_stream.ready = self.w_rdy
|
||||
return w_stream
|
||||
|
||||
@property
|
||||
def r_stream(self):
|
||||
r_stream = stream.Signature(self.width).create()
|
||||
r_stream.payload = self.r_data
|
||||
r_stream.valid = self.r_rdy
|
||||
r_stream.ready = self.r_en
|
||||
return r_stream
|
||||
|
||||
|
||||
def _incr(signal, modulo):
|
||||
if modulo == 2 ** len(signal):
|
||||
|
|
122
amaranth/lib/stream.py
Normal file
122
amaranth/lib/stream.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from ..hdl import *
|
||||
from .._utils import final
|
||||
from . import wiring
|
||||
from .wiring import In, Out
|
||||
|
||||
|
||||
@final
|
||||
class Signature(wiring.Signature):
|
||||
"""Signature of a unidirectional data stream.
|
||||
|
||||
.. note::
|
||||
|
||||
"Minimal streams" as defined in `RFC 61`_ lack support for complex payloads, such as
|
||||
multiple lanes or packetization, as well as introspection of the payload. This limitation
|
||||
will be lifted in a later release.
|
||||
|
||||
.. _RFC 61: https://amaranth-lang.org/rfcs/0061-minimal-streams.html
|
||||
|
||||
Parameters
|
||||
----------
|
||||
payload_shape : :class:`~.hdl.ShapeLike`
|
||||
Shape of the payload.
|
||||
always_valid : :class:`bool`
|
||||
Whether the stream has a payload available each cycle.
|
||||
always_ready : :class:`bool`
|
||||
Whether the stream has its payload accepted whenever it is available (i.e. whether it lacks
|
||||
support for backpressure).
|
||||
|
||||
Members
|
||||
-------
|
||||
payload : :py:`Out(payload_shape)`
|
||||
Payload.
|
||||
valid : :py:`Out(1)`
|
||||
Whether a payload is available. If the stream is :py:`always_valid`, :py:`Const(1)`.
|
||||
ready : :py:`In(1)`
|
||||
Whether a payload is accepted. If the stream is :py:`always_ready`, :py:`Const(1)`.
|
||||
"""
|
||||
def __init__(self, payload_shape: ShapeLike, *, always_valid=False, always_ready=False):
|
||||
Shape.cast(payload_shape)
|
||||
self._payload_shape = payload_shape
|
||||
self._always_valid = bool(always_valid)
|
||||
self._always_ready = bool(always_ready)
|
||||
|
||||
super().__init__({
|
||||
"payload": Out(payload_shape),
|
||||
"valid": Out(1),
|
||||
"ready": In(1)
|
||||
})
|
||||
|
||||
# payload_shape intentionally not introspectable (for now)
|
||||
|
||||
@property
|
||||
def always_valid(self):
|
||||
return self._always_valid
|
||||
|
||||
@property
|
||||
def always_ready(self):
|
||||
return self._always_ready
|
||||
|
||||
def __eq__(self, other):
|
||||
return (type(other) is type(self) and
|
||||
other._payload_shape == self._payload_shape and
|
||||
other.always_valid == self.always_valid and
|
||||
other.always_ready == self.always_ready)
|
||||
|
||||
def create(self, *, path=None, src_loc_at=0):
|
||||
return Interface(self, path=path, src_loc_at=1 + src_loc_at)
|
||||
|
||||
def __repr__(self):
|
||||
always_valid_repr = "" if not self._always_valid else ", always_valid=True"
|
||||
always_ready_repr = "" if not self._always_ready else ", always_ready=True"
|
||||
return f"stream.Signature({self._payload_shape!r}{always_valid_repr}{always_ready_repr})"
|
||||
|
||||
|
||||
@final
|
||||
class Interface:
|
||||
"""A unidirectional data stream.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
signature : :class:`Signature`
|
||||
Signature of this data stream.
|
||||
"""
|
||||
|
||||
payload: Signal
|
||||
valid: 'Signal | Const'
|
||||
ready: 'Signal | Const'
|
||||
|
||||
def __init__(self, signature: Signature, *, path=None, src_loc_at=0):
|
||||
if not isinstance(signature, Signature):
|
||||
raise TypeError(f"Signature of stream.Interface must be a stream.Signature, not "
|
||||
f"{signature!r}")
|
||||
self._signature = signature
|
||||
self.__dict__.update(signature.members.create(path=path, src_loc_at=1 + src_loc_at))
|
||||
if signature.always_valid:
|
||||
self.valid = Const(1)
|
||||
if signature.always_ready:
|
||||
self.ready = Const(1)
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
return self._signature
|
||||
|
||||
@property
|
||||
def p(self):
|
||||
"""Shortcut for :py:`self.payload`.
|
||||
|
||||
This shortcut reduces repetition when manipulating the payload, for example:
|
||||
|
||||
.. code::
|
||||
|
||||
m.d.comb += [
|
||||
self.o_stream.p.result.eq(self.i_stream.p.first + self.i_stream.p.second),
|
||||
self.o_stream.valid.eq(self.i_stream.valid),
|
||||
self.i_stream.ready.eq(self.o_stream.ready),
|
||||
]
|
||||
"""
|
||||
return self.payload
|
||||
|
||||
def __repr__(self):
|
||||
return (f"stream.Interface(payload={self.payload!r}, valid={self.valid!r}, "
|
||||
f"ready={self.ready!r})")
|
|
@ -75,7 +75,7 @@ def _serve_yosys(modules):
|
|||
if not port_name.startswith("_") and isinstance(port, (Signal, Record)):
|
||||
ports += port._lhs_signals()
|
||||
rtlil_text = rtlil.convert(elaboratable, name=module_name, ports=ports)
|
||||
response = {"frontend": "ilang", "source": rtlil_text}
|
||||
response = {"frontend": "rtlil", "source": rtlil_text}
|
||||
except Exception as error:
|
||||
response = {"error": f"{type(error).__name__}: {str(error)}"}
|
||||
|
||||
|
|
|
@ -749,10 +749,18 @@ class AsyncProcess(BaseProcess):
|
|||
self.critical = not self.background
|
||||
self.waits_on = None
|
||||
self.coroutine = self.constructor(self.context)
|
||||
self.first_await = True
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
self.waits_on = self.coroutine.send(None)
|
||||
# Special case to make combination logic replacement work correctly: ensure that
|
||||
# a process looping over `changed()` always gets awakened at least once at time 0,
|
||||
# to see the initial values.
|
||||
if self.first_await and self.waits_on.initial_eligible():
|
||||
self.waits_on.compute_result()
|
||||
self.waits_on = self.coroutine.send(None)
|
||||
self.first_await = False
|
||||
except StopIteration:
|
||||
self.critical = False
|
||||
self.waits_on = None
|
||||
|
|
|
@ -555,7 +555,7 @@ class _PyTriggerState:
|
|||
else:
|
||||
self._broken = True
|
||||
|
||||
def run(self):
|
||||
def compute_result(self):
|
||||
result = []
|
||||
for trigger in self._combination._triggers:
|
||||
if isinstance(trigger, (SampleTrigger, ChangedTrigger)):
|
||||
|
@ -570,12 +570,20 @@ class _PyTriggerState:
|
|||
assert False # :nocov:
|
||||
self._result = tuple(result)
|
||||
|
||||
def run(self):
|
||||
self.compute_result()
|
||||
self._combination._process.runnable = True
|
||||
self._combination._process.waits_on = None
|
||||
self._triggers_hit.clear()
|
||||
for waker, interval_fs in self._delay_wakers.items():
|
||||
self._engine.state.set_delay_waker(interval_fs, waker)
|
||||
|
||||
def initial_eligible(self):
|
||||
return not self._oneshot and any(
|
||||
isinstance(trigger, ChangedTrigger)
|
||||
for trigger in self._combination._triggers
|
||||
)
|
||||
|
||||
def __await__(self):
|
||||
self._result = None
|
||||
if self._broken:
|
||||
|
@ -616,6 +624,10 @@ class PySimEngine(BaseEngine):
|
|||
testbench.reset()
|
||||
|
||||
def add_clock_process(self, clock, *, phase, period):
|
||||
slot = self.state.get_signal(clock)
|
||||
if self.state.slots[slot].is_comb:
|
||||
raise DriverConflict("Clock signal is already driven by combinational logic")
|
||||
|
||||
self._processes.add(PyClockProcess(self._state, clock,
|
||||
phase=phase, period=period))
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
__all__ = [
|
||||
"AlteraPlatform",
|
||||
"AMDPlatform",
|
||||
"GateMatePlatform",
|
||||
"GowinPlatform",
|
||||
"IntelPlatform",
|
||||
"LatticeECP5Platform",
|
||||
|
@ -30,6 +31,9 @@ def __getattr__(name):
|
|||
if name == "GowinPlatform":
|
||||
from ._gowin import GowinPlatform
|
||||
return GowinPlatform
|
||||
if name == "GateMatePlatform":
|
||||
from ._colognechip import GateMatePlatform
|
||||
return GateMatePlatform
|
||||
if name in ("LatticePlatform", "LatticeECP5Platform", "LatticeMachXO2Platform",
|
||||
"LatticeMachXO3LPlatform"):
|
||||
from ._lattice import LatticePlatform
|
||||
|
|
|
@ -222,7 +222,7 @@ class AlteraPlatform(TemplatedPlatform):
|
|||
* ``verbose``: enables logging of informational messages to standard error.
|
||||
* ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
|
||||
* ``synth_opts``: adds options for ``synth_intel_alm`` Yosys command.
|
||||
* ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
|
||||
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
|
||||
* ``script_after_synth``: inserts commands after ``synth_intel_alm`` in Yosys script.
|
||||
* ``yosys_opts``: adds extra options for ``yosys``.
|
||||
* ``nextpnr_opts``: adds extra options for ``nextpnr-mistral``.
|
||||
|
@ -374,9 +374,9 @@ class AlteraPlatform(TemplatedPlatform):
|
|||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_ilang {{file}}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_ilang {{name}}.il
|
||||
read_rtlil {{name}}.il
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
synth_intel_alm {{get_override("synth_opts")|options}} -top {{name}}
|
||||
{{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
|
||||
|
|
399
amaranth/vendor/_colognechip.py
Normal file
399
amaranth/vendor/_colognechip.py
Normal file
|
@ -0,0 +1,399 @@
|
|||
from abc import abstractmethod
|
||||
|
||||
from ..hdl import *
|
||||
from ..hdl._ir import RequirePosedge
|
||||
from ..lib import io, wiring
|
||||
from ..build import *
|
||||
|
||||
# FIXME: be sure attribtues are handled correctly
|
||||
|
||||
|
||||
class InnerBuffer(wiring.Component):
|
||||
"""A private component used to implement ``lib.io`` buffers.
|
||||
|
||||
Works like ``lib.io.Buffer``, with the following differences:
|
||||
|
||||
- ``port.invert`` is ignored (handling the inversion is the outer buffer's responsibility)
|
||||
- ``t`` is per-pin inverted output enable
|
||||
- ``merge_ff`` specifies this is to be used with a simple FF, and P&R should merge these
|
||||
"""
|
||||
def __init__(self, direction, port, merge_ff=False):
|
||||
self.direction = direction
|
||||
self.port = port
|
||||
self.merge_ff = merge_ff
|
||||
|
||||
members = {}
|
||||
|
||||
if direction is not io.Direction.Output:
|
||||
members["i"] = wiring.In(len(port))
|
||||
|
||||
if direction is not io.Direction.Input:
|
||||
members["o"] = wiring.Out(len(port))
|
||||
members["t"] = wiring.Out(len(port))
|
||||
|
||||
super().__init__(wiring.Signature(members).flip())
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
if isinstance(self.port, io.SingleEndedPort):
|
||||
io_port = self.port.io
|
||||
|
||||
for bit in range(len(self.port)):
|
||||
name = f"buf{bit}"
|
||||
if self.direction is io.Direction.Input:
|
||||
m.submodules[name] = Instance("CC_IBUF",
|
||||
i_I=io_port[bit],
|
||||
o_Y=self.i[bit],
|
||||
p_FF_IBF=self.merge_ff,
|
||||
)
|
||||
elif self.direction is io.Direction.Output:
|
||||
m.submodules[name] = Instance("CC_TOBUF",
|
||||
i_T=self.t[bit],
|
||||
i_A=self.o[bit],
|
||||
o_O=io_port[bit],
|
||||
p_FF_OBF=self.merge_ff,
|
||||
)
|
||||
elif self.direction is io.Direction.Bidir:
|
||||
m.submodules[name] = Instance("CC_IOBUF",
|
||||
i_T=self.t[bit],
|
||||
i_Y=self.o[bit],
|
||||
o_A=self.i[bit],
|
||||
io_IO=io_port[bit],
|
||||
p_FF_IBF=self.merge_ff,
|
||||
p_FF_OBF=self.merge_ff,
|
||||
)
|
||||
else:
|
||||
assert False # :nocov:
|
||||
|
||||
elif isinstance(self.port, io.DifferentialPort):
|
||||
p_port = self.port.p
|
||||
n_port = self.port.n
|
||||
|
||||
for bit in range(len(self.port)):
|
||||
name = f"buf{bit}"
|
||||
if self.direction is io.Direction.Input:
|
||||
m.submodules[name] = Instance("CC_LVDS_IBUF",
|
||||
i_I_P=p_port[bit],
|
||||
i_I_N=n_port[bit],
|
||||
o_Y=self.i[bit],
|
||||
p_FF_IBF=self.merge_ff,
|
||||
)
|
||||
elif self.direction is io.Direction.Output:
|
||||
m.submodules[name] = Instance("CC_LVDS_TOBUF",
|
||||
i_T=self.t[bit],
|
||||
i_A=self.o[bit],
|
||||
o_O_P=p_port[bit],
|
||||
o_O_N=n_port[bit],
|
||||
p_FF_OBF=self.merge_ff,
|
||||
)
|
||||
elif self.direction is io.Direction.Bidir:
|
||||
m.submodules[name] = Instance("CC_LVDS_IOBUF",
|
||||
i_T=self.t[bit],
|
||||
i_Y=self.o[bit],
|
||||
o_A=self.i[bit],
|
||||
io_P=p_port[bit],
|
||||
io_N=n_port[bit],
|
||||
p_FF_IBF=self.merge_ff,
|
||||
p_FF_OBF=self.merge_ff,
|
||||
)
|
||||
else:
|
||||
assert False # :nocov:
|
||||
|
||||
else:
|
||||
raise TypeError(f"Unknown port type {self.port!r}")
|
||||
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class IOBuffer(io.Buffer):
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
m.submodules.buf = buf = InnerBuffer(self.direction, self.port)
|
||||
inv_mask = sum(inv << bit for bit, inv in enumerate(self.port.invert))
|
||||
|
||||
if self.direction is not io.Direction.Output:
|
||||
m.d.comb += self.i.eq(buf.i ^ inv_mask)
|
||||
|
||||
if self.direction is not io.Direction.Input:
|
||||
m.d.comb += buf.o.eq(self.o ^ inv_mask)
|
||||
m.d.comb += buf.t.eq(~self.oe.replicate(len(self.port)))
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def _make_oereg(m, domain, oe, q):
|
||||
for bit in range(len(q)):
|
||||
m.submodules[f"oe_ff{bit}"] = Instance("CC_DFF",
|
||||
i_CLK=ClockSignal(domain),
|
||||
i_EN=Const(1),
|
||||
i_SR=Const(0),
|
||||
i_D=oe,
|
||||
o_Q=q[bit],
|
||||
)
|
||||
|
||||
|
||||
class FFBuffer(io.FFBuffer):
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
m.submodules.buf = buf = InnerBuffer(self.direction, self.port, true)
|
||||
inv_mask = sum(inv << bit for bit, inv in enumerate(self.port.invert))
|
||||
|
||||
if self.direction is not io.Direction.Output:
|
||||
m.submodules += RequirePosedge(self.i_domain)
|
||||
i_inv = Signal.like(self.i)
|
||||
for bit in range(len(self.port)):
|
||||
m.submodules[f"i_ff{bit}"] = Instance("CC_DFF",
|
||||
i_CLK=ClockSignal(self.i_domain),
|
||||
i_EN=Const(1),
|
||||
i_SR=Const(0),
|
||||
i_D=buf.i[bit],
|
||||
o_Q=i_inv[bit],
|
||||
)
|
||||
m.d.comb += self.i.eq(i_inv ^ inv_mask)
|
||||
|
||||
if self.direction is not io.Direction.Input:
|
||||
m.submodules += RequirePosedge(self.o_domain)
|
||||
o_inv = Signal.like(self.o)
|
||||
m.d.comb += o_inv.eq(self.o ^ inv_mask)
|
||||
for bit in range(len(self.port)):
|
||||
m.submodules[f"o_ff{bit}"] = Instance("CC_DFF",
|
||||
i_CLK=ClockSignal(self.o_domain),
|
||||
i_EN=Const(1),
|
||||
i_SR=Const(0),
|
||||
i_D=o_inv[bit],
|
||||
o_Q=buf.o[bit],
|
||||
)
|
||||
_make_oereg(m, self.o_domain, ~self.oe, buf.t)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class DDRBuffer(io.DDRBuffer):
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
m.submodules.buf = buf = InnerBuffer(self.direction, self.port)
|
||||
inv_mask = sum(inv << bit for bit, inv in enumerate(self.port.invert))
|
||||
|
||||
if self.direction is not io.Direction.Output:
|
||||
m.submodules += RequirePosedge(self.i_domain)
|
||||
i0_inv = Signal(len(self.port))
|
||||
i1_inv = Signal(len(self.port))
|
||||
for bit in range(len(self.port)):
|
||||
m.submodules[f"i_ddr{bit}"] = Instance("CC_IDDR",
|
||||
i_CLK=ClockSignal(self.i_domain),
|
||||
i_D=buf.i[bit],
|
||||
o_Q0=i0_inv[bit],
|
||||
o_Q1=i1_inv[bit],
|
||||
)
|
||||
m.d.comb += self.i[0].eq(i0_inv ^ inv_mask)
|
||||
m.d.comb += self.i[1].eq(i1_inv ^ inv_mask)
|
||||
|
||||
if self.direction is not io.Direction.Input:
|
||||
m.submodules += RequirePosedge(self.o_domain)
|
||||
o0_inv = Signal(len(self.port))
|
||||
o1_inv = Signal(len(self.port))
|
||||
m.d.comb += [
|
||||
o0_inv.eq(self.o[0] ^ inv_mask),
|
||||
o1_inv.eq(self.o[1] ^ inv_mask),
|
||||
]
|
||||
for bit in range(len(self.port)):
|
||||
m.submodules[f"o_ddr{bit}"] = Instance("CC_ODDR",
|
||||
i_CLK=ClockSignal(self.o_domain),
|
||||
i_DDR=ClockSignal(self.i_domain), # FIXME
|
||||
i_D0=o0_inv[bit],
|
||||
i_D1=o1_inv[bit],
|
||||
o_Q=buf.o[bit],
|
||||
)
|
||||
_make_oereg(m, self.o_domain, ~self.oe, buf.t)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class GateMatePlatform(TemplatedPlatform):
|
||||
"""
|
||||
.. rubric:: Peppercorn toolchain
|
||||
|
||||
Required tools:
|
||||
* ``yosys``
|
||||
* ``nextpnr-himbaechel`` built with the gatemate uarch
|
||||
* ``gmpack``
|
||||
|
||||
The environment is populated by running the script specified in the environment variable
|
||||
``AMARANTH_ENV_PEPPERCORN``, if present.
|
||||
|
||||
Available overrides:
|
||||
* ``verbose``: enables logging of informational messages to standard error.
|
||||
* ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
|
||||
* ``synth_opts``: adds options for ``synth_<family>`` Yosys command.
|
||||
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
|
||||
* ``script_after_synth``: inserts commands after ``synth_<family>`` in Yosys script.
|
||||
* ``yosys_opts``: adds extra options for ``yosys``.
|
||||
* ``nextpnr_opts``: adds extra options for ``nextpnr-<family>``.
|
||||
* ``gmpack_opts``: adds extra options for ``gmpack``.
|
||||
* ``add_preferences``: inserts commands at the end of the LPF file.
|
||||
|
||||
Build products:
|
||||
* ``{{name}}.rpt``: Yosys log.
|
||||
* ``{{name}}.json``: synthesized RTL.
|
||||
* ``{{name}}.tim``: nextpnr log.
|
||||
* ``{{name}}.config``: ASCII bitstream.
|
||||
* ``{{name}}.bit``: binary bitstream.
|
||||
|
||||
"""
|
||||
|
||||
toolchain = "pepercorn"
|
||||
device = property(abstractmethod(lambda: None))
|
||||
|
||||
_required_tools = [
|
||||
"yosys",
|
||||
"nextpnr-himbaechel",
|
||||
"gmpack"
|
||||
]
|
||||
|
||||
_file_templates = {
|
||||
**TemplatedPlatform.build_script_templates,
|
||||
"{{name}}.il": r"""
|
||||
# {{autogenerated}}
|
||||
{{emit_rtlil()}}
|
||||
""",
|
||||
"{{name}}.debug.v": r"""
|
||||
/* {{autogenerated}} */
|
||||
{{emit_debug_verilog()}}
|
||||
""",
|
||||
# Note: synth with -luttree is currently basically required to fit anything significant on the GateMate,
|
||||
# so we're adopting it.
|
||||
"{{name}}.ys": r"""
|
||||
# {{autogenerated}}
|
||||
{% for file in platform.iter_files(".v") -%}
|
||||
read_verilog {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".sv") -%}
|
||||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_rtlil {{name}}.il
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
synth_gatemate {{get_override("synth_opts")|options}} -top {{name}} -luttree
|
||||
{{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
|
||||
write_json {{name}}.json
|
||||
""",
|
||||
"{{name}}.ccf": r"""
|
||||
# {{autogenerated}}
|
||||
{% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
|
||||
NET "{{port_name}}" Loc = "IO_{{pin_name}}" {%- for key, value in attrs.items() %} | {{key}}={{value}}{% endfor %};
|
||||
{% endfor %}
|
||||
{{get_override("add_preferences")|default("# (add_preferences placeholder)")}}
|
||||
""",
|
||||
"{{name}}.sdc": r"""
|
||||
{% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
|
||||
{% if port_signal is not none -%}
|
||||
create_clock -name {{port_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_ports {{port_signal.name|tcl_quote}}]
|
||||
{% else -%}
|
||||
create_clock -name {{net_signal.name|tcl_quote}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")|tcl_quote}}]
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
|
||||
""",
|
||||
}
|
||||
_command_templates = [
|
||||
r"""
|
||||
{{invoke_tool("yosys")}}
|
||||
{{quiet("-q")}}
|
||||
{{get_override("yosys_opts")|options}}
|
||||
-l {{name}}.rpt
|
||||
{{name}}.ys
|
||||
""",
|
||||
r"""
|
||||
{{invoke_tool("nextpnr-himbaechel")}}
|
||||
{{quiet("--quiet")}}
|
||||
{{get_override("nextpnr_opts")|options}}
|
||||
--log {{name}}.tim
|
||||
--device {{platform.device}}
|
||||
--json {{name}}.json
|
||||
--sdc {{name}}.sdc
|
||||
-o ccf={{name}}.ccf
|
||||
-o out={{name}}.config
|
||||
""",
|
||||
r"""
|
||||
{{invoke_tool("gmpack")}}
|
||||
{{verbose("--verbose")}}
|
||||
{{get_override("gmpack_opts")|options}}
|
||||
--input {{name}}.config
|
||||
--bit {{name}}.bit
|
||||
"""
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
device = self.device.lower()
|
||||
|
||||
if device.startswith("ccgm1a"):
|
||||
self.family = "gatemate"
|
||||
else:
|
||||
raise ValueError(f"Device '{self.device}' is not recognized")
|
||||
|
||||
@property
|
||||
def required_tools(self):
|
||||
return self._required_tools
|
||||
|
||||
@property
|
||||
def file_templates(self):
|
||||
return self._file_templates
|
||||
|
||||
@property
|
||||
def command_templates(self):
|
||||
return self._command_templates
|
||||
|
||||
|
||||
def create_missing_domain(self, name):
|
||||
if name == "sync" and self.default_clk is not None:
|
||||
m = Module()
|
||||
|
||||
clk_io = self.request(self.default_clk, dir="-")
|
||||
m.submodules.clk_buf = clk_buf = io.Buffer("i", clk_io)
|
||||
clk_i = clk_buf.i
|
||||
|
||||
if self.default_rst is not None:
|
||||
rst_io = self.request(self.default_rst, dir="-")
|
||||
m.submodules.rst_buf = rst_buf = io.Buffer("i", rst_io)
|
||||
rst_i = rst_buf.i
|
||||
else:
|
||||
rst_i = Const(0)
|
||||
|
||||
# The post-configuration reset implicitly connects to every appropriate storage element.
|
||||
# As such, the sync domain is reset-less; domains driven by other clocks would need to have dedicated
|
||||
# reset circuitry or otherwise meet setup/hold constraints on their own.
|
||||
m.domains += ClockDomain("sync", reset_less=True)
|
||||
m.d.comb += ClockSignal("sync").eq(clk_i)
|
||||
|
||||
return m
|
||||
|
||||
def get_io_buffer(self, buffer):
|
||||
if isinstance(buffer, io.Buffer):
|
||||
result = IOBuffer(buffer.direction, buffer.port)
|
||||
elif isinstance(buffer, io.FFBuffer):
|
||||
result = FFBuffer(buffer.direction, buffer.port,
|
||||
i_domain=buffer.i_domain,
|
||||
o_domain=buffer.o_domain)
|
||||
elif isinstance(buffer, io.DDRBuffer):
|
||||
result = DDRBuffer(buffer.direction, buffer.port,
|
||||
i_domain=buffer.i_domain,
|
||||
o_domain=buffer.o_domain)
|
||||
else:
|
||||
raise TypeError(f"Unsupported buffer type {buffer!r}") # :nocov:
|
||||
|
||||
if buffer.direction is not io.Direction.Output:
|
||||
result.i = buffer.i
|
||||
if buffer.direction is not io.Direction.Input:
|
||||
result.o = buffer.o
|
||||
result.oe = buffer.oe
|
||||
|
||||
return result
|
|
@ -376,9 +376,9 @@ class GowinPlatform(TemplatedPlatform):
|
|||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_ilang {{file}}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_ilang {{name}}.il
|
||||
read_rtlil {{name}}.il
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
synth_gowin {{get_override("synth_opts")|options}} -top {{name}} -json {{name}}.syn.json
|
||||
{{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
|
||||
|
|
|
@ -319,7 +319,7 @@ class LatticePlatform(TemplatedPlatform):
|
|||
* ``verbose``: enables logging of informational messages to standard error.
|
||||
* ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
|
||||
* ``synth_opts``: adds options for ``synth_<family>`` Yosys command.
|
||||
* ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
|
||||
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
|
||||
* ``script_after_synth``: inserts commands after ``synth_<family>`` in Yosys script.
|
||||
* ``yosys_opts``: adds extra options for ``yosys``.
|
||||
* ``nextpnr_opts``: adds extra options for ``nextpnr-<family>``.
|
||||
|
@ -348,7 +348,7 @@ class LatticePlatform(TemplatedPlatform):
|
|||
* ``verbose``: enables logging of informational messages to standard error.
|
||||
* ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
|
||||
* ``synth_opts``: adds options for ``synth_nexus`` Yosys command.
|
||||
* ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
|
||||
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
|
||||
* ``script_after_synth``: inserts commands after ``synth_nexus`` in Yosys script.
|
||||
* ``yosys_opts``: adds extra options for ``yosys``.
|
||||
* ``nextpnr_opts``: adds extra options for ``nextpnr-nexus``.
|
||||
|
@ -474,9 +474,9 @@ class LatticePlatform(TemplatedPlatform):
|
|||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_ilang {{file}}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_ilang {{name}}.il
|
||||
read_rtlil {{name}}.il
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
{% if platform.family == "ecp5" %}
|
||||
synth_ecp5 {{get_override("synth_opts")|options}} -top {{name}}
|
||||
|
@ -567,9 +567,9 @@ class LatticePlatform(TemplatedPlatform):
|
|||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_ilang {{file}}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_ilang {{name}}.il
|
||||
read_rtlil {{name}}.il
|
||||
delete w:$verilog_initial_trigger
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
synth_nexus {{get_override("synth_opts")|options}} -top {{name}}
|
||||
|
|
|
@ -26,7 +26,7 @@ class SiliconBluePlatform(TemplatedPlatform):
|
|||
* ``verbose``: enables logging of informational messages to standard error.
|
||||
* ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
|
||||
* ``synth_opts``: adds options for ``synth_ice40`` Yosys command.
|
||||
* ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
|
||||
* ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script.
|
||||
* ``script_after_synth``: inserts commands after ``synth_ice40`` in Yosys script.
|
||||
* ``yosys_opts``: adds extra options for ``yosys``.
|
||||
* ``nextpnr_opts``: adds extra options for ``nextpnr-ice40``.
|
||||
|
@ -121,9 +121,9 @@ class SiliconBluePlatform(TemplatedPlatform):
|
|||
read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
|
||||
{% endfor %}
|
||||
{% for file in platform.iter_files(".il") -%}
|
||||
read_ilang {{file}}
|
||||
read_rtlil {{file}}
|
||||
{% endfor %}
|
||||
read_ilang {{name}}.il
|
||||
read_rtlil {{name}}.il
|
||||
{{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
|
||||
synth_ice40 {{get_override("synth_opts")|options}} -top {{name}}
|
||||
{{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
|
||||
|
|
|
@ -18,8 +18,8 @@ Documentation for past releases of the Amaranth language and toolchain is availa
|
|||
* `Amaranth 0.3 <https://amaranth-lang.org/docs/amaranth/v0.3/>`_
|
||||
|
||||
|
||||
Version 0.5 (unreleased)
|
||||
========================
|
||||
Version 0.5
|
||||
===========
|
||||
|
||||
The Migen compatibility layer has been removed.
|
||||
|
||||
|
@ -29,23 +29,23 @@ Migrating from version 0.4
|
|||
|
||||
Apply the following changes to code written against Amaranth 0.4 to migrate it to version 0.5:
|
||||
|
||||
* Update uses of :py:`reset=` keyword argument to :py:`init=`.
|
||||
* Ensure all elaboratables are subclasses of :class:`Elaboratable`.
|
||||
* Replace uses of :py:`m.Case()` with no patterns with :py:`m.Default()`.
|
||||
* Replace uses of :py:`Value.matches()` with no patterns with :py:`Const(1)`.
|
||||
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`.
|
||||
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`.
|
||||
* Update uses of :py:`reset=` keyword argument to :py:`init=`.
|
||||
* Ensure clock domains aren't used outside the module that defines them, or its submodules; move clock domain definitions upwards in the hierarchy as necessary
|
||||
* Replace imports of :py:`amaranth.asserts.Assert`, :py:`Assume`, and :py:`Cover` with imports from :py:`amaranth.hdl`.
|
||||
* Remove uses of :py:`name=` keyword argument of :py:`Assert`, :py:`Assume`, and :py:`Cover`; a message can be used instead.
|
||||
* Replace uses of :py:`amaranth.hdl.Memory` with :class:`amaranth.lib.memory.Memory`.
|
||||
* Update uses of :py:`platform.request` to pass :py:`dir="-"` and use :mod:`amaranth.lib.io` buffers.
|
||||
* Remove uses of :py:`amaranth.lib.coding.*` by inlining or copying the implementation of the modules.
|
||||
* Convert uses of :py:`Simulator.add_sync_process` used as testbenches to :meth:`Simulator.add_testbench <amaranth.sim.Simulator.add_testbench>`.
|
||||
* Convert other uses of :py:`Simulator.add_sync_process` to :meth:`Simulator.add_process <amaranth.sim.Simulator.add_process>`.
|
||||
* Convert simulator processes and testbenches to use the new async API.
|
||||
* Replace uses of :py:`amaranth.hdl.Memory` with :class:`amaranth.lib.memory.Memory`.
|
||||
* Replace imports of :py:`amaranth.asserts.Assert`, :py:`Assume`, and :py:`Cover` with imports from :py:`amaranth.hdl`.
|
||||
* Remove uses of :py:`name=` keyword argument of :py:`Assert`, :py:`Assume`, and :py:`Cover`; a message can be used instead.
|
||||
* Ensure all elaboratables are subclasses of :class:`Elaboratable`.
|
||||
* Ensure clock domains aren't used outside the module that defines them, or its submodules; move clock domain definitions upwards in the hierarchy as necessary
|
||||
* Remove uses of :py:`amaranth.lib.coding.*` by inlining or copying the implementation of the modules.
|
||||
* Update uses of :py:`platform.request` to pass :py:`dir="-"` and use :mod:`amaranth.lib.io` buffers.
|
||||
* Update uses of :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` with explicit :py:`phase` to take into account simulator no longer adding implicit :py:`period / 2`. (Previously, :meth:`Simulator.add_clock <amaranth.sim.Simulator.add_clock>` was documented to first toggle the clock at the time :py:`phase`, but actually first toggled the clock at :py:`period / 2 + phase`.)
|
||||
* Update uses of :meth:`Simulator.run_until <amaranth.sim.Simulator.run_until>` to remove the :py:`run_passive=True` argument. If the code uses :py:`run_passive=False`, ensure it still works with the new behavior.
|
||||
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=False)` to :func:`amaranth.utils.ceil_log2`.
|
||||
* Update uses of :py:`amaranth.utils.log2_int(need_pow2=True)` to :func:`amaranth.utils.exact_log2`.
|
||||
|
||||
|
||||
Implemented RFCs
|
||||
|
@ -55,6 +55,7 @@ Implemented RFCs
|
|||
.. _RFC 27: https://amaranth-lang.org/rfcs/0027-simulator-testbenches.html
|
||||
.. _RFC 30: https://amaranth-lang.org/rfcs/0030-component-metadata.html
|
||||
.. _RFC 36: https://amaranth-lang.org/rfcs/0036-async-testbench-functions.html
|
||||
.. _RFC 42: https://amaranth-lang.org/rfcs/0042-const-from-shape-castable.html
|
||||
.. _RFC 39: https://amaranth-lang.org/rfcs/0039-empty-case.html
|
||||
.. _RFC 43: https://amaranth-lang.org/rfcs/0043-rename-reset-to-init.html
|
||||
.. _RFC 45: https://amaranth-lang.org/rfcs/0045-lib-memory.html
|
||||
|
@ -65,6 +66,7 @@ Implemented RFCs
|
|||
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
|
||||
.. _RFC 58: https://amaranth-lang.org/rfcs/0058-valuecastable-format.html
|
||||
.. _RFC 59: https://amaranth-lang.org/rfcs/0059-no-domain-upwards-propagation.html
|
||||
.. _RFC 61: https://amaranth-lang.org/rfcs/0061-minimal-streams.html
|
||||
.. _RFC 62: https://amaranth-lang.org/rfcs/0062-memory-data.html
|
||||
.. _RFC 63: https://amaranth-lang.org/rfcs/0063-remove-lib-coding.html
|
||||
.. _RFC 65: https://amaranth-lang.org/rfcs/0065-format-struct-enum.html
|
||||
|
@ -74,6 +76,7 @@ Implemented RFCs
|
|||
* `RFC 30`_: Component metadata
|
||||
* `RFC 36`_: Async testbench functions
|
||||
* `RFC 39`_: Change semantics of no-argument ``m.Case()``
|
||||
* `RFC 42`_: ``Const`` from shape-castable
|
||||
* `RFC 43`_: Rename ``reset=`` to ``init=``
|
||||
* `RFC 45`_: Move ``hdl.Memory`` to ``lib.Memory``
|
||||
* `RFC 46`_: Change ``Shape.cast(range(1))`` to ``unsigned(0)``
|
||||
|
@ -83,6 +86,7 @@ Implemented RFCs
|
|||
* `RFC 55`_: New ``lib.io`` components
|
||||
* `RFC 58`_: Core support for ``ValueCastable`` formatting
|
||||
* `RFC 59`_: Get rid of upwards propagation of clock domains
|
||||
* `RFC 61`_: Minimal streams
|
||||
* `RFC 62`_: The ``MemoryData`` class
|
||||
* `RFC 63`_: Remove ``amaranth.lib.coding``
|
||||
* `RFC 65`_: Special formatting for structures and enums
|
||||
|
@ -103,6 +107,7 @@ Language changes
|
|||
* Changed: :py:`Value.matches()` with no patterns is :py:`Const(0)` instead of :py:`Const(1)`. (`RFC 39`_)
|
||||
* Changed: :py:`Signal(range(stop), init=stop)` warning has been changed into a hard error and made to trigger on any out-of range value.
|
||||
* Changed: :py:`Signal(range(0))` is now valid without a warning.
|
||||
* Changed: :py:`Const(value, shape)` now accepts shape-castable objects as :py:`shape`. (`RFC 42`_)
|
||||
* Changed: :py:`Shape.cast(range(1))` is now :py:`unsigned(0)`. (`RFC 46`_)
|
||||
* Changed: the :py:`reset=` argument of :class:`Signal`, :meth:`Signal.like`, :class:`amaranth.lib.wiring.Member`, :class:`amaranth.lib.cdc.FFSynchronizer`, and :py:`m.FSM()` has been renamed to :py:`init=`. (`RFC 43`_)
|
||||
* Changed: :class:`Shape` has been made immutable and hashable.
|
||||
|
@ -130,6 +135,7 @@ Standard library changes
|
|||
* Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
|
||||
* Added: :class:`amaranth.lib.io.Buffer`, :class:`amaranth.lib.io.FFBuffer`, :class:`amaranth.lib.io.DDRBuffer`. (`RFC 55`_)
|
||||
* Added: :mod:`amaranth.lib.meta`, :class:`amaranth.lib.wiring.ComponentMetadata`. (`RFC 30`_)
|
||||
* Added: :mod:`amaranth.lib.stream`. (`RFC 61`_)
|
||||
* Deprecated: :mod:`amaranth.lib.coding`. (`RFC 63`_)
|
||||
* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
|
||||
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_)
|
||||
|
|
|
@ -3,8 +3,8 @@ Standard library
|
|||
|
||||
The :mod:`amaranth.lib` module, also known as the standard library, provides modules that falls into one of the three categories:
|
||||
|
||||
1. Modules that will used by essentially all idiomatic Amaranth code, or which are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), :mod:`amaranth.lib.wiring` (interfaces and components), and :mod:`amaranth.lib.meta` (interface metadata).
|
||||
2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.cdc`, :mod:`amaranth.lib.memory`.
|
||||
1. Modules that will used by essentially all idiomatic Amaranth code, or which are necessary for interoperability. This includes :mod:`amaranth.lib.enum` (enumerations), :mod:`amaranth.lib.data` (data structures), :mod:`amaranth.lib.wiring` (interfaces and components), :mod:`amaranth.lib.meta` (interface metadata), and :mod:`amaranth.lib.stream` (data streams).
|
||||
2. Modules that abstract common functionality whose implementation differs between hardware platforms. This includes :mod:`amaranth.lib.memory` and :mod:`amaranth.lib.cdc`.
|
||||
3. Modules that have essentially one correct implementation and are of broad utility in digital designs. This includes :mod:`amaranth.lib.coding`, :mod:`amaranth.lib.fifo`, and :mod:`amaranth.lib.crc`.
|
||||
|
||||
As part of the Amaranth backwards compatibility guarantee, any behaviors described in these documents will not change from a version to another without at least one version including a warning about the impending change. Any nontrivial change to these behaviors must also go through the public review as a part of the `Amaranth Request for Comments process <https://amaranth-lang.org/rfcs/>`_.
|
||||
|
@ -18,6 +18,7 @@ The Amaranth standard library is separate from the Amaranth language: everything
|
|||
stdlib/data
|
||||
stdlib/wiring
|
||||
stdlib/meta
|
||||
stdlib/stream
|
||||
stdlib/memory
|
||||
stdlib/io
|
||||
stdlib/cdc
|
||||
|
|
BIN
docs/stdlib/_images/stream_pipeline.png
Normal file
BIN
docs/stdlib/_images/stream_pipeline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 56 KiB |
416
docs/stdlib/stream.rst
Normal file
416
docs/stdlib/stream.rst
Normal file
|
@ -0,0 +1,416 @@
|
|||
Data streams
|
||||
------------
|
||||
|
||||
.. py:module:: amaranth.lib.stream
|
||||
|
||||
The :mod:`amaranth.lib.stream` module provides a mechanism for unidirectional exchange of arbitrary data between modules.
|
||||
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
One of the most common flow control mechanisms is *ready/valid handshaking*, where a *producer* pushes data to a *consumer* whenever it becomes available, and the consumer signals to the producer whether it can accept more data. In Amaranth, this mechanism is implemented using an :ref:`interface <wiring>` with three members:
|
||||
|
||||
- :py:`payload` (driven by the producer), containing the data;
|
||||
- :py:`valid` (driven by the producer), indicating that data is currently available in :py:`payload`;
|
||||
- :py:`ready` (driven by the consumer), indicating that data is accepted if available.
|
||||
|
||||
This module provides such an interface, :class:`stream.Interface <Interface>`, and defines the exact rules governing the flow of data through it.
|
||||
|
||||
|
||||
.. _stream-rules:
|
||||
|
||||
Data transfer rules
|
||||
===================
|
||||
|
||||
The producer and the consumer must be synchronized: they must belong to the same :ref:`clock domain <lang-clockdomains>`, and any :ref:`control flow modifiers <lang-controlinserter>` must be applied to both, in the same order.
|
||||
|
||||
Data flows through a stream according to the following four rules:
|
||||
|
||||
1. On each cycle where both :py:`valid` and :py:`ready` are asserted, a transfer is performed: the contents of ``payload`` are conveyed from the producer to the consumer.
|
||||
2. Once the producer asserts :py:`valid`, it must not deassert :py:`valid` or change the contents of ``payload`` until a transfer is performed.
|
||||
3. The producer must not wait for :py:`ready` to be asserted before asserting :py:`valid`: any form of feedback from :py:`ready` that causes :py:`valid` to become asserted is prohibited.
|
||||
4. The consumer may assert or deassert :py:`ready` at any time, including via combinational feedback from :py:`valid`.
|
||||
|
||||
Some producers and consumers may be designed without support for backpressure. Such producers must tie :py:`ready` to :py:`Const(1)` by specifying :py:`always_ready=True` when constructing a stream, and consumers may (but are not required to) do the same. Similarly, some producers and consumers may be designed such that a payload is provided or must be provided on each cycle. Such consumers must tie :py:`valid` to :py:`Const(1)` by specifying :py:`always_valid=True` when constructing a stream, and producers may (but are not required to) do the same.
|
||||
|
||||
If these control signals are tied to :py:`Const(1)`, then the :func:`wiring.connect <.lib.wiring.connect>` function ensures that only compatible streams are connected together. For example, if the producer does not support backpressure (:py:`ready` tied to :py:`Const(1)`), it can only be connected to consumers that do not require backpressure. However, consumers that do not require backpressure can be connected to producers with or without support for backpressure. The :py:`valid` control signal is treated similarly.
|
||||
|
||||
These rules ensure that producers and consumers that are developed independently can be safely used together, without unduly restricting the application-specific conditions that determine assertion of :py:`valid` and :py:`ready`.
|
||||
|
||||
|
||||
Examples
|
||||
========
|
||||
|
||||
The following examples demonstrate the use of streams for a data processing pipeline that receives serial data input from an external device, transforms it by negating the 2's complement value, and transmits it to another external device whenever requested. Similar pipelines, albeit more complex, are widely used in :abbr:`DSP (digital signal processing)` applications.
|
||||
|
||||
The use of a unified data transfer mechanism enables uniform testing of individual units, and makes it possible to add a queue to the pipeline using only two additional connections.
|
||||
|
||||
.. testsetup::
|
||||
|
||||
from amaranth import *
|
||||
|
||||
.. testcode::
|
||||
|
||||
from amaranth.lib import stream, wiring
|
||||
from amaranth.lib.wiring import In, Out
|
||||
|
||||
The pipeline is tested using the :doc:`built-in simulator </simulator>` and the two helper functions defined below:
|
||||
|
||||
.. testcode::
|
||||
|
||||
from amaranth.sim import Simulator
|
||||
|
||||
async def stream_get(ctx, stream):
|
||||
ctx.set(stream.ready, 1)
|
||||
payload, = await ctx.tick().sample(stream.payload).until(stream.valid)
|
||||
ctx.set(stream.ready, 0)
|
||||
return payload
|
||||
|
||||
async def stream_put(ctx, stream, payload):
|
||||
ctx.set(stream.valid, 1)
|
||||
ctx.set(stream.payload, payload)
|
||||
await ctx.tick().until(stream.ready)
|
||||
ctx.set(stream.valid, 0)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
"Minimal streams" as defined in `RFC 61`_ do not provide built-in helper functions for testing pending further work on the clock domain system. They will be provided in a later release. For the time being, you can copy the helper functions above to test your designs that use streams.
|
||||
|
||||
|
||||
Serial receiver
|
||||
+++++++++++++++
|
||||
|
||||
The serial receiver captures the serial output of an external device and converts it to a stream of words. While the ``ssel`` signal is high, each low-to-high transition on the ``sclk`` input captures the value of the ``sdat`` signal; eight consecutive captured bits are assembled into a word (:abbr:`MSB (most significant bit)` first) and pushed into the pipeline for processing. If the ``ssel`` signal is low, no data transmission occurs and the transmitter and the receiver are instead synchronized with each other.
|
||||
|
||||
In this example, the external device does not provide a way to pause data transmission. If the pipeline isn't ready to accept the next payload, it is necessary to discard data at some point; here, it is done in the serial receiver.
|
||||
|
||||
.. testcode::
|
||||
|
||||
class SerialReceiver(wiring.Component):
|
||||
ssel: In(1)
|
||||
sclk: In(1)
|
||||
sdat: In(1)
|
||||
|
||||
stream: Out(stream.Signature(signed(8)))
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
# Detect edges on the `sclk` input:
|
||||
sclk_reg = Signal()
|
||||
sclk_edge = ~sclk_reg & self.sclk
|
||||
m.d.sync += sclk_reg.eq(self.sclk)
|
||||
|
||||
# Capture `sdat` and bits into payloads:
|
||||
count = Signal(range(8))
|
||||
data = Signal(8)
|
||||
done = Signal()
|
||||
with m.If(~self.ssel):
|
||||
m.d.sync += count.eq(0)
|
||||
with m.Elif(sclk_edge):
|
||||
m.d.sync += count.eq(count + 1)
|
||||
m.d.sync += data.eq(Cat(self.sdat, data))
|
||||
m.d.sync += done.eq(count == 7)
|
||||
|
||||
# Push assembled payloads into the pipeline:
|
||||
with m.If(done & (~self.stream.valid | self.stream.ready)):
|
||||
m.d.sync += self.stream.payload.eq(data)
|
||||
m.d.sync += self.stream.valid.eq(1)
|
||||
m.d.sync += done.eq(0)
|
||||
with m.Elif(self.stream.ready):
|
||||
m.d.sync += self.stream.valid.eq(0)
|
||||
# Payload is discarded if `done & self.stream.valid & ~self.stream.ready`.
|
||||
|
||||
return m
|
||||
|
||||
.. testcode::
|
||||
|
||||
def test_serial_receiver():
|
||||
dut = SerialReceiver()
|
||||
|
||||
async def testbench_input(ctx):
|
||||
await ctx.tick()
|
||||
ctx.set(dut.ssel, 1)
|
||||
await ctx.tick()
|
||||
for bit in [1, 0, 1, 0, 0, 1, 1, 1]:
|
||||
ctx.set(dut.sdat, bit)
|
||||
ctx.set(dut.sclk, 0)
|
||||
await ctx.tick()
|
||||
ctx.set(dut.sclk, 1)
|
||||
await ctx.tick()
|
||||
ctx.set(dut.ssel, 0)
|
||||
await ctx.tick()
|
||||
|
||||
async def testbench_output(ctx):
|
||||
expected_word = 0b10100111
|
||||
payload = await stream_get(ctx, dut.stream)
|
||||
assert (payload & 0xff) == (expected_word & 0xff), \
|
||||
f"{payload & 0xff:08b} != {expected_word & 0xff:08b} (expected)"
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(1e-6)
|
||||
sim.add_testbench(testbench_input)
|
||||
sim.add_testbench(testbench_output)
|
||||
with sim.write_vcd("stream_serial_receiver.vcd"):
|
||||
sim.run()
|
||||
|
||||
.. testcode::
|
||||
:hide:
|
||||
|
||||
test_serial_receiver()
|
||||
|
||||
The serial protocol recognized by the receiver is illustrated with the following diagram (corresponding to ``stream_serial_receiver.vcd``):
|
||||
|
||||
.. wavedrom:: stream/serial_receiver
|
||||
|
||||
{
|
||||
signal: [
|
||||
{ name: "clk", wave: "lpppppppppppppppppppp" },
|
||||
{},
|
||||
[
|
||||
"serial",
|
||||
{ name: "ssel", wave: "01................0.." },
|
||||
{ name: "sclk", wave: "0..101010101010101..." },
|
||||
{ name: "sdat", wave: "0.=.=.=.=.=.=.=.=....", data: ["1", "0", "1", "0", "0", "0", "0", "1"] },
|
||||
],
|
||||
{},
|
||||
[
|
||||
"stream",
|
||||
{ name: "payload", wave: "=..................=.", data: ["00", "A7"] },
|
||||
{ name: "valid", wave: "0..................10" },
|
||||
{ name: "ready", wave: "1...................0" },
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Serial transmitter
|
||||
++++++++++++++++++
|
||||
|
||||
The serial transmitter accepts a stream of words and provides it to the serial input of an external device whenever requested. Its serial interface is the same as that of the serial receiver, with the exception that the ``sclk`` and ``sdat`` signals are outputs. The ``ssel`` signal remains an input; the external device uses it for flow control.
|
||||
|
||||
.. testcode::
|
||||
|
||||
class SerialTransmitter(wiring.Component):
|
||||
ssel: In(1)
|
||||
sclk: Out(1)
|
||||
sdat: Out(1)
|
||||
|
||||
stream: In(stream.Signature(signed(8)))
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
count = Signal(range(9))
|
||||
data = Signal(8)
|
||||
|
||||
with m.If(~self.ssel):
|
||||
m.d.sync += count.eq(0)
|
||||
m.d.sync += self.sclk.eq(1)
|
||||
with m.Elif(count != 0):
|
||||
m.d.comb += self.stream.ready.eq(0)
|
||||
m.d.sync += self.sclk.eq(~self.sclk)
|
||||
with m.If(self.sclk):
|
||||
m.d.sync += data.eq(Cat(0, data))
|
||||
m.d.sync += self.sdat.eq(data[-1])
|
||||
with m.Else():
|
||||
m.d.sync += count.eq(count - 1)
|
||||
with m.Else():
|
||||
m.d.comb += self.stream.ready.eq(1)
|
||||
with m.If(self.stream.valid):
|
||||
m.d.sync += count.eq(8)
|
||||
m.d.sync += data.eq(self.stream.payload)
|
||||
|
||||
return m
|
||||
|
||||
.. testcode::
|
||||
|
||||
def test_serial_transmitter():
|
||||
dut = SerialTransmitter()
|
||||
|
||||
async def testbench_input(ctx):
|
||||
await stream_put(ctx, dut.stream, 0b10100111)
|
||||
|
||||
async def testbench_output(ctx):
|
||||
await ctx.tick()
|
||||
ctx.set(dut.ssel, 1)
|
||||
for index, expected_bit in enumerate([1, 0, 1, 0, 0, 1, 1, 1]):
|
||||
_, sdat = await ctx.posedge(dut.sclk).sample(dut.sdat)
|
||||
assert sdat == expected_bit, \
|
||||
f"bit {index}: {sdat} != {expected_bit} (expected)"
|
||||
ctx.set(dut.ssel, 0)
|
||||
await ctx.tick()
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(1e-6)
|
||||
sim.add_testbench(testbench_input)
|
||||
sim.add_testbench(testbench_output)
|
||||
with sim.write_vcd("stream_serial_transmitter.vcd"):
|
||||
sim.run()
|
||||
|
||||
.. testcode::
|
||||
:hide:
|
||||
|
||||
test_serial_transmitter()
|
||||
|
||||
|
||||
Value negator
|
||||
+++++++++++++
|
||||
|
||||
The value negator accepts a stream of words, negates the 2's complement value of these words, and provides the result as a stream of words again. In a practical :abbr:`DSP` application, this unit could be replaced with, for example, a :abbr:`FIR (finite impulse response)` filter.
|
||||
|
||||
.. testcode::
|
||||
|
||||
class ValueNegator(wiring.Component):
|
||||
i_stream: In(stream.Signature(signed(8)))
|
||||
o_stream: Out(stream.Signature(signed(8)))
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
with m.If(self.i_stream.valid & (~self.o_stream.valid | self.o_stream.ready)):
|
||||
m.d.comb += self.i_stream.ready.eq(1)
|
||||
m.d.sync += self.o_stream.payload.eq(-self.i_stream.payload)
|
||||
m.d.sync += self.o_stream.valid.eq(1)
|
||||
with m.Elif(self.o_stream.ready):
|
||||
m.d.sync += self.o_stream.valid.eq(0)
|
||||
|
||||
return m
|
||||
|
||||
.. testcode::
|
||||
|
||||
def test_value_negator():
|
||||
dut = ValueNegator()
|
||||
|
||||
async def testbench_input(ctx):
|
||||
await stream_put(ctx, dut.i_stream, 1)
|
||||
await stream_put(ctx, dut.i_stream, 17)
|
||||
|
||||
async def testbench_output(ctx):
|
||||
assert await stream_get(ctx, dut.o_stream) == -1
|
||||
assert await stream_get(ctx, dut.o_stream) == -17
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(1e-6)
|
||||
sim.add_testbench(testbench_input)
|
||||
sim.add_testbench(testbench_output)
|
||||
with sim.write_vcd("stream_value_negator.vcd"):
|
||||
sim.run()
|
||||
|
||||
.. testcode::
|
||||
:hide:
|
||||
|
||||
test_value_negator()
|
||||
|
||||
|
||||
Complete pipeline
|
||||
+++++++++++++++++
|
||||
|
||||
The complete pipeline consists of a serial receiver, a value negator, a FIFO queue, and a serial transmitter connected in series. Without queueing, any momentary mismatch between the rate at which the serial data is produced and consumed would result in data loss. A FIFO queue from the :mod:`.lib.fifo` standard library module is used to avoid this problem.
|
||||
|
||||
.. testcode::
|
||||
|
||||
from amaranth.lib.fifo import SyncFIFOBuffered
|
||||
|
||||
class ExamplePipeline(wiring.Component):
|
||||
i_ssel: In(1)
|
||||
i_sclk: In(1)
|
||||
i_sdat: In(1)
|
||||
|
||||
o_ssel: In(1)
|
||||
o_sclk: Out(1)
|
||||
o_sdat: Out(1)
|
||||
|
||||
def elaborate(self, platform):
|
||||
m = Module()
|
||||
|
||||
# Create and connect serial receiver:
|
||||
m.submodules.receiver = receiver = SerialReceiver()
|
||||
m.d.comb += [
|
||||
receiver.ssel.eq(self.i_ssel),
|
||||
receiver.sclk.eq(self.i_sclk),
|
||||
receiver.sdat.eq(self.i_sdat),
|
||||
]
|
||||
|
||||
# Create and connect value negator:
|
||||
m.submodules.negator = negator = ValueNegator()
|
||||
wiring.connect(m, receiver=receiver.stream, negator=negator.i_stream)
|
||||
|
||||
# Create and connect FIFO queue:
|
||||
m.submodules.queue = queue = SyncFIFOBuffered(width=8, depth=16)
|
||||
wiring.connect(m, negator=negator.o_stream, queue=queue.w_stream)
|
||||
|
||||
# Create and connect serial transmitter:
|
||||
m.submodules.transmitter = transmitter = SerialTransmitter()
|
||||
wiring.connect(m, queue=queue.r_stream, transmitter=transmitter.stream)
|
||||
|
||||
# Connect outputs:
|
||||
m.d.comb += [
|
||||
transmitter.ssel.eq(self.o_ssel),
|
||||
self.o_sclk.eq(transmitter.sclk),
|
||||
self.o_sdat.eq(transmitter.sdat),
|
||||
]
|
||||
|
||||
return m
|
||||
|
||||
.. testcode::
|
||||
|
||||
def test_example_pipeline():
|
||||
dut = ExamplePipeline()
|
||||
|
||||
async def testbench_input(ctx):
|
||||
for value in [1, 17]:
|
||||
ctx.set(dut.i_ssel, 1)
|
||||
for bit in reversed(range(8)):
|
||||
ctx.set(dut.i_sclk, 0)
|
||||
ctx.set(dut.i_sdat, bool(value & (1 << bit)))
|
||||
await ctx.tick()
|
||||
ctx.set(dut.i_sclk, 1)
|
||||
await ctx.tick()
|
||||
await ctx.tick()
|
||||
ctx.set(dut.i_ssel, 0)
|
||||
ctx.set(dut.i_sclk, 0)
|
||||
await ctx.tick()
|
||||
|
||||
async def testbench_output(ctx):
|
||||
await ctx.tick()
|
||||
ctx.set(dut.o_ssel, 1)
|
||||
for index, expected_value in enumerate([-1, -17]):
|
||||
value = 0
|
||||
for _ in range(8):
|
||||
_, sdat = await ctx.posedge(dut.o_sclk).sample(dut.o_sdat)
|
||||
value = (value << 1) | sdat
|
||||
assert value == (expected_value & 0xff), \
|
||||
f"word {index}: {value:08b} != {expected_value & 0xff:08b} (expected)"
|
||||
await ctx.tick()
|
||||
ctx.set(dut.o_ssel, 0)
|
||||
|
||||
sim = Simulator(dut)
|
||||
sim.add_clock(1e-6)
|
||||
sim.add_testbench(testbench_input)
|
||||
sim.add_testbench(testbench_output)
|
||||
with sim.write_vcd("stream_example_pipeline.vcd"):
|
||||
sim.run()
|
||||
|
||||
.. testcode::
|
||||
:hide:
|
||||
|
||||
test_example_pipeline()
|
||||
|
||||
This data processing pipeline overlaps reception and transmission of serial data, with only a few cycles of latency between the completion of reception and the beginning of transmission of the processed data:
|
||||
|
||||
.. image:: _images/stream_pipeline.png
|
||||
|
||||
Implementing such an efficient pipeline can be difficult without the use of appropriate abstractions. The use of streams allows the designer to focus on the data processing and simplifies testing by ensuring that the interaction of the individual units is standard and well-defined.
|
||||
|
||||
|
||||
Reference
|
||||
=========
|
||||
|
||||
Components that communicate using streams must not only use a :class:`stream.Interface <Interface>`, but also follow the :ref:`data transfer rules <stream-rules>`.
|
||||
|
||||
.. autoclass:: Signature
|
||||
|
||||
.. autoclass:: Interface
|
|
@ -76,7 +76,7 @@ test = [
|
|||
docs = [
|
||||
"sphinx~=7.1",
|
||||
"sphinxcontrib-platformpicker~=1.3",
|
||||
"sphinxcontrib-yowasp-wavedrom==1.6", # exact version to avoid changes in rendering
|
||||
"sphinxcontrib-yowasp-wavedrom==1.7", # exact version to avoid changes in rendering
|
||||
"sphinx-rtd-theme~=2.0",
|
||||
"sphinx-autobuild",
|
||||
]
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# amaranth: UnusedElaboratable=no
|
||||
|
||||
import warnings
|
||||
|
||||
from amaranth.hdl import *
|
||||
from amaranth.asserts import Initial, AnyConst
|
||||
from amaranth.sim import *
|
||||
from amaranth.lib.fifo import *
|
||||
from amaranth.lib.memory import *
|
||||
from amaranth.lib import stream
|
||||
|
||||
from .utils import *
|
||||
from amaranth._utils import _ignore_deprecated
|
||||
|
@ -64,6 +63,20 @@ class FIFOTestCase(FHDLTestCase):
|
|||
r"requested exact depth 16 is not$")):
|
||||
AsyncFIFOBuffered(width=8, depth=16, exact_depth=True)
|
||||
|
||||
def test_w_stream(self):
|
||||
fifo = SyncFIFOBuffered(width=8, depth=16)
|
||||
self.assertEqual(fifo.w_stream.signature, stream.Signature(8).flip())
|
||||
self.assertIs(fifo.w_stream.payload, fifo.w_data)
|
||||
self.assertIs(fifo.w_stream.valid, fifo.w_en)
|
||||
self.assertIs(fifo.w_stream.ready, fifo.w_rdy)
|
||||
|
||||
def test_r_stream(self):
|
||||
fifo = SyncFIFOBuffered(width=8, depth=16)
|
||||
self.assertEqual(fifo.r_stream.signature, stream.Signature(8))
|
||||
self.assertIs(fifo.r_stream.payload, fifo.r_data)
|
||||
self.assertIs(fifo.r_stream.valid, fifo.r_rdy)
|
||||
self.assertIs(fifo.r_stream.ready, fifo.r_en)
|
||||
|
||||
|
||||
class FIFOModel(Elaboratable, FIFOInterface):
|
||||
"""
|
||||
|
|
135
tests/test_lib_stream.py
Normal file
135
tests/test_lib_stream.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
from amaranth.hdl import *
|
||||
from amaranth.lib import stream, wiring, fifo
|
||||
from amaranth.lib.wiring import In, Out
|
||||
|
||||
from .utils import *
|
||||
|
||||
|
||||
class StreamTestCase(FHDLTestCase):
|
||||
def test_nav_nar(self):
|
||||
sig = stream.Signature(2)
|
||||
self.assertRepr(sig, f"stream.Signature(2)")
|
||||
self.assertEqual(sig.always_valid, False)
|
||||
self.assertEqual(sig.always_ready, False)
|
||||
self.assertEqual(sig.members, wiring.SignatureMembers({
|
||||
"payload": Out(2),
|
||||
"valid": Out(1),
|
||||
"ready": In(1)
|
||||
}))
|
||||
intf = sig.create()
|
||||
self.assertRepr(intf,
|
||||
f"stream.Interface(payload=(sig intf__payload), valid=(sig intf__valid), "
|
||||
f"ready=(sig intf__ready))")
|
||||
self.assertIs(intf.signature, sig)
|
||||
self.assertIsInstance(intf.payload, Signal)
|
||||
self.assertIs(intf.p, intf.payload)
|
||||
self.assertIsInstance(intf.valid, Signal)
|
||||
self.assertIsInstance(intf.ready, Signal)
|
||||
|
||||
def test_av_nar(self):
|
||||
sig = stream.Signature(2, always_valid=True)
|
||||
self.assertRepr(sig, f"stream.Signature(2, always_valid=True)")
|
||||
self.assertEqual(sig.always_valid, True)
|
||||
self.assertEqual(sig.always_ready, False)
|
||||
self.assertEqual(sig.members, wiring.SignatureMembers({
|
||||
"payload": Out(2),
|
||||
"valid": Out(1),
|
||||
"ready": In(1)
|
||||
}))
|
||||
intf = sig.create()
|
||||
self.assertRepr(intf,
|
||||
f"stream.Interface(payload=(sig intf__payload), valid=(const 1'd1), "
|
||||
f"ready=(sig intf__ready))")
|
||||
self.assertIs(intf.signature, sig)
|
||||
self.assertIsInstance(intf.payload, Signal)
|
||||
self.assertIs(intf.p, intf.payload)
|
||||
self.assertIsInstance(intf.valid, Const)
|
||||
self.assertEqual(intf.valid.value, 1)
|
||||
self.assertIsInstance(intf.ready, Signal)
|
||||
|
||||
def test_nav_ar(self):
|
||||
sig = stream.Signature(2, always_ready=True)
|
||||
self.assertRepr(sig, f"stream.Signature(2, always_ready=True)")
|
||||
self.assertEqual(sig.always_valid, False)
|
||||
self.assertEqual(sig.always_ready, True)
|
||||
self.assertEqual(sig.members, wiring.SignatureMembers({
|
||||
"payload": Out(2),
|
||||
"valid": Out(1),
|
||||
"ready": In(1)
|
||||
}))
|
||||
intf = sig.create()
|
||||
self.assertRepr(intf,
|
||||
f"stream.Interface(payload=(sig intf__payload), valid=(sig intf__valid), "
|
||||
f"ready=(const 1'd1))")
|
||||
self.assertIs(intf.signature, sig)
|
||||
self.assertIsInstance(intf.payload, Signal)
|
||||
self.assertIs(intf.p, intf.payload)
|
||||
self.assertIsInstance(intf.valid, Signal)
|
||||
self.assertIsInstance(intf.ready, Const)
|
||||
self.assertEqual(intf.ready.value, 1)
|
||||
|
||||
def test_av_ar(self):
|
||||
sig = stream.Signature(2, always_valid=True, always_ready=True)
|
||||
self.assertRepr(sig, f"stream.Signature(2, always_valid=True, always_ready=True)")
|
||||
self.assertEqual(sig.always_valid, True)
|
||||
self.assertEqual(sig.always_ready, True)
|
||||
self.assertEqual(sig.members, wiring.SignatureMembers({
|
||||
"payload": Out(2),
|
||||
"valid": Out(1),
|
||||
"ready": In(1)
|
||||
}))
|
||||
intf = sig.create()
|
||||
self.assertRepr(intf,
|
||||
f"stream.Interface(payload=(sig intf__payload), valid=(const 1'd1), "
|
||||
f"ready=(const 1'd1))")
|
||||
self.assertIs(intf.signature, sig)
|
||||
self.assertIsInstance(intf.payload, Signal)
|
||||
self.assertIs(intf.p, intf.payload)
|
||||
self.assertIsInstance(intf.valid, Const)
|
||||
self.assertEqual(intf.valid.value, 1)
|
||||
self.assertIsInstance(intf.ready, Const)
|
||||
self.assertEqual(intf.ready.value, 1)
|
||||
|
||||
def test_eq(self):
|
||||
sig_nav_nar = stream.Signature(2)
|
||||
sig_av_nar = stream.Signature(2, always_valid=True)
|
||||
sig_nav_ar = stream.Signature(2, always_ready=True)
|
||||
sig_av_ar = stream.Signature(2, always_valid=True, always_ready=True)
|
||||
sig_av_ar2 = stream.Signature(3, always_valid=True, always_ready=True)
|
||||
self.assertNotEqual(sig_nav_nar, None)
|
||||
self.assertEqual(sig_nav_nar, sig_nav_nar)
|
||||
self.assertEqual(sig_av_nar, sig_av_nar)
|
||||
self.assertEqual(sig_nav_ar, sig_nav_ar)
|
||||
self.assertEqual(sig_av_ar, sig_av_ar)
|
||||
self.assertEqual(sig_av_ar2, sig_av_ar2)
|
||||
self.assertNotEqual(sig_nav_nar, sig_av_nar)
|
||||
self.assertNotEqual(sig_av_nar, sig_nav_ar)
|
||||
self.assertNotEqual(sig_nav_ar, sig_av_ar)
|
||||
self.assertNotEqual(sig_av_ar, sig_nav_nar)
|
||||
self.assertNotEqual(sig_av_ar, sig_av_ar2)
|
||||
|
||||
def test_interface_create_bad(self):
|
||||
with self.assertRaisesRegex(TypeError,
|
||||
r"^Signature of stream\.Interface must be a stream\.Signature, not "
|
||||
r"Signature\(\{\}\)$"):
|
||||
stream.Interface(wiring.Signature({}))
|
||||
|
||||
|
||||
class FIFOStreamCompatTestCase(FHDLTestCase):
|
||||
def test_r_stream(self):
|
||||
queue = fifo.SyncFIFOBuffered(width=4, depth=16)
|
||||
r = queue.r_stream
|
||||
self.assertFalse(r.signature.always_valid)
|
||||
self.assertFalse(r.signature.always_ready)
|
||||
self.assertIs(r.payload, queue.r_data)
|
||||
self.assertIs(r.valid, queue.r_rdy)
|
||||
self.assertIs(r.ready, queue.r_en)
|
||||
|
||||
def test_w_stream(self):
|
||||
queue = fifo.SyncFIFOBuffered(width=4, depth=16)
|
||||
w = queue.w_stream
|
||||
self.assertFalse(w.signature.always_valid)
|
||||
self.assertFalse(w.signature.always_ready)
|
||||
self.assertIs(w.payload, queue.w_data)
|
||||
self.assertIs(w.valid, queue.w_en)
|
||||
self.assertIs(w.ready, queue.w_rdy)
|
|
@ -1497,6 +1497,31 @@ class SimulatorRegressionTestCase(FHDLTestCase):
|
|||
sim.add_testbench(testbench)
|
||||
sim.run()
|
||||
|
||||
def test_comb_clock_conflict(self):
|
||||
c = Signal()
|
||||
m = Module()
|
||||
m.d.comb += ClockSignal().eq(c)
|
||||
sim = Simulator(m)
|
||||
with self.assertRaisesRegex(DriverConflict,
|
||||
r"^Clock signal is already driven by combinational logic$"):
|
||||
sim.add_clock(1e-6)
|
||||
|
||||
def test_initial(self):
|
||||
a = Signal(4, init=3)
|
||||
m = Module()
|
||||
sim = Simulator(m)
|
||||
fired = 0
|
||||
|
||||
async def process(ctx):
|
||||
nonlocal fired
|
||||
async for val_a, in ctx.changed(a):
|
||||
self.assertEqual(val_a, 3)
|
||||
fired += 1
|
||||
|
||||
sim.add_process(process)
|
||||
sim.run()
|
||||
self.assertEqual(fired, 1)
|
||||
|
||||
def test_sample(self):
|
||||
m = Module()
|
||||
m.domains.sync = cd_sync = ClockDomain()
|
||||
|
|
|
@ -62,7 +62,7 @@ class FHDLTestCase(unittest.TestCase):
|
|||
smtbmc
|
||||
|
||||
[script]
|
||||
read_ilang top.il
|
||||
read_rtlil top.il
|
||||
prep
|
||||
{script}
|
||||
|
||||
|
|
Loading…
Reference in a new issue