docs/stdlib/io: document I/O buffer library.

This commit is contained in:
Catherine 2024-04-10 21:41:06 +00:00
parent e2b47c1a9e
commit 324c37f9fa
6 changed files with 570 additions and 181 deletions

View file

@ -17,24 +17,22 @@ __all__ = [
class Direction(enum.Enum): class Direction(enum.Enum):
"""Represents a direction of an I/O port, or of an I/O buffer.""" """Represents direction of a library I/O port, or of an I/O buffer component."""
#: Input direction (from world to Amaranth design) #: Input direction (from outside world to Amaranth design).
Input = "i" Input = "i"
#: Output direction (from Amaranth design to world) #: Output direction (from Amaranth design to outside world).
Output = "o" Output = "o"
#: Bidirectional (can be switched between input and output) #: Bidirectional (can be switched between input and output).
Bidir = "io" Bidir = "io"
def __or__(self, other):
if not isinstance(other, Direction):
return NotImplemented
if self == other:
return self
else:
return Direction.Bidir
def __and__(self, other): def __and__(self, other):
"""Narrow the set of possible directions.
* :py:`self & self` returns :py:`self`.
* :py:`Bidir & other` returns :py:`other`.
* :py:`Input & Output` raises :exc:`ValueError`.
"""
if not isinstance(other, Direction): if not isinstance(other, Direction):
return NotImplemented return NotImplemented
if self == other: if self == other:
@ -48,66 +46,95 @@ class Direction(enum.Enum):
class PortLike(metaclass=ABCMeta): class PortLike(metaclass=ABCMeta):
"""Represents an abstract port that can be passed to a buffer. """Represents an abstract library I/O port that can be passed to a buffer.
The port types supported by most platforms are :class:`SingleEndedPort` and The port types supported by most platforms are :class:`SingleEndedPort` and
:class:`DifferentialPort`. Platforms may define additional custom port types as appropriate. :class:`DifferentialPort`. Platforms may define additional port types where appropriate.
.. note::
:class:`amaranth.hdl.IOPort` is not an instance of :class:`amaranth.lib.io.PortLike`.
""" """
@property @property
@abstractmethod @abstractmethod
def direction(self): def direction(self):
"""The direction of this port, as :class:`Direction`.""" """Direction of the port.
Returns
-------
:class:`Direction`
"""
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
@abstractmethod @abstractmethod
def __len__(self): def __len__(self):
"""Returns the width of this port in bits.""" """Computes the width of the port.
Returns
-------
:class:`int`
The number of wires (for single-ended library I/O ports) or wire pairs (for differential
library I/O ports) this port consists of.
"""
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
@abstractmethod @abstractmethod
def __getitem__(self, index): def __getitem__(self, key):
"""Slices the port, returning another :class:`PortLike` with a subset """Slices the port.
of its bits.
The index can be a :class:`slice` or :class:`int`. If the index is Returns
an :class:`int`, the result is a single-bit :class:`PortLike`.""" -------
:class:`PortLike`
A new :class:`PortLike` instance of the same type as :py:`self`, containing a selection
of wires of this port according to :py:`key`. Its width is the same as the length of
the slice (if :py:`key` is a :class:`slice`); or 1 (if :py:`key` is an :class:`int`).
"""
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
@abstractmethod @abstractmethod
def __invert__(self): def __invert__(self):
"""Returns a new :class:`PortLike` object like this one, but with inverted polarity. """Inverts polarity of the port.
The result should be such that using :class:`Buffer` on it is equivalent to using Inverting polarity of a library I/O port has the same effect as adding inverters to
:class:`Buffer` on the original, with added inverters on the :py:`i` and :py:`o` ports.""" the :py:`i` and :py:`o` members of an I/O buffer component for that port.
Returns
-------
:class:`PortLike`
A new :class:`PortLike` instance of the same type as :py:`self`, containing the same
wires as this port, but with polarity inverted.
"""
raise NotImplementedError # :nocov: raise NotImplementedError # :nocov:
class SingleEndedPort(PortLike): class SingleEndedPort(PortLike):
"""Represents a single-ended I/O port with optional inversion. """Represents a single-ended library I/O port.
Implements the :class:`PortLike` interface.
Parameters Parameters
---------- ----------
io : :class:`IOValue` io : :class:`IOValue`
The raw I/O value being wrapped. Underlying core I/O value.
invert : :class:`bool` or iterable of :class:`bool` invert : :class:`bool` or iterable of :class:`bool`
If true, the electrical state of the physical pin will be opposite from the Amaranth value Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for
(the ``*Buffer`` classes will insert inverters on :py:`o` and :py:`i` pins, as appropriate). the entire port. If the value is an iterable of :class:`bool`, the iterable must have the
same length as the width of :py:`io`, and the inversion is specified for individual wires.
This can be used for various purposes:
- Normalizing active-low pins (such as ``CS_B``) to be active-high in Amaranth code
- Compensating for boards where an inverting level-shifter (or similar circuitry) was used
on the pin
If the value is a simple :class:`bool`, it is used for all bits of this port. If the value
is an iterable of :class:`bool`, the iterable must have the same length as :py:`io`, and
the inversion is specified per-bit.
direction : :class:`Direction` or :class:`str` direction : :class:`Direction` or :class:`str`
Represents the allowed directions of this port. If equal to :attr:`Direction.Input` or Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
:attr:`Direction.Output`, this port can only be used with buffers of matching direction. If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
If equal to :attr:`Direction.Bidir`, this port can be used with buffers of any direction. with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
If a string is passed, it is cast to :class:`Direction`. used with buffers of any direction.
Attributes
----------
io : :class:`IOValue`
The :py:`io` parameter.
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, io, *, invert=False, direction=Direction.Bidir): def __init__(self, io, *, invert=False, direction=Direction.Bidir):
self._io = IOValue.cast(io) self._io = IOValue.cast(io)
@ -126,52 +153,53 @@ class SingleEndedPort(PortLike):
@property @property
def io(self): def io(self):
"""The :py:`io` argument passed to the constructor."""
return self._io return self._io
@property @property
def invert(self): def invert(self):
"""The :py:`invert` argument passed to the constructor, normalized to a :class:`tuple`
of :class:`bool`."""
return self._invert return self._invert
@property @property
def direction(self): def direction(self):
"""The :py:`direction` argument passed to the constructor, normalized to :class:`Direction`."""
return self._direction return self._direction
def __len__(self): def __len__(self):
"""Returns the width of this port in bits. Equal to :py:`len(self.io)`."""
return len(self._io) return len(self._io)
def __invert__(self): def __invert__(self):
"""Returns a new :class:`SingleEndedPort` with the opposite value of :py:`invert`."""
return SingleEndedPort(self._io, invert=tuple(not inv for inv in self._invert), return SingleEndedPort(self._io, invert=tuple(not inv for inv in self._invert),
direction=self._direction) direction=self._direction)
def __getitem__(self, index): def __getitem__(self, index):
"""Slices the port, returning another :class:`SingleEndedPort` with a subset
of its bits.
The index can be a :class:`slice` or :class:`int`. If the index is
an :class:`int`, the result is a single-bit :class:`SingleEndedPort`."""
return SingleEndedPort(self._io[index], invert=self._invert[index], return SingleEndedPort(self._io[index], invert=self._invert[index],
direction=self._direction) direction=self._direction)
def __add__(self, other): def __add__(self, other):
"""Concatenates two :class:`SingleEndedPort` objects together, returning a new """Concatenates two single-ended library I/O ports.
:class:`SingleEndedPort` object.
When the concatenated ports have different directions, the conflict is resolved as follows: The direction of the resulting port is:
- If a bidirectional port is concatenated with an input port, the result is an input port. * The same as the direction of both, if the two ports have the same direction.
- If a bidirectional port is concatenated with an output port, the result is an output port. * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
- If an input port is concatenated with an output port, :exc:`ValueError` is raised. * :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): if not isinstance(other, SingleEndedPort):
return NotImplemented return NotImplemented
return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert, return SingleEndedPort(Cat(self._io, other._io), invert=self._invert + other._invert,
direction=self._direction | other._direction) direction=self._direction & other._direction)
def __repr__(self): def __repr__(self):
if all(self._invert): if all(self._invert):
@ -184,32 +212,38 @@ class SingleEndedPort(PortLike):
class DifferentialPort(PortLike): class DifferentialPort(PortLike):
"""Represents a differential I/O port with optional inversion. """Represents a differential library I/O port.
Implements the :class:`PortLike` interface.
Parameters Parameters
---------- ----------
p : :class:`IOValue` p : :class:`IOValue`
The raw I/O value used as positive (true) half of the port. Underlying core I/O value for the true (positive) half of the port.
n : :class:`IOValue` n : :class:`IOValue`
The raw I/O value used as negative (complemented) half of the port. Must have the same Underlying core I/O value for the complement (negative) half of the port.
length as :py:`p`. Must have the same width as :py:`p`.
invert : :class:`bool` or iterable of :class`bool` invert : :class:`bool` or iterable of :class:`bool`
If true, the electrical state of the physical pin will be opposite from the Amaranth value Polarity inversion. If the value is a simple :class:`bool`, it specifies inversion for
(the ``*Buffer`` classes will insert inverters on :py:`o` and :py:`i` pins, as appropriate). 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
This can be used for various purposes: individual wires.
- Normalizing active-low pins (such as ``CS_B``) to be active-high in Amaranth code
- Compensating for boards where the P and N pins are swapped (e.g. for easier routing)
If the value is a simple :class:`bool`, it is used for all bits of this port. If the value
is an iterable of :class:`bool`, the iterable must have the same length as :py:`io`, and
the inversion is specified per-bit.
direction : :class:`Direction` or :class:`str` direction : :class:`Direction` or :class:`str`
Represents the allowed directions of this port. If equal to :attr:`Direction.Input` or Set of allowed buffer directions. A string is converted to a :class:`Direction` first.
:attr:`Direction.Output`, this port can only be used with buffers of matching direction. If equal to :attr:`Direction.Input` or :attr:`Direction.Output`, this port can only be used
If equal to :attr:`Direction.Bidir`, this port can be used with buffers of any direction. with buffers of matching direction. If equal to :attr:`Direction.Bidir`, this port can be
If a string is passed, it is cast to :class:`Direction`. used with buffers of any direction.
Attributes
----------
p : :class:`IOValue`
The :py:`p` parameter.
n : :class:`IOValue`
The :py:`n` parameter.
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, p, n, *, invert=False, direction=Direction.Bidir): def __init__(self, p, n, *, invert=False, direction=Direction.Bidir):
self._p = IOValue.cast(p) self._p = IOValue.cast(p)
@ -232,58 +266,58 @@ class DifferentialPort(PortLike):
@property @property
def p(self): def p(self):
"""The :py:`p` argument passed to the constructor."""
return self._p return self._p
@property @property
def n(self): def n(self):
"""The :py:`n` argument passed to the constructor."""
return self._n return self._n
@property @property
def invert(self): def invert(self):
"""The :py:`invert` argument passed to the constructor, normalized to a :class:`tuple`
of :class:`bool`."""
return self._invert return self._invert
@property @property
def direction(self): def direction(self):
"""The :py:`direction` argument passed to the constructor, normalized to :class:`Direction`."""
return self._direction return self._direction
def __len__(self): def __len__(self):
"""Returns the width of this port in bits. Equal to :py:`len(self.p)` (and :py:`len(self.n)`)."""
return len(self._p) return len(self._p)
def __invert__(self): def __invert__(self):
"""Returns a new :class:`DifferentialPort` with the opposite value of :py:`invert`."""
return DifferentialPort(self._p, self._n, invert=tuple(not inv for inv in self._invert), return DifferentialPort(self._p, self._n, invert=tuple(not inv for inv in self._invert),
direction=self._direction) direction=self._direction)
def __getitem__(self, index): def __getitem__(self, index):
"""Slices the port, returning another :class:`DifferentialPort` with a subset
of its bits.
The index can be a :class:`slice` or :class:`int`. If the index is
an :class:`int`, the result is a single-bit :class:`DifferentialPort`."""
return DifferentialPort(self._p[index], self._n[index], invert=self._invert[index], return DifferentialPort(self._p[index], self._n[index], invert=self._invert[index],
direction=self._direction) direction=self._direction)
def __add__(self, other): def __add__(self, other):
"""Concatenates two :class:`DifferentialPort` objects together, returning a new """Concatenates two differential library I/O ports.
:class:`DifferentialPort` object.
When the concatenated ports have different directions, the conflict is resolved as follows: The direction of the resulting port is:
- If a bidirectional port is concatenated with an input port, the result is an input port. * The same as the direction of both, if the two ports have the same direction.
- If a bidirectional port is concatenated with an output port, the result is an output port. * :attr:`Direction.Input` if a bidirectional port is concatenated with an input port.
- If an input port is concatenated with an output port, :exc:`ValueError` is raised. * :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): if not isinstance(other, DifferentialPort):
return NotImplemented return NotImplemented
return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n), return DifferentialPort(Cat(self._p, other._p), Cat(self._n, other._n),
invert=self._invert + other._invert, invert=self._invert + other._invert,
direction=self._direction | other._direction) direction=self._direction & other._direction)
def __repr__(self): def __repr__(self):
if not any(self._invert): if not any(self._invert):
@ -292,36 +326,74 @@ class DifferentialPort(PortLike):
invert = True invert = True
else: else:
invert = self._invert invert = self._invert
return f"DifferentialPort({self._p!r}, {self._n!r}, invert={invert!r}, direction={self._direction})" return (f"DifferentialPort({self._p!r}, {self._n!r}, invert={invert!r}, "
f"direction={self._direction})")
class Buffer(wiring.Component): class Buffer(wiring.Component):
"""A combinational I/O buffer. """A combinational I/O buffer component.
This buffer can be used on any platform; if the platform does not specialize its implementation,
an :ref:`I/O buffer instance <lang-iobufferinstance>` is used.
The following diagram defines the timing relationship between the underlying core I/O value
(for differential ports, the core I/O value of the true half) and the :py:`i`, :py:`o`, and
:py:`oe` members:
.. wavedrom:: io/buffer
{
"signal": [
{"name": "clk", "wave": "p....."},
{"name": "o", "wave": "x345x.", "data": ["A", "B", "C"]},
{"name": "oe", "wave": "01..0."},
{},
{"name": "port", "wave": "z345z.", "data": ["A", "B", "C"]},
{},
{"name": "i", "wave": "x345x.", "data": ["A", "B", "C"]}
],
"config": {
"hscale": 2
}
}
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
port : :class:`PortLike` port : :class:`PortLike`
Port driven by the buffer.
Raises
------
:exc:`ValueError`
Unless :py:`port.direction in (direction, Bidir)`.
Attributes Attributes
---------- ----------
signature : :class:`Buffer.Signature` signature : :class:`Buffer.Signature`
Created based on constructor arguments. :py:`Signature(direction, len(port)).flip()`.
""" """
class Signature(wiring.Signature): class Signature(wiring.Signature):
"""A signature of a combinational I/O buffer. """Signature of a combinational I/O buffer.
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
width : :class:`int` width : :class:`int`
Width of the buffer.
Attributes Members
---------- -------
i: :py:`unsigned(width)` (if :py:`direction in (Direction.Input, Direction.Bidir)`) i: :py:`In(width)`
o: :py:`unsigned(width)` (if :py:`direction in (Direction.Output, Direction.Bidir)`) Present if :py:`direction in (Input, Bidir)`.
oe: :py:`unsigned(1, init=0)` (if :py:`direction is Direction.Bidir`) o: :py:`Out(width)`
oe: :py:`unsigned(1, init=1)` (if :py:`direction is Direction.Output`) Present if :py:`direction in (Output, Bidir)`.
oe: :py:`Out(1, init=0)`
Present if :py:`direction is Bidir`.
oe: :py:`Out(1, init=1)`
Present if :py:`direction is Output`.
""" """
def __init__(self, direction, width): def __init__(self, direction, width):
self._direction = Direction(direction) self._direction = Direction(direction)
@ -343,7 +415,8 @@ class Buffer(wiring.Component):
return self._width return self._width
def __eq__(self, other): def __eq__(self, other):
return type(self) is type(other) and self.direction == other.direction and self.width == other.width return (type(self) is type(other) and self.direction == other.direction and
self.width == other.width)
def __repr__(self): def __repr__(self):
return f"Buffer.Signature({self.direction}, {self.width})" return f"Buffer.Signature({self.direction}, {self.width})"
@ -403,47 +476,84 @@ class Buffer(wiring.Component):
m.submodules += IOBufferInstance(self._port.p, o=o_inv, oe=self.oe, i=i_inv) 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) m.submodules += IOBufferInstance(self._port.n, o=~o_inv, oe=self.oe)
else: else:
raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") raise TypeError("Cannot elaborate generic 'Buffer' with port {self._port!r}") # :nocov:
return m return m
class FFBuffer(wiring.Component): class FFBuffer(wiring.Component):
"""A registered I/O buffer. """A registered I/O buffer component.
Equivalent to a plain :class:`Buffer` combined with reset-less registers on :py:`i`, :py:`o`, This buffer can be used on any platform; if the platform does not specialize its implementation,
:py:`oe`. an :ref:`I/O buffer instance <lang-iobufferinstance>` is used, combined with reset-less
registers on :py:`i`, :py:`o`, and :py:`oe` members.
The following diagram defines the timing relationship between the underlying core I/O value
(for differential ports, the core I/O value of the true half) and the :py:`i`, :py:`o`, and
:py:`oe` members:
.. wavedrom:: io/ff-buffer
{
"signal": [
{"name": "clk", "wave": "p......"},
{"name": "o", "wave": "x345x..", "data": ["A", "B", "C"]},
{"name": "oe", "wave": "01..0.."},
{},
{"name": "port", "wave": "z.345z.", "data": ["A", "B", "C"]},
{},
{"name": "i", "wave": "x..345x", "data": ["A", "B", "C"]}
],
"config": {
"hscale": 2
}
}
.. warning::
On some platforms, this buffer can only be used with rising edge clock domains, and will
raise an exception during conversion of the design to a netlist otherwise.
This limitation will be lifted in the future.
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
port : :class:`PortLike` port : :class:`PortLike`
Port driven by the buffer.
i_domain : :class:`str` i_domain : :class:`str`
Domain for input register. Only used when :py:`direction in (Direction.Input, Direction.Bidir)`. Name of the input register's clock domain. Used when :py:`direction in (Input, Bidir)`.
Defaults to :py:`"sync"` Defaults to :py:`"sync"`.
o_domain : :class:`str` o_domain : :class:`str`
Domain for output and output enable registers. Only used when Name of the output and output enable registers' clock domain. Used when
:py:`direction in (Direction.Output, Direction.Bidir)`. Defaults to :py:`"sync"` :py:`direction in (Output, Bidir)`. Defaults to :py:`"sync"`.
Attributes Attributes
---------- ----------
signature : FFBuffer.Signature signature : :class:`FFBuffer.Signature`
Created based on constructor arguments. :py:`Signature(direction, len(port)).flip()`.
""" """
class Signature(wiring.Signature): class Signature(wiring.Signature):
"""A signature of a registered I/O buffer. """Signature of a registered I/O buffer.
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
width : :class:`int` width : :class:`int`
Width of the buffer.
Attributes Members
---------- -------
i: :py:`unsigned(width)` (if :py:`direction in (Direction.Input, Direction.Bidir)`) i: :py:`In(width)`
o: :py:`unsigned(width)` (if :py:`direction in (Direction.Output, Direction.Bidir)`) Present if :py:`direction in (Input, Bidir)`.
oe: :py:`unsigned(1, init=0)` (if :py:`direction is Direction.Bidir`) o: :py:`Out(width)`
oe: :py:`unsigned(1, init=1)` (if :py:`direction is Direction.Output`) Present if :py:`direction in (Output, Bidir)`.
oe: :py:`Out(1, init=0)`
Present if :py:`direction is Bidir`.
oe: :py:`Out(1, init=1)`
Present if :py:`direction is Output`.
""" """
def __init__(self, direction, width): def __init__(self, direction, width):
self._direction = Direction(direction) self._direction = Direction(direction)
@ -465,7 +575,8 @@ class FFBuffer(wiring.Component):
return self._width return self._width
def __eq__(self, other): def __eq__(self, other):
return type(self) is type(other) and self.direction == other.direction and self.width == other.width return (type(self) is type(other) and self.direction == other.direction and
self.width == other.width)
def __repr__(self): def __repr__(self):
return f"FFBuffer.Signature({self.direction}, {self.width})" return f"FFBuffer.Signature({self.direction}, {self.width})"
@ -533,48 +644,117 @@ class FFBuffer(wiring.Component):
class DDRBuffer(wiring.Component): class DDRBuffer(wiring.Component):
"""A double data rate registered I/O buffer. """A double data rate I/O buffer component.
In the input direction, the port is sampled at both edges of the input clock domain. This buffer is only available on platforms that support double data rate I/O.
The data sampled on the active clock edge of the domain appears on :py:`i[0]` with a delay
of 1 clock cycle. The data sampled on the opposite clock edge appears on :py:`i[1]` with a delay
of 0.5 clock cycle. Both :py:`i[0]` and :py:`i[1]` thus change on the active clock edge of the domain.
In the output direction, both :py:`o[0]` and :py:`o[1]` are sampled on the active clock edge The following diagram defines the timing relationship between the underlying core I/O value
of the domain. The value of :py:`o[0]` immediately appears on the output port. The value (for differential ports, the core I/O value of the true half) and the :py:`i`, :py:`o`, and
of :py:`o[1]` then appears on the output port on the opposite edge, with a delay of 0.5 clock cycle. :py:`oe` members:
Support for this compoment is platform-specific, and may be missing on some platforms. ..
This diagram should have `port` phase shifted, but it hits wavedrom/wavedrom#416.
It is also affected by wavedrom/wavedrom#417.
.. wavedrom:: io/ddr-buffer
{
"head": {
"tick": 0
},
"signal": [
{"name": "clk", "wave": "p......."},
{"name": "o[0]", "wave": "x357x...", "node": ".a",
"data": ["A", "C", "E"]},
{"name": "o[1]", "wave": "x468x...", "node": ".b",
"data": ["B", "D", "F"]},
{"name": "oe", "wave": "01..0..."},
{ "node": "........R.S",
"period": 0.5},
{"name": "port", "wave": "z...345678z.....", "node": "....123456",
"data": ["A", "B", "C", "D", "E", "F"],
"period": 0.5},
{ "node": "..P.Q",
"period": 0.5},
{"name": "i[0]", "wave": "x...468x", "node": ".....d",
"data": ["B", "D", "F"]},
{"name": "i[1]", "wave": "x..357x.", "node": ".....e",
"data": ["A", "C", "E"]}
],
"edge": [
"a~1", "b-~2", "P+Q t1",
"5~-d", "6~e", "R+S t2"
],
"config": {
"hscale": 2
}
}
The output data (labelled *a*, *b*) is input from :py:`o` into internal registers at
the beginning of clock cycle 2, and transmitted at points labelled *1*, *2* during the same
clock cycle. The output latency *t1* is defined as the amount of cycles between the time of
capture of :py:`o` and the time of transmission of rising edge data plus one cycle, and is 1
for this diagram.
The received data is captured into internal registers during the clock cycle 4 at points
labelled *5*, *6*, and output to :py:`i` during the next clock cycle (labelled *d*, *e*).
The input latency *t2* is defined as the amount of cycles between the time of reception of
rising edge data and the time of update of :py:`i`, and is 1 for this diagram.
The output enable signal is input from :py:`oe` once per cycle and affects the entire cycle it
applies to. Its latency is defined in the same way as the output latency, and is equal to *t1*.
.. warning::
Some platforms include additional pipeline registers that may cause latencies *t1* and *t2*
to be higher than one cycle. At the moment there is no way to query these latencies.
This limitation will be lifted in the future.
.. warning::
On all supported platforms, this buffer can only be used with rising edge clock domains,
and will raise an exception during conversion of the design to a netlist otherwise.
This limitation may be lifted in the future.
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
port : :class:`PortLike` port : :class:`PortLike`
Port driven by the buffer.
i_domain : :class:`str` i_domain : :class:`str`
Domain for input register. Only used when :py:`direction in (Direction.Input, Direction.Bidir)`. Name of the input registers' clock domain. Only used when :py:`direction in (Input, Bidir)`.
o_domain : :class:`str` o_domain : :class:`str`
Domain for output and output enable registers. Only used when Name of the output and output enable registers' clock domain. Only used when
:py:`direction in (Direction.Output, Direction.Bidir)`. :py:`direction in (Output, Bidir)`.
Attributes Attributes
---------- ----------
signature : DDRBuffer.Signature signature : :class:`DDRBuffer.Signature`
Created based on constructor arguments. :py:`Signature(direction, len(port)).flip()`.
""" """
class Signature(wiring.Signature): class Signature(wiring.Signature):
"""A signature of a double data rate registered I/O buffer. """Signature of a double data rate I/O buffer.
Parameters Parameters
---------- ----------
direction : :class:`Direction` direction : :class:`Direction`
Direction of the buffer.
width : :class:`int` width : :class:`int`
Width of the buffer.
Attributes Members
---------- -------
i: :py:`unsigned(ArrayLayout(width, 2))` (if :py:`direction in (Direction.Input, Direction.Bidir)`) i: :py:`In(ArrayLayout(width, 2))`
o: :py:`unsigned(ArrayLayout(width, 2))` (if :py:`direction in (Direction.Output, Direction.Bidir)`) Present if :py:`direction in (Input, Bidir)`.
oe: :py:`unsigned(1, init=0)` (if :py:`direction is Direction.Bidir`) o: :py:`Out(ArrayLayout(width, 2))`
oe: :py:`unsigned(1, init=1)` (if :py:`direction is Direction.Output`) Present if :py:`direction in (Output, Bidir)`.
oe: :py:`Out(1, init=0)`
Present if :py:`direction is Bidir`.
oe: :py:`Out(1, init=1)`
Present if :py:`direction is Output`.
""" """
def __init__(self, direction, width): def __init__(self, direction, width):
self._direction = Direction(direction) self._direction = Direction(direction)
@ -596,7 +776,8 @@ class DDRBuffer(wiring.Component):
return self._width return self._width
def __eq__(self, other): def __eq__(self, other):
return type(self) is type(other) and self.direction == other.direction and self.width == other.width return (type(self) is type(other) and self.direction == other.direction and
self.width == other.width)
def __repr__(self): def __repr__(self):
return f"DDRBuffer.Signature({self.direction}, {self.width})" return f"DDRBuffer.Signature({self.direction}, {self.width})"
@ -643,7 +824,7 @@ class DDRBuffer(wiring.Component):
if hasattr(platform, "get_io_buffer"): if hasattr(platform, "get_io_buffer"):
return platform.get_io_buffer(self) return platform.get_io_buffer(self)
raise NotImplementedError("DDR buffers cannot be elaborated without a supported platform") raise NotImplementedError(f"DDR buffers are not supported on {platform!r}") # :nocov:
class Pin(wiring.PureInterface): class Pin(wiring.PureInterface):

