diff --git a/amaranth/lib/io.py b/amaranth/lib/io.py index e4ea8b2ec..6eed2bf76 100644 --- a/amaranth/lib/io.py +++ b/amaranth/lib/io.py @@ -1,5 +1,6 @@ import enum import operator +import warnings from abc import ABCMeta, abstractmethod from collections.abc import Iterable @@ -11,7 +12,7 @@ __all__ = [ - "Direction", "PortLike", "SingleEndedPort", "DifferentialPort", + "Direction", "PortLike", "SingleEndedPort", "DifferentialPort", "SimulationPort", "Buffer", "FFBuffer", "DDRBuffer", "Pin", ] @@ -57,6 +58,12 @@ class PortLike(metaclass=ABCMeta): :class:`amaranth.hdl.IOPort` is not an instance of :class:`amaranth.lib.io.PortLike`. """ + # TODO(amaranth-0.6): remove + def __init_subclass__(cls): + if cls.__add__ is PortLike.__add__: + warnings.warn(f"{cls.__module__}.{cls.__qualname__} must override the `__add__` method", + DeprecationWarning, stacklevel=2) + @property @abstractmethod def direction(self): @@ -108,6 +115,32 @@ def __invert__(self): """ raise NotImplementedError # :nocov: + # TODO(amaranth-0.6): make abstract + # @abstractmethod + def __add__(self, other): + """Concatenates two library I/O ports of the same type. + + The direction of the resulting port is: + + * The same as the direction of both, if the two ports have the same direction. + * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. + * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. + + Returns + ------- + :py:`type(self)` + A new :py:`type(self)` which contains wires from :py:`self` followed by wires + from :py:`other`, preserving their polarity inversion. + + Raises + ------ + :exc:`ValueError` + If an input port is concatenated with an output port. + :exc:`TypeError` + If :py:`self` and :py:`other` have different types. + """ + raise NotImplementedError # :nocov: + class SingleEndedPort(PortLike): """Represents a single-ended library I/O port. @@ -124,9 +157,9 @@ class SingleEndedPort(PortLike): same length as the width of :py:`io`, and the inversion is specified for individual wires. direction : :class:`Direction` or :class:`str` Set of allowed buffer directions. A string is converted to a :class:`Direction` first. - If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used - with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be - used with buffers of any direction. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. Attributes ---------- @@ -176,27 +209,6 @@ def __getitem__(self, index): direction=self._direction) def __add__(self, other): - """Concatenates two single-ended library I/O ports. - - The direction of the resulting port is: - - * The same as the direction of both, if the two ports have the same direction. - * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. - * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. - - Returns - ------- - :class:`SingleEndedPort` - A new :class:`SingleEndedPort` which contains wires from :py:`self` followed by wires - from :py:`other`, preserving their polarity inversion. - - Raises - ------ - :exc:`ValueError` - If an input port is concatenated with an output port. - :exc:`TypeError` - If :py:`self` and :py:`other` have incompatible types. - """ if not isinstance(other, SingleEndedPort): return NotImplemented return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert, @@ -231,9 +243,9 @@ class DifferentialPort(PortLike): individual wires. direction : :class:`Direction` or :class:`str` Set of allowed buffer directions. A string is converted to a :class:`Direction` first. - If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used - with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be - used with buffers of any direction. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. Attributes ---------- @@ -293,27 +305,6 @@ def __getitem__(self, index): direction=self._direction) def __add__(self, other): - """Concatenates two differential library I/O ports. - - The direction of the resulting port is: - - * The same as the direction of both, if the two ports have the same direction. - * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port. - * :attr:`Direction.Output` if a bidirectional port is concatenated with an output port. - - Returns - ------- - :class:`DifferentialPort` - A new :class:`DifferentialPort` which contains pairs of wires from :py:`self` followed - by pairs of wires from :py:`other`, preserving their polarity inversion. - - Raises - ------ - :exc:`ValueError` - If an input port is concatenated with an output port. - :exc:`TypeError` - If :py:`self` and :py:`other` have incompatible types. - """ if not isinstance(other, DifferentialPort): return NotImplemented return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n), @@ -331,6 +322,167 @@ def __repr__(self): f"direction={self._direction})") +class SimulationPort(PortLike): + """Represents a simulation library I/O port. + + Implements the :class:`PortLike` interface. + + Parameters + ---------- + direction : :class:`Direction` or :class:`str` + Set of allowed buffer directions. A string is converted to a :class:`Direction` first. + If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be + used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port + can be used with buffers of any direction. + width : :class:`int` + Width of the port. The width of each of the attributes :py:`i`, :py:`o`, :py:`oe` (whenever + present) equals :py:`width`. + invert : :class:`bool` or iterable of :class:`bool` + Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for + the entire port. If the value is an iterable of :class:`bool`, the iterable must have the + same length as the width of :py:`p` and :py:`n`, and the inversion is specified for + individual wires. + name : :class:`str` or :py:`None` + Name of the port. This name is only used to derive the names of the input, output, and + output enable signals. + src_loc_at : :class:`int` + :ref:`Source location `. Used to infer :py:`name` if not specified. + + Attributes + ---------- + i : :class:`Signal` + Input signal. Present if :py:`direction in (Input, Bidir)`. + o : :class:`Signal` + Ouptut signal. Present if :py:`direction in (Output, Bidir)`. + oe : :class:`Signal` + Output enable signal. Present if :py:`direction in (Output, Bidir)`. + invert : :class:`tuple` of :class:`bool` + The :py:`invert` parameter, normalized to specify polarity inversion per-wire. + direction : :class:`Direction` + The :py:`direction` parameter, normalized to the :class:`Direction` enumeration. + """ + def __init__(self, direction, width, *, invert=False, name=None, src_loc_at=0): + if name is not None and not isinstance(name, str): + raise TypeError(f"Name must be a string, not {name!r}") + if name is None: + name = tracer.get_var_name(depth=2 + src_loc_at, default="$port") + + if not (isinstance(width, int) and width >= 0): + raise TypeError(f"Width must be a non-negative integer, not {width!r}") + + self._direction = Direction(direction) + + self._i = self._o = self._oe = None + if self._direction in (Direction.Input, Direction.Bidir): + self._i = Signal(width, name=f"{name}__i") + if self._direction in (Direction.Output, Direction.Bidir): + self._o = Signal(width, name=f"{name}__o") + self._oe = Signal(width, name=f"{name}__oe", + init=~0 if self._direction is Direction.Output else 0) + + if isinstance(invert, bool): + self._invert = (invert,) * width + elif isinstance(invert, Iterable): + self._invert = tuple(invert) + if len(self._invert) != width: + raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match " + f"port width ({width})") + if not all(isinstance(item, bool) for item in self._invert): + raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}") + else: + raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}") + + @property + def i(self): + if self._i is None: + raise AttributeError( + "Simulation port with output direction does not have an input signal") + return self._i + + @property + def o(self): + if self._o is None: + raise AttributeError( + "Simulation port with input direction does not have an output signal") + return self._o + + @property + def oe(self): + if self._oe is None: + raise AttributeError( + "Simulation port with input direction does not have an output enable signal") + return self._oe + + @property + def invert(self): + return self._invert + + @property + def direction(self): + return self._direction + + def __len__(self): + if self._direction is Direction.Input: + return len(self._i) + if self._direction is Direction.Output: + assert len(self._o) == len(self._oe) + return len(self._o) + if self._direction is Direction.Bidir: + assert len(self._i) == len(self._o) == len(self._oe) + return len(self._i) + assert False # :nocov: + + def __getitem__(self, key): + result = object.__new__(type(self)) + result._i = None if self._i is None else self._i [key] + result._o = None if self._o is None else self._o [key] + result._oe = None if self._oe is None else self._oe[key] + if isinstance(key, slice): + result._invert = self._invert[key] + else: + result._invert = (self._invert[key],) + result._direction = self._direction + return result + + def __invert__(self): + result = object.__new__(type(self)) + result._i = self._i + result._o = self._o + result._oe = self._oe + result._invert = tuple(not invert for invert in self._invert) + result._direction = self._direction + return result + + def __add__(self, other): + if not isinstance(other, SimulationPort): + return NotImplemented + direction = self._direction & other._direction + result = object.__new__(type(self)) + result._i = None if direction is Direction.Output else Cat(self._i, other._i) + result._o = None if direction is Direction.Input else Cat(self._o, other._o) + result._oe = None if direction is Direction.Input else Cat(self._oe, other._oe) + result._invert = self._invert + other._invert + result._direction = direction + return result + + def __repr__(self): + parts = [] + if self._i is not None: + parts.append(f"i={self._i!r}") + if self._o is not None: + parts.append(f"o={self._o!r}") + if self._oe is not None: + parts.append(f"oe={self._oe!r}") + if not any(self._invert): + invert = False + elif all(self._invert): + invert = True + else: + invert = self._invert + return (f"SimulationPort({', '.join(parts)}, invert={invert!r}, " + f"direction={self._direction})") + + class Buffer(wiring.Component): """A combinational I/O buffer component. @@ -476,6 +628,18 @@ def elaborate(self, platform): else: m.submodules += IOBufferInstance(self._port.p, o=o_inv, oe=self.oe, i=i_inv) m.submodules += IOBufferInstance(self._port.n, o=~o_inv, oe=self.oe) + elif isinstance(self._port, SimulationPort): + if self.direction is Direction.Bidir: + # Loop back `o` if `oe` is asserted. This frees the test harness from having to + # provide this functionality itself. + for i_inv_bit, oe_bit, o_bit, i_bit in \ + zip(i_inv, self._port.oe, self._port.o, self._port.i): + m.d.comb += i_inv_bit.eq(Cat(Mux(oe_bit, o_bit, i_bit))) + if self.direction is Direction.Input: + m.d.comb += i_inv.eq(self._port.i) + if self.direction in (Direction.Output, Direction.Bidir): + m.d.comb += self._port.o.eq(o_inv) + m.d.comb += self._port.oe.eq(self.oe.replicate(len(self._port))) else: raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") # :nocov: @@ -719,6 +883,12 @@ class DDRBuffer(wiring.Component): This limitation may be lifted in the future. + .. warning:: + + Double data rate I/O buffers are not compatible with :class:`SimulationPort`. + + This limitation may be lifted in the future. + Parameters ---------- direction : :class:`Direction` @@ -826,6 +996,9 @@ def elaborate(self, platform): if hasattr(platform, "get_io_buffer"): return platform.get_io_buffer(self) + if isinstance(self._port, SimulationPort): + raise NotImplementedError(f"DDR buffers are not supported in simulation") # :nocov: + raise NotImplementedError(f"DDR buffers are not supported on {platform!r}") # :nocov: diff --git a/docs/changes.rst b/docs/changes.rst index 93d44fb0b..cc99a0b84 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -1,7 +1,7 @@ Changelog ######### -This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes. +This document describes changes to the public interfaces in the Amaranth language and standard library. It does not include most bug fixes or implementation changes; versions which do not include notable changes are not listed here. Documentation for past releases @@ -9,6 +9,7 @@ Documentation for past releases Documentation for past releases of the Amaranth language and toolchain is available online: +* `Amaranth 0.5.0 `_ * `Amaranth 0.4.5 `_ * `Amaranth 0.4.4 `_ * `Amaranth 0.4.3 `_ @@ -22,6 +23,14 @@ Version 0.5.1 ============= +Implemented RFCs +---------------- + +.. _RFC 69: https://amaranth-lang.org/rfcs/0069-simulation-port.html + +* `RFC 69`_: Add a ``lib.io.PortLike`` object usable in simulation + + Standard library changes ------------------------ @@ -30,10 +39,11 @@ Standard library changes * Added: views of :class:`amaranth.lib.data.ArrayLayout` can be indexed with negative integers or slices. * Added: :py:`len()` works on views of :class:`amaranth.lib.data.ArrayLayout`. * Added: views of :class:`amaranth.lib.data.ArrayLayout` are iterable. +* Added: :class:`io.SimulationPort`. (`RFC 69`_) -Version 0.5 -=========== +Version 0.5.0 +============= The Migen compatibility layer has been removed. @@ -132,9 +142,9 @@ Language changes * Deprecated: :class:`amaranth.hdl.Memory`. (`RFC 45`_) * Deprecated: upwards propagation of clock domains. (`RFC 59`_) * Deprecated: :meth:`Value.implies`. -* Removed: (deprecated in 0.4) :meth:`Const.normalize`. (`RFC 5`_) -* Removed: (deprecated in 0.4) :class:`Repl`. (`RFC 10`_) -* Removed: (deprecated in 0.4) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`. +* Removed: (deprecated in 0.4.0) :meth:`Const.normalize`. (`RFC 5`_) +* Removed: (deprecated in 0.4.0) :class:`Repl`. (`RFC 10`_) +* Removed: (deprecated in 0.4.0) :class:`ast.Sample`, :class:`ast.Past`, :class:`ast.Stable`, :class:`ast.Rose`, :class:`ast.Fell`. * Removed: assertion names in :class:`Assert`, :class:`Assume` and :class:`Cover`. (`RFC 50`_) * Removed: accepting non-subclasses of :class:`Elaboratable` as elaboratables. @@ -153,9 +163,9 @@ Standard library changes * 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`_) -* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_) +* Removed: (deprecated in 0.4.0) :mod:`amaranth.lib.scheduler`. (`RFC 19`_) +* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.FIFOInterface` with :py:`fwft=False`. (`RFC 20`_) +* Removed: (deprecated in 0.4.0) :class:`amaranth.lib.fifo.SyncFIFO` with :py:`fwft=False`. (`RFC 20`_) Toolchain changes @@ -170,7 +180,7 @@ Toolchain changes * Deprecated: :py:`Simulator.add_sync_process`. (`RFC 27`_) * Deprecated: generator-based simulation processes and testbenches. (`RFC 36`_) * Deprecated: the :py:`run_passive` argument to :meth:`Simulator.run_until ` has been deprecated, and does nothing. -* Removed: (deprecated in 0.4) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``. +* Removed: (deprecated in 0.4.0) use of mixed-case toolchain environment variable names, such as ``NMIGEN_ENV_Diamond`` or ``AMARANTH_ENV_Diamond``; use upper-case environment variable names, such as ``AMARANTH_ENV_DIAMOND``. Platform integration changes @@ -183,11 +193,11 @@ Platform integration changes * Added: ``build.sh`` begins with ``#!/bin/sh``. * Changed: ``IntelPlatform`` renamed to ``AlteraPlatform``. * Deprecated: argument :py:`run_script=` in :meth:`BuildPlan.execute_local`. -* Removed: (deprecated in 0.4) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_) +* Removed: (deprecated in 0.4.0) :mod:`vendor.intel`, :mod:`vendor.lattice_ecp5`, :mod:`vendor.lattice_ice40`, :mod:`vendor.lattice_machxo2_3l`, :mod:`vendor.quicklogic`, :mod:`vendor.xilinx`. (`RFC 18`_) -Version 0.4 -=========== +Version 0.4.0 +============= Support has been added for a new and improved way of defining data structures in :mod:`amaranth.lib.data` and component interfaces in :mod:`amaranth.lib.wiring`, as defined in `RFC 1`_ and `RFC 2`_. :class:`Record` has been deprecated. In a departure from the usual policy, to give designers additional time to migrate, :class:`Record` will be removed in Amaranth 0.6 (one release later than normal). diff --git a/docs/stdlib/io.rst b/docs/stdlib/io.rst index 3740ccfdd..295fbc867 100644 --- a/docs/stdlib/io.rst +++ b/docs/stdlib/io.rst @@ -60,7 +60,8 @@ All of the following examples assume that one of the built-in FPGA platforms is .. testcode:: - from amaranth.lib import io, wiring + from amaranth.sim import Simulator + from amaranth.lib import io, wiring, stream from amaranth.lib.wiring import In, Out @@ -191,6 +192,74 @@ In this example of a `source-synchronous interface ` or :ref:`I/O buffer instances ` as it only operates on unidirectionally driven two-state wires. This module provides a simulation-only library I/O port, :class:`SimulationPort`, so that components that use library I/O buffers can be tested. + +A component that is designed for testing should accept the library I/O ports it will drive as constructor parameters rather than requesting them from the platform directly. Synthesizable designs will instantiate the component with a :class:`SingleEndedPort`, :class:`DifferentialPort`, or a platform-specific library I/O port, while tests will instantiate the component with a :class:`SimulationPort`. Tests are able to inject inputs into the component using :py:`sim_port.i`, capture the outputs of the component via :py:`sim_port.o`, and ensure that the component is driving the outputs at the appropriate times using :py:`sim_port.oe`. + +For example, consider a simple serializer that accepts a stream of multi-bit data words and outputs them bit by bit. It can be tested as follows: + +.. testcode:: + + class OutputSerializer(wiring.Component): + data: In(stream.Signature(8)) + + def __init__(self, dclk_port, dout_port): + self.dclk_port = dclk_port + self.dout_port = dout_port + + super().__init__() + + def elaborate(self, platform): + m = Module() + + m.submodules.dclk = dclk = io.Buffer("o", self.dclk_port) + m.submodules.dout = dout = io.Buffer("o", self.dout_port) + + index = Signal(range(8)) + m.d.comb += dout.o.eq(self.data.payload.bit_select(index, 1)) + + with m.If(self.data.valid): + m.d.sync += dclk.o.eq(~dclk.o) + with m.If(dclk.o): + m.d.sync += index.eq(index + 1) + with m.If(index == 7): + m.d.comb += self.data.ready.eq(1) + + return m + + def test_output_serializer(): + dclk_port = io.SimulationPort("o", 1) + dout_port = io.SimulationPort("o", 1) + + dut = OutputSerializer(dclk_port, dout_port) + + async def testbench_write_data(ctx): + ctx.set(dut.data.payload, 0xA1) + ctx.set(dut.data.valid, 1) + await ctx.tick().until(dut.data.ready) + ctx.set(dut.data.valid, 0) + + async def testbench_sample_output(ctx): + for bit in [1,0,0,0,0,1,0,1]: + _, dout_value = await ctx.posedge(dut.dclk_port.o).sample(dut.dout_port.o) + assert ctx.get(dut.dout_port.oe) == 1, "DUT is not driving the data output" + assert dout_value == bit, "DUT drives the wrong value on data output" + + sim = Simulator(dut) + sim.add_clock(1e-6) + sim.add_testbench(testbench_write_data) + sim.add_testbench(testbench_sample_output) + sim.run() + +.. testcode:: + :hide: + + test_output_serializer() + + Ports ----- @@ -199,6 +268,7 @@ Ports .. autoclass:: PortLike .. autoclass:: SingleEndedPort .. autoclass:: DifferentialPort +.. autoclass:: SimulationPort Buffers diff --git a/tests/test_lib_io.py b/tests/test_lib_io.py index 9dc208ede..e904e7f41 100644 --- a/tests/test_lib_io.py +++ b/tests/test_lib_io.py @@ -30,6 +30,14 @@ def test_and(self): Direction.Bidir & 3 +class PortLikeTestCase(FHDLTestCase): + def test_warn___add__(self): + with self.assertWarnsRegex(DeprecationWarning, + r"WrongPortLike must override the `__add__` method$"): + class WrongPortLike(PortLike): + pass + + class SingleEndedPortTestCase(FHDLTestCase): def test_construct(self): io = IOPort(4) @@ -161,6 +169,123 @@ def test_invert(self): self.assertRepr(iport, "DifferentialPort((io-port iop), (io-port ion), invert=(False, True, False, True), direction=Direction.Output)") +class SimulationPortTestCase(FHDLTestCase): + def test_construct(self): + port_io = SimulationPort("io", 2) + self.assertEqual(port_io.direction, Direction.Bidir) + self.assertEqual(len(port_io), 2) + self.assertEqual(port_io.invert, (False, False)) + self.assertIsInstance(port_io.i, Signal) + self.assertEqual(port_io.i.shape(), unsigned(2)) + self.assertIsInstance(port_io.o, Signal) + self.assertEqual(port_io.o.shape(), unsigned(2)) + self.assertEqual(port_io.o.init, 0) + self.assertIsInstance(port_io.oe, Signal) + self.assertEqual(port_io.oe.shape(), unsigned(2)) + self.assertEqual(port_io.oe.init, 0) + self.assertRepr(port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=False, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertEqual(port_i.direction, Direction.Input) + self.assertEqual(len(port_i), 3) + self.assertEqual(port_i.invert, (True, True, True)) + self.assertIsInstance(port_i.i, Signal) + self.assertEqual(port_i.i.shape(), unsigned(3)) + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with input direction does not have an output signal$"): + port_i.o + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with input direction does not have an output enable signal$"): + port_i.oe + self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=True, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertEqual(port_o.direction, Direction.Output) + self.assertEqual(len(port_o), 2) + self.assertEqual(port_o.invert, (True, False)) + with self.assertRaisesRegex(AttributeError, + r"^Simulation port with output direction does not have an input signal$"): + port_o.i + self.assertIsInstance(port_o.o, Signal) + self.assertEqual(port_o.o.shape(), unsigned(2)) + self.assertEqual(port_o.o.init, 0) + self.assertIsInstance(port_o.oe, Signal) + self.assertEqual(port_o.oe.shape(), unsigned(2)) + self.assertEqual(port_o.oe.init, 0b11) + self.assertRepr(port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(True, False), direction=Direction.Output)") + + def test_construct_empty(self): + port_i = SimulationPort("i", 0, invert=True) + self.assertEqual(port_i.direction, Direction.Input) + self.assertEqual(len(port_i), 0) + self.assertEqual(port_i.invert, ()) + self.assertIsInstance(port_i.i, Signal) + self.assertEqual(port_i.i.shape(), unsigned(0)) + self.assertRepr(port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)") + + def test_name(self): + port = SimulationPort("io", 2, name="nyaa") + self.assertRepr(port, "SimulationPort(i=(sig nyaa__i), o=(sig nyaa__o), oe=(sig nyaa__oe), invert=False, direction=Direction.Bidir)") + + def test_name_wrong(self): + with self.assertRaisesRegex(TypeError, + r"^Name must be a string, not 1$"): + SimulationPort("io", 1, name=1) + + def test_construct_wrong(self): + with self.assertRaisesRegex(TypeError, + r"^Width must be a non-negative integer, not 'a'$"): + SimulationPort("io", "a") + with self.assertRaisesRegex(TypeError, + r"^Width must be a non-negative integer, not -1$"): + SimulationPort("io", -1) + with self.assertRaisesRegex(TypeError, + r"^'invert' must be a bool or iterable of bool, not 3$"): + SimulationPort("io", 1, invert=3) + with self.assertRaisesRegex(TypeError, + r"^'invert' must be a bool or iterable of bool, not \[1, 2\]$"): + SimulationPort("io", 2, invert=[1, 2]) + with self.assertRaisesRegex(ValueError, + r"^Length of 'invert' \(2\) doesn't match port width \(1\)$"): + SimulationPort("io", 1, invert=(False, True)) + + def test_slice(self): + port_io = SimulationPort("io", 2) + self.assertRepr(port_io[0], "SimulationPort(i=(slice (sig port_io__i) 0:1), o=(slice (sig port_io__o) 0:1), oe=(slice (sig port_io__oe) 0:1), invert=False, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertRepr(port_i[1:3], "SimulationPort(i=(slice (sig port_i__i) 1:3), invert=True, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertRepr(port_o[1], "SimulationPort(o=(slice (sig port_o__o) 1:2), oe=(slice (sig port_o__oe) 1:2), invert=False, direction=Direction.Output)") + + def test_invert(self): + port_io = SimulationPort("io", 2) + self.assertRepr(~port_io, "SimulationPort(i=(sig port_io__i), o=(sig port_io__o), oe=(sig port_io__oe), invert=True, direction=Direction.Bidir)") + + port_i = SimulationPort("i", 3, invert=True) + self.assertRepr(~port_i, "SimulationPort(i=(sig port_i__i), invert=False, direction=Direction.Input)") + + port_o = SimulationPort("o", 2, invert=(True, False)) + self.assertRepr(~port_o, "SimulationPort(o=(sig port_o__o), oe=(sig port_o__oe), invert=(False, True), direction=Direction.Output)") + + def test_add(self): + port_io = SimulationPort("io", 2) + port_io2 = SimulationPort("io", 2) + port_i = SimulationPort("i", 3, invert=True) + port_o = SimulationPort("o", 2, invert=(True, False)) + + self.assertRepr(port_io + port_io2, "SimulationPort(i=(cat (sig port_io__i) (sig port_io2__i)), o=(cat (sig port_io__o) (sig port_io2__o)), oe=(cat (sig port_io__oe) (sig port_io2__oe)), invert=False, direction=Direction.Bidir)") + self.assertRepr(port_io + port_i, "SimulationPort(i=(cat (sig port_io__i) (sig port_i__i)), invert=(False, False, True, True, True), direction=Direction.Input)") + self.assertRepr(port_io + port_o, "SimulationPort(o=(cat (sig port_io__o) (sig port_o__o)), oe=(cat (sig port_io__oe) (sig port_o__oe)), invert=(False, False, True, False), direction=Direction.Output)") + + def test_add_wrong(self): + io = IOPort(1) + with self.assertRaisesRegex(TypeError, + r"^unsupported operand type\(s\) for \+: 'SimulationPort' and 'SingleEndedPort'$"): + SimulationPort("io", 2) + SingleEndedPort(io) + + class BufferTestCase(FHDLTestCase): def test_signature(self): sig_i = Buffer.Signature("i", 4) @@ -378,6 +503,121 @@ def test_elaborate_diff(self): ) """) + def test_elaborate_sim(self): + port = SimulationPort("io", 4) + buf = Buffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (output 'i' 5.0:4) + (output 'port__o' 0.2:6) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (output 'i' 5.0:4) + (output 'port__o' 0.2:6) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (m 0.6 0.2 0.7)) + (cell 2 0 (m 0.6 0.3 0.8)) + (cell 3 0 (m 0.6 0.4 0.9)) + (cell 4 0 (m 0.6 0.5 0.10)) + (cell 5 0 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0))) + ) + """) + + port = SimulationPort("io", 4, invert=[False, True, False, True]) + buf = Buffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (output 'i' 2.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (output 'i' 2.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + (cell 2 0 (^ 7.0:4 4'd10)) + (cell 3 0 (m 0.6 1.0 0.7)) + (cell 4 0 (m 0.6 1.1 0.8)) + (cell 5 0 (m 0.6 1.2 0.9)) + (cell 6 0 (m 0.6 1.3 0.10)) + (cell 7 0 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0))) + ) + """) + + buf = Buffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (output 'i' 1.0:4) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + + buf = Buffer("o", port) + nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 0.6 0.6 0.6 0.6)) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + + # check that a port without `port.o`/`port.oe` works + port = SimulationPort("i", 4, invert=[False, True, False, True]) + buf = Buffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (output 'i' 1.0:4) + )) + (cell 1 0 (^ 0.2:6 4'd10)) + ) + """) + class FFBufferTestCase(FHDLTestCase): def test_signature(self): @@ -616,6 +856,150 @@ def test_elaborate(self): ) """) + def test_elaborate_sim(self): + port = SimulationPort("io", 4) + buf = FFBuffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (input 'clk' 0.11) + (input 'rst' 0.12) + (output 'i' 5.0:4) + (output 'port__o' 6.0:4) + (output 'port__oe' (cat 7.0 7.0 7.0 7.0)) + ) + (module 1 0 ('top' 'io_buffer') + (input 'port__i' 0.7:11) + (input 'port__o' 6.0:4) + (input 'oe' 7.0) + (output 'i' 8.0:4) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (input 'clk' 11:12) + (input 'rst' 12:13) + (output 'i' 5.0:4) + (output 'port__o' 6.0:4) + (output 'port__oe' (cat 7.0 7.0 7.0 7.0)) + )) + (cell 1 1 (m 7.0 6.0 0.7)) + (cell 2 1 (m 7.0 6.1 0.8)) + (cell 3 1 (m 7.0 6.2 0.9)) + (cell 4 1 (m 7.0 6.3 0.10)) + (cell 5 0 (flipflop 8.0:4 0 pos 0.11 0)) + (cell 6 0 (flipflop 0.2:6 0 pos 0.11 0)) + (cell 7 0 (flipflop 0.6 0 pos 0.11 0)) + (cell 8 1 (assignment_list 4'd0 (1 0:1 1.0) (1 1:2 2.0) (1 2:3 3.0) (1 3:4 4.0))) + ) + """) + + port = SimulationPort("io", 4, invert=[False, True, False, True]) + buf = FFBuffer("io", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, buf.o, buf.oe, port.i, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'port__i' 0.7:11) + (input 'clk' 0.11) + (input 'rst' 0.12) + (output 'i' 7.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 9.0 9.0 9.0 9.0)) + ) + (module 1 0 ('top' 'io_buffer') + (input 'port__i' 0.7:11) + (output 'o_inv' 1.0:4) + (output 'i' 2.0:4) + (input 'o' 8.0:4) + (input 'oe' 9.0) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'port__i' 7:11) + (input 'clk' 11:12) + (input 'rst' 12:13) + (output 'i' 7.0:4) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 9.0 9.0 9.0 9.0)) + )) + (cell 1 1 (^ 8.0:4 4'd10)) + (cell 2 1 (^ 10.0:4 4'd10)) + (cell 3 1 (m 9.0 1.0 0.7)) + (cell 4 1 (m 9.0 1.1 0.8)) + (cell 5 1 (m 9.0 1.2 0.9)) + (cell 6 1 (m 9.0 1.3 0.10)) + (cell 7 0 (flipflop 2.0:4 0 pos 0.11 0)) + (cell 8 0 (flipflop 0.2:6 0 pos 0.11 0)) + (cell 9 0 (flipflop 0.6 0 pos 0.11 0)) + (cell 10 1 (assignment_list 4'd0 (1 0:1 3.0) (1 1:2 4.0) (1 2:3 5.0) (1 3:4 6.0))) + ) + """) + + buf = FFBuffer("i", port) + nl = build_netlist(Fragment.get(buf, None), [buf.i, port.i]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'port__i' 0.2:6) + (input 'clk' 0.6) + (input 'rst' 0.7) + (output 'i' 2.0:4) + ) + (module 1 0 ('top' 'io_buffer') + (input 'i_inv' 0.2:6) + (output 'i' 1.0:4) + ) + (cell 0 0 (top + (input 'port__i' 2:6) + (input 'clk' 6:7) + (input 'rst' 7:8) + (output 'i' 2.0:4) + )) + (cell 1 1 (^ 0.2:6 4'd10)) + (cell 2 0 (flipflop 1.0:4 0 pos 0.6 0)) + ) + """) + + buf = FFBuffer("o", port) + nl = build_netlist(Fragment.get(buf, None), [buf.o, buf.oe, port.o, port.oe]) + self.assertRepr(nl, """ + ( + (module 0 None ('top') + (input 'o' 0.2:6) + (input 'oe' 0.6) + (input 'clk' 0.7) + (input 'rst' 0.8) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 3.0 3.0 3.0 3.0)) + ) + (module 1 0 ('top' 'io_buffer') + (output 'o_inv' 1.0:4) + (input 'o' 2.0:4) + (input 'oe' 3.0) + ) + (cell 0 0 (top + (input 'o' 2:6) + (input 'oe' 6:7) + (input 'clk' 7:8) + (input 'rst' 8:9) + (output 'port__o' 1.0:4) + (output 'port__oe' (cat 3.0 3.0 3.0 3.0)) + )) + (cell 1 1 (^ 2.0:4 4'd10)) + (cell 2 0 (flipflop 0.2:6 0 pos 0.7 0)) + (cell 3 0 (flipflop 0.6 0 pos 0.7 0)) + ) + """) + class DDRBufferTestCase(FHDLTestCase): def test_signature(self): diff --git a/tests/utils.py b/tests/utils.py index a069f18fb..5509b37fb 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,15 +16,57 @@ class FHDLTestCase(unittest.TestCase): + maxDiff = None + def assertRepr(self, obj, repr_str): if isinstance(obj, list): obj = Statement.cast(obj) - def prepare_repr(repr_str): + def squish_repr(repr_str): repr_str = re.sub(r"\s+", " ", repr_str) repr_str = re.sub(r"\( (?=\()", "(", repr_str) repr_str = re.sub(r"\) (?=\))", ")", repr_str) return repr_str.strip() - self.assertEqual(prepare_repr(repr(obj)), prepare_repr(repr_str)) + def format_repr(input_repr, *, indent=" "): + output_repr = [] + prefix = "\n" + name = None + index = 0 + stack = [] + current = "" + for char in input_repr: + if char == "(": + stack.append((prefix, name, index)) + name, index = None, 0 + output_repr.append(char) + if len(stack) == 1: + prefix += indent + output_repr.append(prefix) + elif char == ")": + indented = (len(stack) == 1 or name in ("module", "top")) + prefix, name, index = stack.pop() + if indented: + output_repr.append(prefix) + output_repr.append(char) + elif char == " ": + if name is None: + name = current + if name in ("module", "top"): + prefix += indent + else: + index += 1 + current = "" + if len(stack) == 1 or name == "module" and index >= 3 or name == "top": + output_repr.append(prefix) + else: + output_repr.append(char) + elif name is None: + current += char + output_repr.append(char) + else: + output_repr.append(char) + return "".join(output_repr) + # print("\n" + format_repr(squish_repr(repr(obj)))) + self.assertEqual(format_repr(squish_repr(repr(obj))), format_repr(squish_repr(repr_str))) def assertFormal(self, spec, ports=None, mode="bmc", depth=1): stack = traceback.extract_stack()