Skip to content

Commit 7851094

Browse files
committed
Implement RFC 69: Add a lib.io.PortLike object usable in simulation.
1 parent cde68fb commit 7851094

File tree

5 files changed

+753
-74
lines changed

5 files changed

+753
-74
lines changed

amaranth/lib/io.py

Lines changed: 222 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import enum
22
import operator
3+
import warnings
34
from abc import ABCMeta, abstractmethod
45
from collections.abc import Iterable
56

@@ -11,7 +12,7 @@
1112

1213

1314
__all__ = [
14-
"Direction", "PortLike", "SingleEndedPort", "DifferentialPort",
15+
"Direction", "PortLike", "SingleEndedPort", "DifferentialPort", "SimulationPort",
1516
"Buffer", "FFBuffer", "DDRBuffer",
1617
"Pin",
1718
]
@@ -57,6 +58,12 @@ class PortLike(metaclass=ABCMeta):
5758
:class:`amaranth.hdl.IOPort` is not an instance of :class:`amaranth.lib.io.PortLike`.
5859
"""
5960

61+
# TODO(amaranth-0.6): remove
62+
def __init_subclass__(cls):
63+
if cls.__add__ is PortLike.__add__:
64+
warnings.warn(f"{cls.__module__}.{cls.__qualname__} must override the `__add__` method",
65+
DeprecationWarning, stacklevel=2)
66+
6067
@property
6168
@abstractmethod
6269
def direction(self):
@@ -108,6 +115,32 @@ def __invert__(self):
108115
"""
109116
raise NotImplementedError # :nocov:
110117

118+
# TODO(amaranth-0.6): make abstract
119+
# @abstractmethod
120+
def __add__(self, other):
121+
"""Concatenates two library I/O ports of the same type.
122+
123+
The direction of the resulting port is:
124+
125+
* The same as the direction of both, if the two ports have the same direction.
126+
* :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
127+
* :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
128+
129+
Returns
130+
-------
131+
:py:`type(self)`
132+
A new :py:`type(self)` which contains wires from :py:`self` followed by wires
133+
from :py:`other`, preserving their polarity inversion.
134+
135+
Raises
136+
------
137+
:exc:`ValueError`
138+
If an input port is concatenated with an output port.
139+
:exc:`TypeError`
140+
If :py:`self` and :py:`other` have different types.
141+
"""
142+
raise NotImplementedError # :nocov:
143+
111144

