Skip to content

Commit 598cf8d

Browse files
wanda-phiwhitequark
authored andcommitted
lib.io: Implement *Port from RFC 55.
1 parent 7445760 commit 598cf8d

File tree

4 files changed

+421
-26
lines changed

4 files changed

+421
-26
lines changed

amaranth/build/res.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,6 @@ class ResourceError(Exception):
1414
pass
1515

1616

17-
class SingleEndedPort:
18-
def __init__(self, io):
19-
self.io = io
20-
21-
22-
class DifferentialPort:
23-
def __init__(self, p, n):
24-
self.p = p
25-
self.n = n
26-
27-
2817
class PortGroup:
2918
pass
3019

@@ -138,26 +127,23 @@ def resolve(resource, dir, xdr, path, attrs):
138127

139128
elif isinstance(resource.ios[0], (Pins, DiffPairs)):
140129
phys = resource.ios[0]
141-
# The flow is `In` below regardless of requested pin direction. The flow should
142-
# never be used as it's not used internally and anyone using `dir="-"` should
143-
# ignore it as well.
130+
if phys.dir == "oe":
131+
direction = "o"
132+
else:
133+
direction = phys.dir
144134
if isinstance(phys, Pins):
145135
phys_names = phys.names
146136
io = IOPort(len(phys), name="__".join(path) + "__io")
147-
port = SingleEndedPort(io)
137+
port = SingleEndedPort(io, invert=phys.invert, direction=direction)
148138
if isinstance(phys, DiffPairs):
149139
phys_names = []
140+
p = IOPort(len(phys), name="__".join(path) + "__p")
141+
n = IOPort(len(phys), name="__".join(path) + "__n")
150142
if not self.should_skip_port_component(None, attrs, "p"):
151-
p = IOPort(len(phys), name="__".join(path) + "__p")
152143
phys_names += phys.p.names
153-
else:
154-
p = None
155144
if not self.should_skip_port_component(None, attrs, "n"):
156-
n = IOPort(len(phys), name="__".join(path) + "__n")
157145
phys_names += phys.n.names
158-
else:
159-
n = None
160-
port = DifferentialPort(p, n)
146+
port = DifferentialPort(p, n, invert=phys.invert, direction=direction)
161147
if dir == "-":
162148
pin = None
163149
else:

amaranth/lib/io.py

Lines changed: 248 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,256 @@
1-
from .. import *
1+
import enum
2+
from collections.abc import Iterable
3+
4+
from ..hdl import *
25
from ..lib import wiring
36
from ..lib.wiring import In, Out
47
from .. import tracer
58

69

