From f5a8c07d54acd2c05082cd14117d5952076007e9 Mon Sep 17 00:00:00 2001 From: Wanda Date: Fri, 14 Jun 2024 19:34:19 +0200 Subject: [PATCH 01/10] sim: raise an exception on `add_clock` conflict with comb driver. --- amaranth/sim/pysim.py | 4 ++++ tests/test_sim.py | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py index a3c07cf..3c1275a 100644 --- a/amaranth/sim/pysim.py +++ b/amaranth/sim/pysim.py @@ -616,6 +616,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)) diff --git a/tests/test_sim.py b/tests/test_sim.py index 94631a9..b681683 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -1497,6 +1497,15 @@ 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_sample(self): m = Module() m.domains.sync = cd_sync = ClockDomain() From 3c64c66b5c402addcbd20b9056d4e0965d2acc39 Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 3 Apr 2024 09:50:26 +0000 Subject: [PATCH 02/10] _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 35adcf9..9384615 100644 --- a/amaranth/_utils.py +++ b/amaranth/_utils.py @@ -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: From 2c0265d827e7b2ad1b80d4c50ba2c814bfb9e397 Mon Sep 17 00:00:00 2001 From: Catherine Date: Wed, 3 Apr 2024 10:49:23 +0000 Subject: [PATCH 03/10] 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 768b6c6..5b8be9c 100644 --- a/amaranth/lib/fifo.py +++ b/amaranth/lib/fifo.py @@ -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): diff --git a/amaranth/lib/stream.py b/amaranth/lib/stream.py new file mode 100644 index 0000000..2cbf0d4 --- /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 88bd6b2..59e6986 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 4fbc510..01e2034 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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

)NdCvT5y+H`K7z5X8gx#C|fWE#W6vfk|v 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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 0000000..acdcf9a --- /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 080c3cc..1d74683 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 939fe8a..d7afd0e 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 @@ 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): """ diff --git a/tests/test_lib_stream.py b/tests/test_lib_stream.py new file mode 100644 index 0000000..17ce596 --- /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 64809bea9995efb25e68d6082e447de74ae41a6c Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 14 Jun 2024 19:19:49 +0100 Subject: [PATCH 04/10] 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 59e6986..1b02fe7 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=`. +* 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 `. * 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`. -* 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 ` 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 f3bcdf4782e17797cd93a831016963d4a81a18ab Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 14 Jun 2024 19:21:44 +0100 Subject: [PATCH 05/10] 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 1b02fe7..d6bcb48 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. From 3d2cd15435bed62f026af64ba9c99b6f72759a97 Mon Sep 17 00:00:00 2001 From: Wanda Date: Fri, 14 Jun 2024 20:51:37 +0200 Subject: [PATCH 06/10] sim: awaken all processes waiting on `changed()` at time 0. --- amaranth/sim/_async.py | 8 ++++++++ amaranth/sim/pysim.py | 10 +++++++++- tests/test_sim.py | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/amaranth/sim/_async.py b/amaranth/sim/_async.py index dd38e12..2f41dd3 100644 --- a/amaranth/sim/_async.py +++ b/amaranth/sim/_async.py @@ -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 diff --git a/amaranth/sim/pysim.py b/amaranth/sim/pysim.py index 3c1275a..2cc646e 100644 --- a/amaranth/sim/pysim.py +++ b/amaranth/sim/pysim.py @@ -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: diff --git a/tests/test_sim.py b/tests/test_sim.py index b681683..612068d 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -1506,6 +1506,22 @@ class SimulatorRegressionTestCase(FHDLTestCase): 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() From a0750b89c6060d9f809159a012a26cff4e22e69d Mon Sep 17 00:00:00 2001 From: Catherine Date: Fri, 14 Jun 2024 20:32:49 +0100 Subject: [PATCH 07/10] Release version 0.5. --- docs/changes.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/changes.rst b/docs/changes.rst index d6bcb48..0280916 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -18,8 +18,8 @@ Documentation for past releases of the Amaranth language and toolchain is availa * `Amaranth 0.3 `_ -Version 0.5 (unreleased) -======================== +Version 0.5 +=========== The Migen compatibility layer has been removed. From de685ca026ba1eee4d115d664621a1c498410589 Mon Sep 17 00:00:00 2001 From: Robin Ole Heinemann Date: Mon, 11 Nov 2024 18:40:28 +0100 Subject: [PATCH 08/10] rpc: use rtlil instead of ilang The ilang alias to the rtlil frontend got removed in https://github.com/YosysHQ/yosys/pull/4704 --- amaranth/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/amaranth/rpc.py b/amaranth/rpc.py index af0f125..c34b22b 100644 --- a/amaranth/rpc.py +++ b/amaranth/rpc.py @@ -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)}"} From bf4f22d2d57e3f17faf3e80bf3631b5e811bce8d Mon Sep 17 00:00:00 2001 From: Sebastian Holzapfel Date: Fri, 15 Nov 2024 10:18:31 +0100 Subject: [PATCH 09/10] backends: s/ilang/rtlil as a result of https://github.com/YosysHQ/yosys/pull/4704 --- amaranth/back/cxxrtl.py | 4 ++-- amaranth/back/verilog.py | 2 +- amaranth/vendor/_altera.py | 6 +++--- amaranth/vendor/_gowin.py | 4 ++-- amaranth/vendor/_lattice.py | 12 ++++++------ amaranth/vendor/_siliconblue.py | 6 +++--- tests/utils.py | 2 +- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/amaranth/back/cxxrtl.py b/amaranth/back/cxxrtl.py index 0fa657b..725bda4 100644 --- a/amaranth/back/cxxrtl.py +++ b/amaranth/back/cxxrtl.py @@ -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 <= (0, 40)) script = [] - script.append(f"read_ilang <`` 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_`` in Yosys script. * ``yosys_opts``: adds extra options for ``yosys``. * ``nextpnr_opts``: adds extra options for ``nextpnr-``. @@ -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}} diff --git a/amaranth/vendor/_siliconblue.py b/amaranth/vendor/_siliconblue.py index 0db8380..ae0d183 100644 --- a/amaranth/vendor/_siliconblue.py +++ b/amaranth/vendor/_siliconblue.py @@ -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)")}} diff --git a/tests/utils.py b/tests/utils.py index a069f18..c11e237 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -62,7 +62,7 @@ class FHDLTestCase(unittest.TestCase): smtbmc [script] - read_ilang top.il + read_rtlil top.il prep {script} From 02f63ba874b633145b0309f71ac519e812f33cf2 Mon Sep 17 00:00:00 2001 From: tmbv Date: Sun, 17 Aug 2025 19:21:16 +0200 Subject: [PATCH 10/10] add GateMate backend --- amaranth/vendor/__init__.py | 4 + amaranth/vendor/_colognechip.py | 399 ++++++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+) create mode 100644 amaranth/vendor/_colognechip.py diff --git a/amaranth/vendor/__init__.py b/amaranth/vendor/__init__.py index c60bb4d..2abe079 100644 --- a/amaranth/vendor/__init__.py +++ b/amaranth/vendor/__init__.py @@ -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 diff --git a/amaranth/vendor/_colognechip.py b/amaranth/vendor/_colognechip.py new file mode 100644 index 0000000..cc239dc --- /dev/null +++ b/amaranth/vendor/_colognechip.py @@ -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_`` Yosys command. + * ``script_after_read``: inserts commands after ``read_rtlil`` in Yosys script. + * ``script_after_synth``: inserts commands after ``synth_`` in Yosys script. + * ``yosys_opts``: adds extra options for ``yosys``. + * ``nextpnr_opts``: adds extra options for ``nextpnr-``. + * ``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