View file

@ -37,6 +37,7 @@ autodoc_default_options = {
"members": True "members": True
} }
autodoc_preserve_defaults = True autodoc_preserve_defaults = True
autodoc_inherit_docstrings = False
napoleon_google_docstring = False napoleon_google_docstring = False
napoleon_numpy_docstring = True napoleon_numpy_docstring = True
@ -71,3 +72,9 @@ linkcheck_anchors_ignore_for_url = [
# React page with README content included as a JSON payload. # React page with README content included as a JSON payload.
r"^https://github\.com/[^/]+/[^/]+/$", r"^https://github\.com/[^/]+/[^/]+/$",
] ]
# Silence the warnings globally; otherwise they may fire on object destruction and crash completely
# unrelated tests.
import amaranth._unused
amaranth._unused.MustUse._MustUse__silence = True

View file

@ -1673,9 +1673,9 @@ Amaranth provides support for memories in the standard library module :mod:`amar
I/O values I/O values
========== ==========
To interoperate with external circuitry, Amaranth provides *I/O values*, which represent bundles of wires carrying uninterpreted signals. Unlike regular :ref:`values <lang-values>`, which represent binary numbers and can be :ref:`assigned <lang-assigns>` to create a unidirectional connection or used in computations, I/O values represent electrical signals that may be digital or analog and have no :ref:`shape <lang-shapes>`, cannot be assigned, used in computations, or simulated. To interoperate with external circuitry, Amaranth provides *core I/O values*, which represent bundles of wires carrying uninterpreted signals. Unlike regular :ref:`values <lang-values>`, which represent binary numbers and can be :ref:`assigned <lang-assigns>` to create a unidirectional connection or used in computations, core I/O values represent electrical signals that may be digital or analog and have no :ref:`shape <lang-shapes>`, cannot be assigned, used in computations, or simulated.
I/O values are only used to define connections between non-Amaranth building blocks that traverse an Amaranth design, including :ref:`instances <lang-instance>` and :ref:`I/O buffer instances <lang-iobufferinstance>`. Core I/O values are only used to define connections between non-Amaranth building blocks that traverse an Amaranth design, including :ref:`instances <lang-instance>` and :ref:`I/O buffer instances <lang-iobufferinstance>`.
.. _lang-ioports: .. _lang-ioports:
@ -1683,7 +1683,7 @@ I/O values are only used to define connections between non-Amaranth building blo
I/O ports I/O ports
--------- ---------
An *I/O port* is an I/O value representing a connection to a port of the topmost module in the :ref:`design hierarchy <lang-submodules>`. It can be created with an explicitly specified width. A *core I/O port* is a core I/O value representing a connection to a port of the topmost module in the :ref:`design hierarchy <lang-submodules>`. It can be created with an explicitly specified width.
.. testcode:: .. testcode::
@ -1695,7 +1695,7 @@ An *I/O port* is an I/O value representing a connection to a port of the topmost
>>> port.width >>> port.width
4 4
I/O ports can be named in the same way as :ref:`signals <lang-signalname>`: Core I/O ports can be named in the same way as :ref:`signals <lang-signalname>`:
.. doctest:: .. doctest::
@ -1703,7 +1703,7 @@ I/O ports can be named in the same way as :ref:`signals <lang-signalname>`:
>>> clk_port.name >>> clk_port.name
'clk' 'clk'
If two I/O ports with the same name exist in a design, one of them will be renamed to remove the ambiguity. Because the name of an I/O port is significant, they should be named unambiguously. If two core I/O ports with the same name exist in a design, one of them will be renamed to remove the ambiguity. Because the name of a core I/O port is significant, they should be named unambiguously.
.. _lang-ioops: .. _lang-ioops:
@ -1711,7 +1711,7 @@ If two I/O ports with the same name exist in a design, one of them will be renam
I/O operators I/O operators
------------- -------------
I/O values support only a limited set of :ref:`sequence <python:typesseq>` operators, all of which return another I/O value. The following table lists the I/O operators provided by Amaranth: Core I/O values support only a limited set of :ref:`sequence <python:typesseq>` operators, all of which return another core I/O value. The following table lists the operators provided by Amaranth for core I/O values:
=============== ============================== =================== =============== ============================== ===================
Operation Description Notes Operation Description Notes
@ -1747,9 +1747,9 @@ An instance can be added as a submodule using the :py:`m.submodules.name = Insta
* An attribute is specified using the :py:`a_ANAME=attr` or :py:`("a", "ANAME", attr)` syntaxes. The :py:`attr` must be an :class:`int`, a :class:`str`, or a :class:`Const`. * An attribute is specified using the :py:`a_ANAME=attr` or :py:`("a", "ANAME", attr)` syntaxes. The :py:`attr` must be an :class:`int`, a :class:`str`, or a :class:`Const`.
* A parameter is specified using the :py:`p_PNAME=param` or :py:`("p", "PNAME", param)` syntaxes. The :py:`param` must be an :class:`int`, a :class:`str`, or a :class:`Const`. * A parameter is specified using the :py:`p_PNAME=param` or :py:`("p", "PNAME", param)` syntaxes. The :py:`param` must be an :class:`int`, a :class:`str`, or a :class:`Const`.
* An input is specified using the :py:`i_INAME=in_val` or :py:`("i", "INAME", in_val)` syntaxes. The :py:`in_val` must be an :ref:`I/O value <lang-iovalues>` or a :ref:`value-like <lang-valuelike>` object. * An input is specified using the :py:`i_INAME=in_val` or :py:`("i", "INAME", in_val)` syntaxes. The :py:`in_val` must be a :ref:`core I/O value <lang-iovalues>` or a :ref:`value-like <lang-valuelike>` object.
* An output is specified using the :py:`o_ONAME=out_val` or :py:`("o", "ONAME", out_val)` syntaxes. The :py:`out_val` must be an :ref:`I/O value <lang-iovalues>` or a :ref:`value-like <lang-valuelike>` object that casts to a :ref:`signal <lang-signals>`, a concatenation of signals, or a slice of a signal. * An output is specified using the :py:`o_ONAME=out_val` or :py:`("o", "ONAME", out_val)` syntaxes. The :py:`out_val` must be a :ref:`core I/O value <lang-iovalues>` or a :ref:`value-like <lang-valuelike>` object that casts to a :ref:`signal <lang-signals>`, a concatenation of signals, or a slice of a signal.
* An inout is specified using the :py:`io_IONAME=inout_val` or :py:`("io", "IONAME", inout_val)` syntaxes. The :py:`inout_val` must be an :ref:`I/O value <lang-iovalues>`. * An inout is specified using the :py:`io_IONAME=inout_val` or :py:`("io", "IONAME", inout_val)` syntaxes. The :py:`inout_val` must be a :ref:`core I/O value <lang-iovalues>`.
The two following examples use both syntaxes to add the same instance of type ``external`` as a submodule named ``processor``: The two following examples use both syntaxes to add the same instance of type ``external`` as a submodule named ``processor``:
@ -1833,7 +1833,11 @@ Although an :class:`Instance` is not an elaboratable, as a special case, it can
I/O buffer instances I/O buffer instances
==================== ====================
An *I/O buffer instance* is a submodule that allows connecting :ref:`I/O values <lang-iovalues>` and regular :ref:`values <lang-values>` without the use of an external, toolchain- and technology-dependent :ref:`instance <lang-instance>`. It can be created in four configurations: input, output, tristatable output, and bidirectional (input/output). .. note::
I/O buffer instances are a low-level primitive which is documented to ensure that the standard library does not rely on private interfaces in the core language. Most designers should use the :mod:`amaranth.lib.io` module instead.
An *I/O buffer instance* is a submodule that allows connecting :ref:`core I/O values <lang-iovalues>` and regular :ref:`values <lang-values>` without the use of an external, toolchain- and technology-dependent :ref:`instance <lang-instance>`. It can be created in four configurations: input, output, tristatable output, and bidirectional (input/output).
.. testcode:: .. testcode::
@ -1841,7 +1845,7 @@ An *I/O buffer instance* is a submodule that allows connecting :ref:`I/O values
m = Module() m = Module()
In the input configuration, the buffer combinationally drives a signal :py:`i` by the port: In the input configuration, the buffer instance combinationally drives a signal :py:`i` by the port:
.. testcode:: .. testcode::
@ -1849,7 +1853,7 @@ In the input configuration, the buffer combinationally drives a signal :py:`i` b
port_i = Signal(4) port_i = Signal(4)
m.submodules += IOBufferInstance(port, i=port_i) m.submodules += IOBufferInstance(port, i=port_i)
In the output configuration, the buffer combinationally drives the port by a value :py:`o`: In the output configuration, the buffer instance combinationally drives the port by a value :py:`o`:
.. testcode:: .. testcode::
@ -1857,7 +1861,7 @@ In the output configuration, the buffer combinationally drives the port by a val
port_o = Signal(4) port_o = Signal(4)
m.submodules += IOBufferInstance(port, o=port_o) m.submodules += IOBufferInstance(port, o=port_o)
In the tristatable output configuration, the buffer combinationally drives the port by a value :py:`o` if :py:`oe` is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise: In the tristatable output configuration, the buffer instance combinationally drives the port by a value :py:`o` if :py:`oe` is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise:
.. testcode:: .. testcode::
@ -1866,7 +1870,7 @@ In the tristatable output configuration, the buffer combinationally drives the p
port_oe = Signal() port_oe = Signal()
m.submodules += IOBufferInstance(port, o=port_o, oe=port_oe) m.submodules += IOBufferInstance(port, o=port_o, oe=port_oe)
In the bidirectional (input/output) configuration, the buffer combinationally drives a signal :py:`i` by the port, combinationally drives the port by a value :py:`o` if :py:`oe` is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise: In the bidirectional (input/output) configuration, the buffer instance combinationally drives a signal :py:`i` by the port, combinationally drives the port by a value :py:`o` if :py:`oe` is asserted, and does not drive (leaves in a high-impedance state, or tristates) the port otherwise:
.. testcode:: .. testcode::

View file

@ -19,6 +19,7 @@ The Amaranth standard library is separate from the Amaranth language: everything
stdlib/wiring stdlib/wiring
stdlib/meta stdlib/meta
stdlib/memory stdlib/memory
stdlib/io
stdlib/cdc stdlib/cdc
stdlib/coding stdlib/coding
stdlib/fifo stdlib/fifo

209
docs/stdlib/io.rst Normal file
View file

@ -0,0 +1,209 @@
Input/output buffers
====================
.. py:module:: amaranth.lib.io
The :mod:`amaranth.lib.io` module provides a platform-independent way to instantiate platform-specific input/output buffers: combinational, synchronous, and double data rate (DDR).
Introduction
------------
The Amaranth language provides :ref:`core I/O values <lang-iovalues>` that designate connections to external devices, and :ref:`I/O buffer instances <lang-iobufferinstance>` that implement platform-independent combinational I/O buffers. This low-level mechanism is foundational to all I/O in Amaranth and must be used whenever a device-specific platform is unavailable, but is limited in its capabilities. The :mod:`amaranth.lib.io` module builds on top of it to provide *library I/O ports* that specialize and annotate I/O values, and *buffer components* that connect ports to logic.
.. note::
Unfortunately, the terminology related to I/O has several ambiguities:
* A "port" could refer to an *interface port* (:class:`.Signal` objects created by the :mod:`amaranth.lib.wiring` module), a *core I/O port* (:class:`amaranth.hdl.IOPort` object), or a *library I/O port* (:class:`amaranth.lib.io.PortLike` object).
* A "I/O buffer" could refer to an *I/O buffer instance* (:class:`amaranth.hdl.IOBufferInstance`) or a *I/O buffer component* (:class:`amaranth.lib.io.Buffer`, :class:`.FFBuffer`, or :class:`.DDRBuffer` objects).
Amaranth documentation always uses the least ambiguous form of these terms.
Examples
--------
.. testsetup::
from amaranth import *
class MockPlatform:
def request(self, name, *, dir):
from amaranth.hdl import IOPort
from amaranth.lib import io
if name == "led":
return io.SingleEndedPort(IOPort(1, name=name), direction="o")
if name == "clk24":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "d":
return io.SingleEndedPort(IOPort(8, name=name), direction="io")
if name == "re":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "we":
return io.SingleEndedPort(IOPort(1, name=name), direction="i")
if name == "dclk":
return io.SingleEndedPort(IOPort(1, name=name), direction="o")
if name == "dout":
return io.SingleEndedPort(IOPort(8, name=name), direction="o")
raise NameError
def get_io_buffer(self, buffer):
return Fragment()
def build(self, top):
from amaranth.back import rtlil
return rtlil.convert(Fragment.get(top, self), ports=[])
All of the following examples assume that one of the built-in FPGA platforms is used.
.. testcode::
from amaranth.lib import io, wiring
from amaranth.lib.wiring import In, Out
LED output
++++++++++
In this example, a library I/O port for a LED is requested from the platform and driven to blink the LED:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
delay = Signal(24)
state = Signal()
with m.If(delay == 0):
m.d.sync += delay.eq(~0)
m.d.sync += state.eq(~state)
with m.Else():
m.d.sync += delay.eq(delay - 1)
m.submodules.led = led = io.Buffer("o", platform.request("led", dir="-"))
m.d.comb += led.o.eq(state)
return m
.. testcode::
:hide:
MockPlatform().build(Toplevel())
Clock input
+++++++++++
In this example, a clock domain is created and driven from an external clock source:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
m.domains.sync = cd_sync = ClockDomain(local=True)
m.submodules.clk24 = clk24 = io.Buffer("i", platform.request("clk24", dir="-"))
m.d.comb += cd_sync.clk.eq(clk24.i)
...
return m
.. testcode::
:hide:
MockPlatform().build(Toplevel())
Bidirectional bus
+++++++++++++++++
This example implements a peripheral for a clocked parallel bus. This peripheral can store and recall one byte of data. The data is stored with a write enable pulse, and recalled with a read enable pulse:
.. testcode::
class Toplevel(Elaboratable):
def elaborate(self, platform):
m = Module()
m.submodules.bus_d = bus_d = io.FFBuffer("io", platform.request("d", dir="-"))
m.submodules.bus_re = bus_re = io.Buffer("i", platform.request("re", dir="-"))
m.submodules.bus_we = bus_we = io.Buffer("i", platform.request("we", dir="-"))
data = Signal.like(bus_d.i)
with m.If(bus_re.i):
m.d.comb += bus_d.oe.eq(1)
m.d.comb += bus_d.o.eq(data)
with m.Elif(bus_we.i):
m.d.sync += data.eq(bus_d.i)
return m
.. testcode::
:hide:
MockPlatform().build(Toplevel())
This bus requires a turn-around time of at least 1 cycle to avoid electrical contention.
Note that data appears on the bus one cycle after the read enable input is asserted, and that the write enable input stores the data present on the bus in the *previous* cycle. This is called *pipelining* and is typical for clocked buses; see :class:`.FFBuffer` for a waveform diagram. Although it increases the maximum clock frequency at which the bus can run, it also makes the bus signaling more complicated.
Clock forwarding
++++++++++++++++
In this example of a `source-synchronous interface <https://en.wikipedia.org/wiki/Source-synchronous>`__, a clock signal is generated with the same phase as the DDR data signals associated with it:
.. testcode::
class SourceSynchronousOutput(wiring.Component):
dout: In(16)
def elaborate(self, platform):
m = Module()
m.submodules.bus_dclk = bus_dclk = \
io.DDRBuffer("o", platform.request("dclk", dir="-"))
m.d.comb += [
bus_dclk.o[0].eq(1),
bus_dclk.o[1].eq(0),
]
m.submodules.bus_dout = bus_dout = \
io.DDRBuffer("o", platform.request("dout", dir="-"))
m.d.comb += [
bus_dout.o[0].eq(self.dout[:8]),
bus_dout.o[1].eq(self.dout[8:]),
]
return m
.. testcode::
:hide:
MockPlatform().build(SourceSynchronousOutput())
This component transmits :py:`dout` on each cycle as two halves: the low 8 bits on the rising edge of the data clock, and the high 8 bits on the falling edge of the data clock. The transmission is *edge-aligned*, meaning that the data edges exactly coincide with the clock edges.
Ports
-----
.. autoclass:: Direction()
.. autoclass:: PortLike
.. autoclass:: SingleEndedPort
.. autoclass:: DifferentialPort
Buffers
-------
.. autoclass:: Buffer(direction, port)
.. autoclass:: FFBuffer(direction, port, *, i_domain=None, o_domain=None)
.. autoclass:: DDRBuffer(direction, port, *, i_domain=None, o_domain=None)

