Compare commits

...

10 commits

Author SHA1 Message Date
tmbv 02f63ba874 add GateMate backend
Some checks are pending
CI / test (${{ matrix.python-version }}) (false, 3.10) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, 3.11) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, 3.12) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, 3.8) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, 3.9) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, pypy-3.10) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, pypy-3.8) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (false, pypy-3.9) (push) Waiting to run
CI / test (${{ matrix.python-version }}) (true, 3.13-dev) (push) Waiting to run
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-boards) (push) Waiting to run
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-soc) (push) Waiting to run
CI / smoke (${{ matrix.project }}) (amaranth-lang/amaranth-stdio) (push) Waiting to run
CI / document (push) Waiting to run
CI / check-links (push) Waiting to run
CI / required (push) Blocked by required conditions
CI / publish-docs (push) Blocked by required conditions
CI / publish-docs-dev (push) Blocked by required conditions
CI / publish-schemas (push) Blocked by required conditions
CI / publish-package (push) Blocked by required conditions
CI / publish-release (push) Blocked by required conditions
2025-08-18 18:03:20 +02:00
Sebastian Holzapfel bf4f22d2d5 backends: s/ilang/rtlil as a result of https://github.com/YosysHQ/yosys/pull/4704 2025-08-18 17:46:28 +02:00
Robin Ole Heinemann de685ca026 rpc: use rtlil instead of ilang
The ilang alias to the rtlil frontend got removed in https://github.com/YosysHQ/yosys/pull/4704
2025-08-18 17:43:41 +02:00
Catherine a0750b89c6 Release version 0.5. 2024-06-14 19:41:03 +00:00
Wanda 3d2cd15435 sim: awaken all processes waiting on changed() at time 0. 2024-06-14 19:25:47 +00:00
Catherine f3bcdf4782 docs/changes: add RFC 42. 2024-06-14 18:51:38 +00:00
Catherine 64809bea99 docs/changes: reorder migration checklist by importance. 2024-06-14 18:51:38 +00:00
Catherine 2c0265d827 Implement RFC 61: Minimal streams. 2024-06-14 18:51:38 +00:00
Catherine 3c64c66b5c _utils: remove unused extend decorator.
This decorator was only used in the (removed) compat layer.
2024-06-14 18:51:38 +00:00
Wanda f5a8c07d54 sim: raise an exception on add_clock conflict with comb driver. 2024-06-14 17:47:08 +00:00
23 changed files with 1195 additions and 47 deletions

View file

@ -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:

View file

@ -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)

View file

@ -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")

View file

@ -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
View 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})")

View file

@ -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)}"}

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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)")}}

View 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

View file

@ -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)")}}

View file

@ -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}}

View file

@ -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)")}}

View file

@ -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`_)

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

416
docs/stdlib/stream.rst Normal file
View 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

View file

@ -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",
]

View file

@ -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
View 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)

View file

@ -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()

View file

@ -62,7 +62,7 @@ class FHDLTestCase(unittest.TestCase):
smtbmc
[script]
read_ilang top.il
read_rtlil top.il
prep
{script}