112145
class SingleEndedPort(PortLike):
113146
"""Represents a single-ended library I/O port.
@@ -124,9 +157,9 @@ class SingleEndedPort(PortLike):
124157
same length as the width of :py:`io`, and the inversion is specified for individual wires.
125158
direction : :class:`Direction` or :class:`str`
126159
Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
127-
If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
128-
with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
129-
used with buffers of any direction.
160+
If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
161+
used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
162+
can be used with buffers of any direction.
130163
131164
Attributes
132165
----------
@@ -176,27 +209,6 @@ def __getitem__(self, index):
176209
direction=self._direction)
177210

178211
def __add__(self, other):
179-
"""Concatenates two single-ended library I/O ports.
180-
181-
The direction of the resulting port is:
182-
183-
* The same as the direction of both, if the two ports have the same direction.
184-
* :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
185-
* :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
186-
187-
Returns
188-
-------
189-
:class:`SingleEndedPort`
190-
A new :class:`SingleEndedPort` which contains wires from :py:`self` followed by wires
191-
from :py:`other`, preserving their polarity inversion.
192-
193-
Raises
194-
------
195-
:exc:`ValueError`
196-
If an input port is concatenated with an output port.
197-
:exc:`TypeError`
198-
If :py:`self` and :py:`other` have incompatible types.
199-
"""
200212
if not isinstance(other, SingleEndedPort):
201213
return NotImplemented
202214
return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert,
@@ -231,9 +243,9 @@ class DifferentialPort(PortLike):
231243
individual wires.
232244
direction : :class:`Direction` or :class:`str`
233245
Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
234-
If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
235-
with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
236-
used with buffers of any direction.
246+
If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
247+
used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
248+
can be used with buffers of any direction.
237249
238250
Attributes
239251
----------
@@ -293,27 +305,6 @@ def __getitem__(self, index):
293305
direction=self._direction)
294306

295307
def __add__(self, other):
296-
"""Concatenates two differential library I/O ports.
297-
298-
The direction of the resulting port is:
299-
300-
* The same as the direction of both, if the two ports have the same direction.
301-
* :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
302-
* :attr:`Direction.Output` if a bidirectional port is concatenated with an output port.
303-
304-
Returns
305-
-------
306-
:class:`DifferentialPort`
307-
A new :class:`DifferentialPort` which contains pairs of wires from :py:`self` followed
308-
by pairs of wires from :py:`other`, preserving their polarity inversion.
309-
310-
Raises
311-
------
312-
:exc:`ValueError`
313-
If an input port is concatenated with an output port.
314-
:exc:`TypeError`
315-
If :py:`self` and :py:`other` have incompatible types.
316-
"""
317308
if not isinstance(other, DifferentialPort):
318309
return NotImplemented
319310
return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n),
@@ -331,6 +322,167 @@ def __repr__(self):
331322
f"direction={self._direction})")
332323

333324

325+
class SimulationPort(PortLike):
326+
"""Represents a simulation library I/O port.
327+
328+
Implements the :class:`PortLike` interface.
329+
330+
Parameters
331+
----------
332+
direction : :class:`Direction` or :class:`str`
333+
Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
334+
If equal to :attr:`~Direction.Input` or :attr:`~Direction.Output`, this port can only be
335+
used with buffers of matching direction. If equal to :attr:`~Direction.Bidir`, this port
336+
can be used with buffers of any direction.
337+
width : :class:`int`
338+
Width of the port. The width of each of the attributes :py:`i`, :py:`o`, :py:`oe` (whenever
339+
present) equals :py:`width`.
340+
invert : :class:`bool` or iterable of :class:`bool`
341+
Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for
342+
the entire port. If the value is an iterable of :class:`bool`, the iterable must have the
343+
same length as the width of :py:`p` and :py:`n`, and the inversion is specified for
344+
individual wires.
345+
name : :class:`str` or :py:`None`
346+
Name of the port. This name is only used to derive the names of the input, output, and
347+
output enable signals.
348+
src_loc_at : :class:`int`
349+
:ref:`Source location <lang-srcloc>`. Used to infer :py:`name` if not specified.
350+
351+
Attributes
352+
----------
353+
i : :class:`Signal`
354+
Input signal. Present if :py:`direction in (Input, Bidir)`.
355+
o : :class:`Signal`
356+
Ouptut signal. Present if :py:`direction in (Output, Bidir)`.
357+
oe : :class:`Signal`
358+
Output enable signal. Present if :py:`direction in (Output, Bidir)`.
359+
invert : :class:`tuple` of :class:`bool`
360+
The :py:`invert` parameter, normalized to specify polarity inversion per-wire.
361+
direction : :class:`Direction`
362+
The :py:`direction` parameter, normalized to the :class:`Direction` enumeration.
363+
"""
364+
def __init__(self, direction, width, *, invert=False, name=None, src_loc_at=0):
365+
if name is not None and not isinstance(name, str):
366+
raise TypeError(f"Name must be a string, not {name!r}")
367+
if name is None:
368+
name = tracer.get_var_name(depth=2 + src_loc_at, default="$port")
369+
370+
if not (isinstance(width, int) and width >= 0):
371+
raise TypeError(f"Width must be a non-negative integer, not {width!r}")
372+
373+
self._direction = Direction(direction)
374+
375+
self._i = self._o = self._oe = None
376+
if self._direction in (Direction.Input, Direction.Bidir):
377+
self._i = Signal(width, name=f"{name}__i")
378+
if self._direction in (Direction.Output, Direction.Bidir):
379+
self._o = Signal(width, name=f"{name}__o")
380+
self._oe = Signal(width, name=f"{name}__oe",
381+
init=~0 if self._direction is Direction.Output else 0)
382+
383+
if isinstance(invert, bool):
384+
self._invert = (invert,) * width
385+
elif isinstance(invert, Iterable):
386+
self._invert = tuple(invert)
387+
if len(self._invert) != width:
388+
raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match "
389+
f"port width ({width})")
390+
if not all(isinstance(item, bool) for item in self._invert):
391+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
392+
else:
393+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
394+
395+
@property
396+
def i(self):
397+
if self._i is None:
398+
raise AttributeError(
399+
"Simulation port with output direction does not have an input signal")
400+
return self._i
401+
402+
@property
403+
def o(self):
404+
if self._o is None:
405+
raise AttributeError(
406+
"Simulation port with input direction does not have an output signal")
407+
return self._o
408+
409+
@property
410+
def oe(self):
411+
if self._oe is None:
412+
raise AttributeError(
413+
"Simulation port with input direction does not have an output enable signal")
414+
return self._oe
415+
416+
@property
417+
def invert(self):
418+
return self._invert
419+
420+
@property
421+
def direction(self):
422+
return self._direction
423+
424+
def __len__(self):
425+
if self._direction is Direction.Input:
426+
return len(self._i)
427+
if self._direction is Direction.Output:
428+
assert len(self._o) == len(self._oe)
429+
return len(self._o)
430+
if self._direction is Direction.Bidir:
431+
assert len(self._i) == len(self._o) == len(self._oe)
432+
return len(self._i)
433+
assert False # :nocov:
434+
435+
def __getitem__(self, key):
436+
result = object.__new__(type(self))
437+
result._i = None if self._i is None else self._i [key]
438+
result._o = None if self._o is None else self._o [key]
439+
result._oe = None if self._oe is None else self._oe[key]
440+
if isinstance(key, slice):
441+
result._invert = self._invert[key]
442+
else:
443+
result._invert = (self._invert[key],)
444+
result._direction = self._direction
445+
return result
446+
447+
def __invert__(self):
448+
result = object.__new__(type(self))
449+
result._i = self._i
450+
result._o = self._o
451+
result._oe = self._oe
452+
result._invert = tuple(not invert for invert in self._invert)
453+
result._direction = self._direction
454+
return result
455+
456+
def __add__(self, other):
457+
if not isinstance(other, SimulationPort):
458+
return NotImplemented
459+
direction = self._direction & other._direction
460+
result = object.__new__(type(self))
461+
result._i = None if direction is Direction.Output else Cat(self._i, other._i)
462+
result._o = None if direction is Direction.Input else Cat(self._o, other._o)
463+
result._oe = None if direction is Direction.Input else Cat(self._oe, other._oe)
464+
result._invert = self._invert + other._invert
465+
result._direction = direction
466+
return result
467+
468+
def __repr__(self):
469+
parts = []
470+
if self._i is not None:
471+
parts.append(f"i={self._i!r}")
472+
if self._o is not None:
473+
parts.append(f"o={self._o!r}")
474+
if self._oe is not None:
475+
parts.append(f"oe={self._oe!r}")
476+
if not any(self._invert):
477+
invert = False
478+
elif all(self._invert):
479+
invert = True
480+
else:
481+
invert = self._invert
482+
return (f"SimulationPort({', '.join(parts)}, invert={invert!r}, "
483+
f"direction={self._direction})")
484+
485+
334486
class Buffer(wiring.Component):
335487
"""A combinational I/O buffer component.
336488
@@ -476,6 +628,18 @@ def elaborate(self, platform):
476628
else:
477629
m.submodules += IOBufferInstance(self._port.p, o=o_inv, oe=self.oe, i=i_inv)
478630
m.submodules += IOBufferInstance(self._port.n, o=~o_inv, oe=self.oe)
631+
elif isinstance(self._port, SimulationPort):
632+
if self.direction is Direction.Bidir:
633+
# Loop back `o` if `oe` is asserted. This frees the test harness from having to
634+
# provide this functionality itself.
635+
for i_inv_bit, oe_bit, o_bit, i_bit in \
636+
zip(i_inv, self._port.oe, self._port.o, self._port.i):
637+
m.d.comb += i_inv_bit.eq(Cat(Mux(oe_bit, o_bit, i_bit)))
638+
if self.direction is Direction.Input:
639+
m.d.comb += i_inv.eq(self._port.i)
640+
if self.direction in (Direction.Output, Direction.Bidir):
641+
m.d.comb += self._port.o.eq(o_inv)
642+
m.d.comb += self._port.oe.eq(self.oe.replicate(len(self._port)))
479643
else:
480644
raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") # :nocov:
481645

@@ -719,6 +883,12 @@ class DDRBuffer(wiring.Component):
719883
720884
This limitation may be lifted in the future.
721885
886+
.. warning::
887+
888+
Double data rate I/O buffers are not compatible with :class:`SimulationPort`.
889+
890+
This limitation may be lifted in the future.
891+
722892
Parameters
723893
----------
724894
direction : :class:`Direction`
@@ -826,6 +996,9 @@ def elaborate(self, platform):
826996
if hasattr(platform, "get_io_buffer"):
827997
return platform.get_io_buffer(self)
828998

999+
if isinstance(self._port, SimulationPort):
1000+
raise NotImplementedError(f"DDR buffers are not supported in simulation") # :nocov:
1001+
8291002
raise NotImplementedError(f"DDR buffers are not supported on {platform!r}") # :nocov:
8301003

8311004

0 commit comments

Comments
 (0)