|
| 1 | +- Start Date: 2024-03-18 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#55](https://github.com/amaranth-lang/rfcs/pull/55) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1210](https://github.com/amaranth-lang/amaranth/issues/1210) |
| 4 | + |
| 5 | +# New `lib.io` components |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +## Motivation |
| 13 | +[motivation]: #motivation |
| 14 | + |
| 15 | +Currently, all IO buffer and register logic is instantiated in one place by `platform.request`. Per [amaranth-lang/amaranth#458](https://github.com/amaranth-lang/amaranth/issues/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). |
| 16 | + |
| 17 | +Further, we currently have no standard I/O buffer components, other than the low-level `IOBufferInstance`. |
| 18 | + |
| 19 | +## Guide-level explanation |
| 20 | +[guide-level-explanation]: #guide-level-explanation |
| 21 | + |
| 22 | +`IOPort`, introduced in RFC 53, is Amaranth's low-level IO port primitive. In `lib.io`, `IOPort`s |
| 23 | +are wrapped in two higher-level objects: `SingleEndedPort` and `DifferentialPort`. These objects contain the raw `IOValue`s 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: |
| 24 | + |
| 25 | +```py |
| 26 | +a = SingleEndedPort(IOPort(8, name="a")) # simple 8-bit IO port |
| 27 | +b = SingleEndedPort(IOPort(8, name="b"), invert=True) # 8-bit IO port, all bits inverted |
| 28 | +c = SingleEndedPort(IOPort(4, name="c"), invert=[False, True, False, True]) # 4-bit IO port, varying per-bit inversions |
| 29 | +d = DifferentialPort(p=IOPort(4, name="dp"), n=IOPort(4, name="dn")) # differential 4-bit IO port |
| 30 | +``` |
| 31 | + |
| 32 | +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`. |
| 33 | + |
| 34 | +For a non-registered port, the `lib.io.Buffer` can be used: |
| 35 | + |
| 36 | +```py |
| 37 | +port = platform.request(...) # or = SingleEndedPort(,,,) |
| 38 | +m.submodules.iob = iob = lib.io.Buffer(lib.io.Direction.Bidir, port) |
| 39 | +m.d.comb += [ |
| 40 | + iob.o.eq(...), |
| 41 | + iob.oe.eq(...), |
| 42 | + (...).eq(iob.i), |
| 43 | +] |
| 44 | +``` |
| 45 | + |
| 46 | +For an SDR registered port, the `lib.io.FFBuffer` can be used: |
| 47 | + |
| 48 | +```py |
| 49 | +m.submodules.iob = iob = lib.io.FFBuffer(lib.io.Direction.Bidir, port, i_domain="sync", o_domain="sync") |
| 50 | +m.d.comb += [ |
| 51 | + iob.o.eq(...), |
| 52 | + iob.oe.eq(...), |
| 53 | + (...).eq(iob.i), |
| 54 | +] |
| 55 | +``` |
| 56 | + |
| 57 | +For a DDR registered port (given a supported platform), the `lib.io.DDRBuffer` can be used: |
| 58 | + |
| 59 | +```py |
| 60 | +m.submodules.iob = iob = lib.io.DDRBuffer(lib.io.Direction.Bidir, port, i_domain="sync", o_domain="sync") |
| 61 | +m.d.comb += [ |
| 62 | + iob.o[0].eq(...), |
| 63 | + iob.o[1].eq(...), |
| 64 | + iob.oe.eq(...), |
| 65 | + (...).eq(iob.i[0]), |
| 66 | + (...).eq(iob.i[1]), |
| 67 | +] |
| 68 | +``` |
| 69 | + |
| 70 | +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. |
| 71 | + |
| 72 | +## Reference-level explanation |
| 73 | +[reference-level-explanation]: #reference-level-explanation |
| 74 | + |
| 75 | +The following classes are added to `lib.io`: |
| 76 | + |
| 77 | +- ```py |
| 78 | + class Direction(enum.Enum): |
| 79 | + Input = "i" |
| 80 | + Output = "o" |
| 81 | + Bidir = "io" |
| 82 | + ``` |
| 83 | + |
| 84 | + Represents a port or buffer direction. |
| 85 | + |
| 86 | +- `SingleEndedPort(io: IOValue, *, invert: bool | Iterable[bool]=False, direction: Direction=Direction.Bidir)`: represents a single ended port; the `invert` parameter is normalized to a tuple of `bool` before being stored as an attribute |
| 87 | + - `__len__(self)`: returns `len(io)` |
| 88 | + - `__getitem__(self, index: slice | int)`: allows slicing the object, returning another `SingleEndedPort`; requesting a single index is equivalent to requesting a one-element slice |
| 89 | + - `__add__(self, other: SingleEndedPort)`: concatenates two ports together into a bigger `SingleEndedPort` |
| 90 | + - `__invert__(self)`: returns a new `SingleEndedPort` derived from this one by having the opposite (every element of) `invert` |
| 91 | +- `DifferentialPort(p: IOValue, n: IOValue, *, invert: bool | Iterable[bool]=False, direction: Direction=Direction.Bidir)`: represents a differential pair; both `IOValue`s given as arguments must have equal width |
| 92 | + - `__len__(self)`: returns `len(p)` (which is equal to `len(n)`) |
| 93 | + - `__getitem__(self, index: slice | int)`: allows slicing the object, returning another `DifferentialPort` |
| 94 | + - `__add__(self, other: DifferentialPort)`: concatenates two ports together into a bigger `DifferentialPort` |
| 95 | + - `__invert__(self)`: returns a new `DifferentialPort` derived from this one by having the opposite (every element of) `invert` |
| 96 | +- `Buffer.Signature(direction: Direction | str, width: int)`: a signature for the `Buffer`; if `direction` is a string, it is converted to `Direction` |
| 97 | + - `i: Out(width)` if `direction in (Direction.Input, Direction.Bidir)` |
| 98 | + - `o: In(width)` if `direction in (Direction.Output, Direction.Bidir)` |
| 99 | + - `oe: In(1, init=1)` if `direction is Direction.Output` |
| 100 | + - `oe: In(1, init=0)` if `direction is Direction.Bidir` |
| 101 | +- `Buffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ...)`: non-registered buffer, derives from `Component` |
| 102 | + - when elaborated, tries to return `platform.get_io_buffer(self)`; if such a function doesn't exist, lowers to `IOBufferInstance` plus optional inverters |
| 103 | +- `FFBuffer.Signature(direction: Direction | str, width: int)`: a signature for the `FFBuffer` |
| 104 | + - `i: Out(width)` if `direction in (Direction.Input, Direction.Bidir)` |
| 105 | + - `o: In(width)` if `direction in (Direction.Output, Direction.Bidir)` |
| 106 | + - `oe: In(1, init=1)` if `direction is Direction.Output` |
| 107 | + - `oe: In(1, init=0)` if `direction is Direction.Bidir` |
| 108 | +- `FFBuffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ..., *, i_domain="sync", o_domain="sync")`: SDR registered buffer, derives from `Component` |
| 109 | + - when elaborated, tries to return `platform.get_io_buffer(self)`; if such a function doesn't exist, lowers to `IOBufferInstance`, plus reset-less FFs realized by `m.d[*_domain]` assignment, plus optional inverters |
| 110 | +- `DDRBuffer.Signature(direction: Direction | str, width: int)`: a signature for the `DDRBuffer` |
| 111 | + - `i: Out(ArrayLayout(width, 2))` if `direction in (Direction.Input, Direction.Bidir)` |
| 112 | + - `o: In(ArrayLayout(width, 2))` if `direction in (Direction.Output, Direction.Bidir)` |
| 113 | + - `oe: In(1, init=1)` if `direction is Direction.Output` |
| 114 | + - `oe: In(1, init=0)` if `direction is Direction.Bidir` |
| 115 | +- `DDRBuffer(direction: Direction | str, port: SingleEndedPort | DifferentialPort | ..., *, i_domain="sync", o_domain="sync")`: DDR registered buffer, derives from `Component` |
| 116 | + - when elaborated, tries to return `platform.get_io_buffer(self)`; if such a function doesn't exist, raises an error |
| 117 | + |
| 118 | +All of the above classes are fully introspectable, and the constructor arguments are accessible as read-only attributes. |
| 119 | + |
| 120 | +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: |
| 121 | + |
| 122 | +- `__len__` must provide length in bits (so that `*Buffer` can know the proper signature) |
| 123 | +- `__getitem__` which supports slices, and where plain indices return single-bit slices |
| 124 | +- `__invert__` that returns another port-like |
| 125 | +- `direction` attribute that must be a `Direction` |
| 126 | + |
| 127 | +If a platform is not used, and a `DifferentialPort` is used, a pseudo-differential port is effectively created. |
| 128 | + |
| 129 | +The `direction` argument on `*Port` can be used to restrict valid allowed buffer directions as follows: |
| 130 | + |
| 131 | +- an `Input` buffer will not accept an `Output` port |
| 132 | +- an `Output` buffer will not accept an `Input` port |
| 133 | +- a `Bidir` buffer will only accept a `Bidir` port |
| 134 | + |
| 135 | +This is validated by the `*Buffer` constructors. Custom buffer-like elaboratables that take `*Port` are likewise encouraged to perform similar checking. |
| 136 | + |
| 137 | +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 `*Buffer`s. The `lib.io.Pin` interface and its signature likewise become deprecated. |
| 138 | + |
| 139 | +## Drawbacks |
| 140 | +[drawbacks]: #drawbacks |
| 141 | + |
| 142 | +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. |
| 143 | + |
| 144 | +Using plain domains for `DDRBuffer` has the unprecedented property of triggering logic on the opposite of active edge of the domain. |
| 145 | + |
| 146 | +## Rationale and alternatives |
| 147 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 148 | + |
| 149 | +The buffers have minimal functionality on purpose, to allow them to be widely supported. In particular: |
| 150 | + |
| 151 | +- clock enables are not supported |
| 152 | +- reset is not supported |
| 153 | +- initial values are not supported |
| 154 | +- `xdr > 2` is not supported |
| 155 | + |
| 156 | +Such functionality can be provided by vendor-specific primitives. |
| 157 | + |
| 158 | +## Prior art |
| 159 | +[prior-art]: #prior-art |
| 160 | + |
| 161 | +None. |
| 162 | + |
| 163 | +## Unresolved questions |
| 164 | +[unresolved-questions]: #unresolved-questions |
| 165 | + |
| 166 | +None. |
| 167 | + |
| 168 | +## Future possibilities |
| 169 | +[future-possibilities]: #future-possibilities |
| 170 | + |
| 171 | +Vendor-specific versions of the proposed buffers can be added to the `vendor` module, allowing access to the full range of hardware functionality. |
0 commit comments