- Start Date: 2024-03-18
- RFC PR: amaranth-lang/rfcs#55
- Amaranth Issue: amaranth-lang/amaranth#1210
New lib.io components
Summary
Building on RFC 2 and RFC 53, a new set of components is added to lib.io. The current contents of lib.io (Pin and its signature) become deprecated.
Motivation
Currently, all IO buffer and register logic is instantiated in one place by platform.request. Per amaranth-lang/amaranth#458, this has caused problems. Ideally, the act of requesting a specific I/O (user's responsibility) would be decoupled from instantiating the I/O buffer (peripherial library's responsibility).
Further, we currently have no standard I/O buffer components, other than the low-level IOBufferInstance.
Guide-level explanation
IOPort, introduced in RFC 53, is Amaranth's low-level IO port primitive. In lib.io, IOPorts
are wrapped in two higher-level objects: SingleEndedPort and DifferentialPort. These objects contain the raw IOValues together with per-bit inversion flags. They are obtained from platform.request. If no platform is used, they can be constructed by the user directly, like this:
a = SingleEndedPort(IOPort(8, name="a")) # simple 8-bit IO port
b = SingleEndedPort(IOPort(8, name="b"), invert=True) # 8-bit IO port, all bits inverted
c = SingleEndedPort(IOPort(4, name="c"), invert=[False, True, False, True]) # 4-bit IO port, varying per-bit inversions
d = DifferentialPort(p=IOPort(4, name="dp"), n=IOPort(4, name="dn")) # differential 4-bit IO port
Once a *Port object is obtained, whether from platform.request or by direct creation, it most likely needs to be passed to an I/O buffer. Amaranth provides a set of cross-platform I/O buffer components in lib.io.
For a non-registered port, the lib.io.Buffer can be used:
port = platform.request(...) # or = SingleEndedPort(,,,)
m.submodules.iob = iob = lib.io.Buffer(lib.io.Direction.Bidir, port)
m.d.comb += [
iob.o.eq(...),
iob.oe.eq(...),
(...).eq(iob.i),
]
For an SDR registered port, the lib.io.FFBuffer can be used:
m.submodules.iob = iob = lib.io.FFBuffer(lib.io.Direction.Bidir, port, i_domain="sync", o_domain="sync")
m.d.comb += [
iob.o.eq(...),
iob.oe.eq(...),
(...).eq(iob.i),
]
For a DDR registered port (given a supported platform), the lib.io.DDRBuffer can be used:
m.submodules.iob = iob = lib.io.DDRBuffer(lib.io.Direction.Bidir, port, i_domain="sync", o_domain="sync")
m.d.comb += [
iob.o[0].eq(...),
iob.o[1].eq(...),
iob.oe.eq(...),
(...).eq(iob.i[0]),
(...).eq(iob.i[1]),
]
All of the above primitives are components with corresponding signature types. When elaborated, the primitives call a platform hook, allowing it to provide a custom implementation using vendor-specific cells. If no special support is provided by the platform, Buffer and FFBuffer provide a simple vendor-agnostic default implementation, while DDRBuffer raises an error when elaborated.
Reference-level explanation
The following classes are added to lib.io:
-
class Direction(enum.Enum): Input = "i" Output = "o" Bidir = "io"Represents a port or buffer direction.
-
SingleEndedPort(io: IOValue, *, invert: bool | Iterable[bool]=False, direction: Direction=Direction.Bidir): represents a single ended port; theinvertparameter is normalized to a tuple ofboolbefore being stored as an attribute__len__(self): returnslen(io)__getitem__(self, index: slice | int): allows slicing the object, returning anotherSingleEndedPort; requesting a single index is equivalent to requesting a one-element slice__add__(self, other: SingleEndedPort): concatenates two ports together into a biggerSingleEndedPort__invert__(self): returns a newSingleEndedPortderived from this one by having the opposite (every element of)invert
-
DifferentialPort(p: IOValue, n: IOValue, *, invert: bool | Iterable[bool]=False, direction: Direction=Direction.Bidir): represents a differential pair; bothIOValues given as arguments must have equal width__len__(self): returnslen(p)(which is equal tolen(n))__getitem__(self, index: slice | int): allows slicing the object, returning anotherDifferentialPort__add__(self, other: DifferentialPort): concatenates two ports together into a biggerDifferentialPort__invert__(self): returns a newDifferentialPortderived from this one by having the opposite (every element of)invert
-
Buffer.Signature(direction: Direction | str, width: int): a signature for theBuffer; ifdirectionis a string, it is converted toDirectioni: Out(width)ifdirection in (Direction.Input, Direction.Bidir)o: In(width)ifdirection in (Direction.Output, Direction.Bidir)oe: In(1, init=1)ifdirection is Direction.Outputoe: In(1, init=0)ifdirection is Direction.Bidir
-
Buffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ...): non-registered buffer, derives fromComponent- when elaborated, tries to return
platform.get_io_buffer(self); if such a function doesn't exist, lowers toIOBufferInstanceplus optional inverters
- when elaborated, tries to return
-
FFBuffer.Signature(direction: Direction | str, width: int): a signature for theFFBufferi: Out(width)ifdirection in (Direction.Input, Direction.Bidir)o: In(width)ifdirection in (Direction.Output, Direction.Bidir)oe: In(1, init=1)ifdirection is Direction.Outputoe: In(1, init=0)ifdirection is Direction.Bidir
-
FFBuffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ..., *, i_domain="sync", o_domain="sync"): SDR registered buffer, derives fromComponent- when elaborated, tries to return
platform.get_io_buffer(self); if such a function doesn't exist, lowers toIOBufferInstance, plus reset-less FFs realized bym.d[*_domain]assignment, plus optional inverters
- when elaborated, tries to return
-
DDRBuffer.Signature(direction: Direction | str, width: int): a signature for theDDRBufferi: Out(ArrayLayout(width, 2))ifdirection in (Direction.Input, Direction.Bidir)o: In(ArrayLayout(width, 2))ifdirection in (Direction.Output, Direction.Bidir)oe: In(1, init=1)ifdirection is Direction.Outputoe: In(1, init=0)ifdirection is Direction.Bidir
-
DDRBuffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ..., *, i_domain="sync", o_domain="sync"): DDR registered buffer, derives fromComponent- when elaborated, tries to return
platform.get_io_buffer(self); if such a function doesn't exist, raises an error
- when elaborated, tries to return
All of the above classes are fully introspectable, and the constructor arguments are accessible as read-only attributes.
If a platform is not used, the port argument must be a SingleEndedPort or DifferentialPort. If a platform is used, the platform may define support for additional types. Such types must implement the same interface as *Port objects, that is:
__len__must provide length in bits (so that*Buffercan know the proper signature)__getitem__which supports slices, and where plain indices return single-bit slices__invert__that returns another port-likedirectionattribute that must be aDirection
If a platform is not used, and a DifferentialPort is used, a pseudo-differential port is effectively created.
The direction argument on *Port can be used to restrict valid allowed buffer directions as follows:
- an
Inputbuffer will not accept anOutputport - an
Outputbuffer will not accept anInputport - a
Bidirbuffer will only accept aBidirport
This is validated by the *Buffer constructors. Custom buffer-like elaboratables that take *Port are likewise encouraged to perform similar checking.
The platform.request function with dir="-" returns SingleEndedPort when called on single-ended ports, DifferentialPort when called on differential pairs. Using platform.request with any other dir becomes deprecated, in favor of having the user (or peripherial library) code explicitly instantiate *Buffers. The lib.io.Pin interface and its signature likewise become deprecated.
Drawbacks
The proposed FFBuffer and DDRBuffer interfaces have a minor problem of not actually being currently implementable in many cases, as there is no way to obtain clock signal polarity at that stage of elaboration. A solution for that needs to be proposed, whether as a private hack for the current platforms, or as an RFC.
Using plain domains for DDRBuffer has the unprecedented property of triggering logic on the opposite of active edge of the domain.
Rationale and alternatives
The buffers have minimal functionality on purpose, to allow them to be widely supported. In particular:
- clock enables are not supported
- reset is not supported
- initial values are not supported
xdr > 2is not supported
Such functionality can be provided by vendor-specific primitives.
Prior art
None.
Unresolved questions
None.
Future possibilities
Vendor-specific versions of the proposed buffers can be added to the vendor module, allowing access to the full range of hardware functionality.