7-
__all__ = ["Pin"]
10+
__all__ = ["Direction", "SingleEndedPort", "DifferentialPort", "Pin"]
11+
12+
13+
class Direction(enum.Enum):
14+
"""Represents a direction of an I/O port, or of an I/O buffer."""
15+
16+
#: Input direction (from world to Amaranth design)
17+
Input = "i"
18+
#: Output direction (from Amaranth design to world)
19+
Output = "o"
20+
#: Bidirectional (can be switched between input and output)
21+
Bidir = "io"
22+
23+
def __or__(self, other):
24+
if not isinstance(other, Direction):
25+
return NotImplemented
26+
if self == other:
27+
return self
28+
else:
29+
return Direction.Bidir
30+
31+
def __and__(self, other):
32+
if not isinstance(other, Direction):
33+
return NotImplemented
34+
if self == other:
35+
return self
36+
elif self is Direction.Bidir:
37+
return other
38+
elif other is Direction.Bidir:
39+
return self
40+
else:
41+
raise ValueError("Cannot combine input port with output port")
42+
43+
44+
class SingleEndedPort:
45+
"""Represents a single-ended I/O port with optional inversion.
46+
47+
Parameters
48+
----------
49+
io : IOValue
50+
The raw I/O value being wrapped.
51+
invert : bool or iterable of bool
52+
If true, the electrical state of the physical pin will be opposite from the Amaranth value
53+
(the ``*Buffer`` classes will insert inverters on ``o`` and ``i`` pins, as appropriate).
54+
55+
This can be used for various purposes:
56+
57+
- Normalizing active-low pins (such as ``CS_B``) to be active-high in Amaranth code
58+
- Compensating for boards where an inverting level-shifter (or similar circuitry) was used
59+
on the pin
60+
61+
If the value is a simple :class:`bool`, it is used for all bits of this port. If the value
62+
is an iterable of :class:`bool`, the iterable must have the same length as ``io``, and
63+
the inversion is specified per-bit.
64+
direction : Direction or str
65+
Represents the allowed directions of this port. If equal to :attr:`Direction.Input` or
66+
:attr:`Direction.Output`, this port can only be used with buffers of matching direction.
67+
If equal to :attr:`Direction.Bidir`, this port can be used with buffers of any direction.
68+
If a string is passed, it is cast to :class:`Direction`.
69+
"""
70+
def __init__(self, io, *, invert=False, direction=Direction.Bidir):
71+
self._io = IOValue.cast(io)
72+
if isinstance(invert, bool):
73+
self._invert = (invert,) * len(self._io)
74+
elif isinstance(invert, Iterable):
75+
self._invert = tuple(invert)
76+
if len(self._invert) != len(self._io):
77+
raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match "
78+
f"length of 'io' ({len(self._io)})")
79+
if not all(isinstance(item, bool) for item in self._invert):
80+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
81+
else:
82+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
83+
self._direction = Direction(direction)
84+
85+
@property
86+
def io(self):
87+
"""The ``io`` argument passed to the constructor."""
88+
return self._io
89+
90+
@property
91+
def invert(self):
92+
"""The ``invert`` argument passed to the constructor, normalized to a :class:`tuple`
93+
of :class:`bool`."""
94+
return self._invert
95+
96+
@property
97+
def direction(self):
98+
"""The ``direction`` argument passed to the constructor, normalized to :class:`Direction`."""
99+
return self._direction
100+
101+
def __len__(self):
102+
"""Returns the width of this port in bits. Equal to :py:`len(self.io)`."""
103+
return len(self._io)
104+
105+
def __invert__(self):
106+
"""Returns a new :class:`SingleEndedPort` with the opposite value of ``invert``."""
107+
return SingleEndedPort(self._io, invert=tuple(not inv for inv in self._invert),
108+
direction=self._direction)
109+
110+
def __getitem__(self, index):
111+
"""Slices the port, returning another :class:`SingleEndedPort` with a subset
112+
of its bits.
113+
114+
The index can be a :class:`slice` or :class:`int`. If the index is
115+
an :class:`int`, the result is a single-bit :class:`SingleEndedPort`."""
116+
return SingleEndedPort(self._io[index], invert=self._invert[index],
117+
direction=self._direction)
118+
119+
def __add__(self, other):
120+
"""Concatenates two :class:`SingleEndedPort` objects together, returning a new
121+
:class:`SingleEndedPort` object.
122+
123+
When the concatenated ports have different directions, the conflict is resolved as follows:
124+
125+
- If a bidirectional port is concatenated with an input port, the result is an input port.
126+
- If a bidirectional port is concatenated with an output port, the result is an output port.
127+
- If an input port is concatenated with an output port, :exc:`ValueError` is raised.
128+
"""
129+
if not isinstance(other, SingleEndedPort):
130+
return NotImplemented
131+
return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert,
132+
direction=self._direction | other._direction)
133+
134+
def __repr__(self):
135+
if all(self._invert):
136+
invert = True
137+
elif not any(self._invert):
138+
invert = False
139+
else:
140+
invert = self._invert
141+
return f"SingleEndedPort({self._io!r}, invert={invert!r}, direction={self._direction})"
142+
143+
144+
class DifferentialPort:
145+
"""Represents a differential I/O port with optional inversion.
146+
147+
Parameters
148+
----------
149+
p : IOValue
150+
The raw I/O value used as positive (true) half of the port.
151+
n : IOValue
152+
The raw I/O value used as negative (complemented) half of the port. Must have the same
153+
length as ``p``.
154+
invert : bool or iterable of bool
155+
If true, the electrical state of the physical pin will be opposite from the Amaranth value
156+
(the ``*Buffer`` classes will insert inverters on ``o`` and ``i`` pins, as appropriate).
157+
158+
This can be used for various purposes:
159+
160+
- Normalizing active-low pins (such as ``CS_B``) to be active-high in Amaranth code
161+
- Compensating for boards where the P and N pins are swapped (e.g. for easier routing)
162+
163+
If the value is a simple :class:`bool`, it is used for all bits of this port. If the value
164+
is an iterable of :class:`bool`, the iterable must have the same length as ``io``, and
165+
the inversion is specified per-bit.
166+
direction : Direction or str
167+
Represents the allowed directions of this port. If equal to :attr:`Direction.Input` or
168+
:attr:`Direction.Output`, this port can only be used with buffers of matching direction.
169+
If equal to :attr:`Direction.Bidir`, this port can be used with buffers of any direction.
170+
If a string is passed, it is cast to :class:`Direction`.
171+
"""
172+
def __init__(self, p, n, *, invert=False, direction=Direction.Bidir):
173+
self._p = IOValue.cast(p)
174+
self._n = IOValue.cast(n)
175+
if len(self._p) != len(self._n):
176+
raise ValueError(f"Length of 'p' ({len(self._p)}) doesn't match length of 'n' "
177+
f"({len(self._n)})")
178+
if isinstance(invert, bool):
179+
self._invert = (invert,) * len(self._p)
180+
elif isinstance(invert, Iterable):
181+
self._invert = tuple(invert)
182+
if len(self._invert) != len(self._p):
183+
raise ValueError(f"Length of 'invert' ({len(self._invert)}) doesn't match "
184+
f"length of 'p' ({len(self._p)})")
185+
if not all(isinstance(item, bool) for item in self._invert):
186+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
187+
else:
188+
raise TypeError(f"'invert' must be a bool or iterable of bool, not {invert!r}")
189+
self._direction = Direction(direction)
190+
191+
@property
192+
def p(self):
193+
"""The ``p`` argument passed to the constructor."""
194+
return self._p
195+
196+
@property
197+
def n(self):
198+
"""The ``n`` argument passed to the constructor."""
199+
return self._n
200+
201+
@property
202+
def invert(self):
203+
"""The ``invert`` argument passed to the constructor, normalized to a :class:`tuple`
204+
of :class:`bool`."""
205+
return self._invert
206+
207+
@property
208+
def direction(self):
209+
"""The ``direction`` argument passed to the constructor, normalized to :class:`Direction`."""
210+
return self._direction
211+
212+
def __len__(self):
213+
"""Returns the width of this port in bits. Equal to :py:`len(self.p)` (and :py:`len(self.n)`)."""
214+
return len(self._p)
215+
216+
def __invert__(self):
217+
"""Returns a new :class:`DifferentialPort` with the opposite value of ``invert``."""
218+
return DifferentialPort(self._p, self._n, invert=tuple(not inv for inv in self._invert),
219+
direction=self._direction)
220+
221+
def __getitem__(self, index):
222+
"""Slices the port, returning another :class:`DifferentialPort` with a subset
223+
of its bits.
224+
225+
The index can be a :class:`slice` or :class:`int`. If the index is
226+
an :class:`int`, the result is a single-bit :class:`DifferentialPort`."""
227+
return DifferentialPort(self._p[index], self._n[index], invert=self._invert[index],
228+
direction=self._direction)
229+
230+
def __add__(self, other):
231+
"""Concatenates two :class:`DifferentialPort` objects together, returning a new
232+
:class:`DifferentialPort` object.
233+
234+
When the concatenated ports have different directions, the conflict is resolved as follows:
235+
236+
- If a bidirectional port is concatenated with an input port, the result is an input port.
237+
- If a bidirectional port is concatenated with an output port, the result is an output port.
238+
- If an input port is concatenated with an output port, :exc:`ValueError` is raised.
239+
"""
240+
if not isinstance(other, DifferentialPort):
241+
return NotImplemented
242+
return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n),
243+
invert=self._invert + other._invert,
244+
direction=self._direction | other._direction)
245+
246+
def __repr__(self):
247+
if not any(self._invert):
248+
invert = False
249+
elif all(self._invert):
250+
invert = True
251+
else:
252+
invert = self._invert
253+
return f"DifferentialPort({self._p!r}, {self._n!r}, invert={invert!r}, direction={self._direction})"
8254

