From f6ed388d43209e82c9c41881925243eb90319cd2 Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 3 Apr 2024 09:50:26 +0000 Subject: [PATCH 1/4] _utils: remove unused `extend` decorator. This decorator was only used in the (removed) compat layer. --- amaranth/_utils.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/amaranth/_utils.py b/amaranth/_utils.py index 35adcf9fc..938461527 100644 --- a/amaranth/_utils.py +++ b/amaranth/_utils.py @@ -76,16 +76,6 @@ def decorator_like(*args, **kwargs): 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: From ce64d640c2c6b9d499857a33e1940de88b73f60e Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 3 Apr 2024 10:49:23 +0000 Subject: [PATCH 2/4] Implement RFC 61: Minimal streams. --- amaranth/lib/fifo.py | 17 + amaranth/lib/stream.py | 122 +++++++ docs/changes.rst | 3 + docs/stdlib.rst | 5 +- docs/stdlib/_images/stream_pipeline.png | Bin 0 -> 57637 bytes docs/stdlib/stream.rst | 416 ++++++++++++++++++++++++ pyproject.toml | 2 +- tests/test_lib_fifo.py | 17 +- tests/test_lib_stream.py | 135 ++++++++ 9 files changed, 712 insertions(+), 5 deletions(-) create mode 100644 amaranth/lib/stream.py create mode 100644 docs/stdlib/_images/stream_pipeline.png create mode 100644 docs/stdlib/stream.rst create mode 100644 tests/test_lib_stream.py diff --git a/amaranth/lib/fifo.py b/amaranth/lib/fifo.py index 768b6c60d..5b8be9cae 100644 --- a/amaranth/lib/fifo.py +++ b/amaranth/lib/fifo.py @@ -6,6 +6,7 @@ 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 @@ def __init__(self, *, width, depth): 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): diff --git a/amaranth/lib/stream.py b/amaranth/lib/stream.py new file mode 100644 index 000000000..2cbf0d422 --- /dev/null +++ b/amaranth/lib/stream.py @@ -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})") \ No newline at end of file diff --git a/docs/changes.rst b/docs/changes.rst index 88bd6b2c6..59e698644 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -65,6 +65,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 @@ -83,6 +84,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 @@ -130,6 +132,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`_) diff --git a/docs/stdlib.rst b/docs/stdlib.rst index 4fbc51068..01e203459 100644 --- a/docs/stdlib.rst +++ b/docs/stdlib.rst @@ -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 `_. @@ -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 diff --git a/docs/stdlib/_images/stream_pipeline.png b/docs/stdlib/_images/stream_pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..89d90ef1a28758e77beb34661062c37d8620742e GIT binary patch literal 57637 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 literal 0 HcmV?d00001 diff --git a/docs/stdlib/stream.rst b/docs/stdlib/stream.rst new file mode 100644 index 000000000..acdcf9a48 --- /dev/null +++ b/docs/stdlib/stream.rst @@ -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 ` 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 `, 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 `, and any :ref:`control flow modifiers ` 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 ` 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 `, but also follow the :ref:`data transfer rules `. + +.. autoclass:: Signature + +.. autoclass:: Interface diff --git a/pyproject.toml b/pyproject.toml index 080c3cc77..1d74683c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/tests/test_lib_fifo.py b/tests/test_lib_fifo.py index 939fe8a7e..d7afd0e43 100644 --- a/tests/test_lib_fifo.py +++ b/tests/test_lib_fifo.py @@ -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 @@ def test_async_buffered_depth_wrong(self): 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): """ diff --git a/tests/test_lib_stream.py b/tests/test_lib_stream.py new file mode 100644 index 000000000..17ce59684 --- /dev/null +++ b/tests/test_lib_stream.py @@ -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) From 582870dd5432c1f5bdf377c47ab5868d9c5c3c18 Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 14 Jun 2024 19:19:49 +0100 Subject: [PATCH 3/4] docs/changes: reorder migration checklist by importance. --- docs/changes.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index 59e698644..1b02fe7fb 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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=`. -* Convert uses of :py:`Simulator.add_sync_process` used as testbenches to :meth:`Simulator.add_testbench `. -* Convert other uses of :py:`Simulator.add_sync_process` to :meth:`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`. +* 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. -* 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. +* 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 `. +* Convert other uses of :py:`Simulator.add_sync_process` to :meth:`Simulator.add_process `. +* Convert simulator processes and testbenches to use the new async API. * Update uses of :meth:`Simulator.add_clock ` with explicit :py:`phase` to take into account simulator no longer adding implicit :py:`period / 2`. (Previously, :meth:`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 ` 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 From b0d1d1107393fa456de9d6497b04457a39543f87 Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 14 Jun 2024 19:21:44 +0100 Subject: [PATCH 4/4] docs/changes: add RFC 42. --- docs/changes.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/changes.rst b/docs/changes.rst index 1b02fe7fb..d6bcb4872 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -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 @@ -75,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)`` @@ -105,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.