View file

@ -11,19 +11,6 @@ from .utils import *
class DirectionTestCase(FHDLTestCase): class DirectionTestCase(FHDLTestCase):
def test_or(self):
self.assertIs(Direction.Input | Direction.Input, Direction.Input)
self.assertIs(Direction.Input | Direction.Output, Direction.Bidir)
self.assertIs(Direction.Input | Direction.Bidir, Direction.Bidir)
self.assertIs(Direction.Output | Direction.Input, Direction.Bidir)
self.assertIs(Direction.Output | Direction.Output, Direction.Output)
self.assertIs(Direction.Output | Direction.Bidir, Direction.Bidir)
self.assertIs(Direction.Bidir | Direction.Input, Direction.Bidir)
self.assertIs(Direction.Bidir | Direction.Output, Direction.Bidir)
self.assertIs(Direction.Bidir | Direction.Bidir, Direction.Bidir)
with self.assertRaises(TypeError):
Direction.Bidir | 3
def test_and(self): def test_and(self):
self.assertIs(Direction.Input & Direction.Input, Direction.Input) self.assertIs(Direction.Input & Direction.Input, Direction.Input)
self.assertIs(Direction.Input & Direction.Bidir, Direction.Input) self.assertIs(Direction.Input & Direction.Bidir, Direction.Input)