9255

10256
class Pin(wiring.PureInterface):

docs/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Implemented RFCs
5050
.. _RFC 46: https://amaranth-lang.org/rfcs/0046-shape-range-1.html
5151
.. _RFC 50: https://amaranth-lang.org/rfcs/0050-print.html
5252
.. _RFC 53: https://amaranth-lang.org/rfcs/0053-ioport.html
53+
.. _RFC 55: https://amaranth-lang.org/rfcs/0055-lib-io.html
5354

5455
* `RFC 17`_: Remove ``log2_int``
5556
* `RFC 27`_: Testbench processes for the simulator
@@ -93,6 +94,7 @@ Standard library changes
9394
.. currentmodule:: amaranth.lib
9495

9596
* Added: :mod:`amaranth.lib.memory`. (`RFC 45`_)
97+
* Added: :class:`amaranth.lib.io.SingleEndedPort`, :class:`amaranth.lib.io.DifferentialPort`. (`RFC 55`_)
9698
* Removed: (deprecated in 0.4) :mod:`amaranth.lib.scheduler`. (`RFC 19`_)
9799
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.FIFOInterface` with ``fwft=False``. (`RFC 20`_)
98100
* Removed: (deprecated in 0.4) :class:`amaranth.lib.fifo.SyncFIFO` with ``fwft=False``. (`RFC 20`_)

0 commit comments

Comments
 (0)