|
| 1 | +- Start Date: 2024-03-11 |
| 2 | +- RFC PR: [amaranth-lang/rfcs#53](https://github.com/amaranth-lang/rfcs/pull/53) |
| 3 | +- Amaranth Issue: [amaranth-lang/amaranth#1195](https://github.com/amaranth-lang/amaranth/issues/1195) |
| 4 | + |
| 5 | +# Low-level I/O primitives |
| 6 | + |
| 7 | +## Summary |
| 8 | +[summary]: #summary |
| 9 | + |
| 10 | +A new first-class concept is introduced to the language, I/O ports, representing top-level ports in synthesis flows. I/O ports can be obtained from the platform, or can be manually constructed by the user in raw Verilog flows that don't use a supported platform. |
| 11 | + |
| 12 | +I/O ports can be connected to `Instance` ports. This becomes the only thing that can be connected to `Instance` ports with `io` directionality. |
| 13 | + |
| 14 | +A new `IOBufferInstance` primitive is introduced that can consume an I/O port without involving vendor-specific cells. |
| 15 | + |
| 16 | +## Motivation |
| 17 | +[motivation]: #motivation |
| 18 | + |
| 19 | +The current process for creating top-level ports is rather roundabout and involves the platform calling into multiple private APIs: |
| 20 | + |
| 21 | +1. The user calls `platform.request`, which information about the requested pin, and returns an interface object. |
| 22 | + 1. A `Signal` for the raw port is created and stored on the platform, together with metadata. |
| 23 | + 2. If raw port is requested (`dir='-'`), that signal is returned (in a wrapper object). |
| 24 | + 3. Otherwise, an I/O buffer primitive is instantiated that drives the raw port, and the returned interface contains signals controlling that buffer. Depending on the platform, this could be either a vendor-specific cell (via `Instance`) or a generic tristate buffer (via `IOBufferInstance`, which is currently a private API). |
| 25 | +2. The hierarchy is elaborated via `Fragment.get`. |
| 26 | +3. Platform performs first half of design preparation via private `Fragment` APIs (domains are resolved, missing domains are created). |
| 27 | +4. Platform installs all instantiated I/O buffers into the elaborated hierarchy and gathers all top-level port signals. |
| 28 | +5. Platform finishes design preparation via more private APIs, then calls RTLIL or Verilog backend. |
| 29 | +6. Platform uses the gathered list of top-level ports to create a constraint file. |
| 30 | + |
| 31 | +If the `io` directionality is involved, the top-level port is a cursed kind of `Signal` that doesn't follow the usual rules: |
| 32 | + |
| 33 | +- it effectively has two drivers (the `Instance` or `IOBufferInstance` and the external world) |
| 34 | +- on the Amaranth side, it can only be connected to at most one `Instance` or `IOBufferInstance` (ie. it cannot be peeked at) |
| 35 | + |
| 36 | +This proposal aims to: |
| 37 | + |
| 38 | +- provide a platform-independent way to instantiate and use top-level ports |
| 39 | +- significantly reduce the amount of private APIs used in platform code |
| 40 | +- provide a stricter model of I/O ports, no longer overloading `Signal` |
| 41 | + |
| 42 | +## Scope and roadmap |
| 43 | +[scope-roadmap]: #scope-roadmap |
| 44 | + |
| 45 | +The proposal is only about low-level primitives implemented in `amaranth.hdl`. It is to be followed with: |
| 46 | + |
| 47 | +1. Another RFC proposing generic I/O buffer components with platform hooks in `lib.io`. |
| 48 | +2. Overhaul of the platform API (as of now undetermined). |
| 49 | + |
| 50 | + |
| 51 | +## Guide-level explanation |
| 52 | +[guide-level-explanation]: #guide-level-explanation |
| 53 | + |
| 54 | +### `IOPort`s and their use |
| 55 | + |
| 56 | +When creating a synthesizable top-level design with Amaranth, top-level ports are represented by the `IOPort` class. If you're not using the Amaranth platform machinery, you can instantiate a top-level port like this: |
| 57 | + |
| 58 | +```py |
| 59 | +abc = IOPort(8, name="abc") # The port is 8 bits wide. |
| 60 | +``` |
| 61 | + |
| 62 | +If a platform is in use, `IOPort`s should be requested via `platform.request` instead of created manually — the platform will associate its own metadata with ports. |
| 63 | + |
| 64 | +To actually use such a port from a design, you need to instantiate an I/O buffer: |
| 65 | + |
| 66 | +```py |
| 67 | +abc_o = Signal(8) |
| 68 | +abc_i = Signal(8) |
| 69 | +abc_oe = Signal() |
| 70 | +m.submodules += IOBufferInstance(abc, i=abc_i, o=abc_o, oe=abc_oe) |
| 71 | +# abc_o and abc_oe can now be written to drive the port, abc_i can be read to determine the state of the port. |
| 72 | +``` |
| 73 | + |
| 74 | +This automatically creates an `inout` port on the design. You can also create an `output` port by skipping the `i=` argument, or an `input` port by skipping the `o=` and `oe=` arguments. |
| 75 | + |
| 76 | +If the `o=` argument is passed, but `oe=` is not, a default of `oe=Const(1)` is assumed. |
| 77 | + |
| 78 | +Alternatively, `IOPort`s can be connected directly to `Instance` ports: |
| 79 | + |
| 80 | +```py |
| 81 | +# Equivalent to the above, using Xilinx IOBUF cells. |
| 82 | +for i in range(8): |
| 83 | + m.submodules += Instance("IOBUF", |
| 84 | + i_I=abc_o[i], |
| 85 | + i_T=~abc_oe, |
| 86 | + o_O=abc_i[i], |
| 87 | + io_IO=abc[i], |
| 88 | + ) |
| 89 | +``` |
| 90 | + |
| 91 | +Just like values, `IOPort`s can be sliced with normal Python indexing and concatenated with `Cat`. |
| 92 | + |
| 93 | +`IOPort`s can only be consumed by `IOBufferInstance` and `Instance` ports — they cannot be used as plain values. Every `IOPort` bit can be consumed at most once. |
| 94 | + |
| 95 | +Only `IOPort`s (and their slices or concatenations) can be connected to `Instance` ports with `io` directionality. Ports with `i` and `o` directionalities can be connected to both `IOPort`s and plain `Value`s. |
| 96 | + |
| 97 | +### General note on top-level ports |
| 98 | + |
| 99 | +Amaranth provides many ways of specifying top-level ports, to be used as appropriate for the design: |
| 100 | + |
| 101 | +1. For a top-level synthesizable design using a platform, ports are created by `platform.request` which either returns a raw `IOPort` or immediately wraps it in an I/O buffer. |
| 102 | +2. For a top-level synthesizable design without using a platform, `IOPort`s can be created manually as above. |
| 103 | +3. For a partial synthesizable design (to be used with eg. a Verilog top level), the top elaboratable can be a `lib.wiring.Component`, and the ports will be automatically derived from its signature + any unresolved domains. |
| 104 | +4. For a partial synthesizable design without using `lib.wiring.Component`, the list of signals to be used as top-level ports can be specified out-of-band to the backend via the `ports=` argument. |
| 105 | +5. For simulation, top-level ports are not used at all. |
| 106 | + |
| 107 | +## Reference-level explanation |
| 108 | +[reference-level-explanation]: #reference-level-explanation |
| 109 | + |
| 110 | +### `IOPort` and `IOValue` |
| 111 | + |
| 112 | +Two public classes are added to the language: |
| 113 | + |
| 114 | +- `amaranth.hdl.IOValue`: represents a top-level port, or a slice or concatenation thereof. Analogous to `Value`. No public constructor. |
| 115 | + - `__len__(self)`: returns the width of this value in bits. I/O values have no shape or signedness, only width. |
| 116 | + - `__getitem__(self, index: int | slice)`: like slicing on values, returns an `IOValue` subclass. |
| 117 | + - `metadata`: a read-only attribute returning a tuple of platform-specific objects; the tuple has one element per bit. |
| 118 | + - `cast(obj)` (class method): converts the given object to `IOValue`, or raises an exception; the only non-`IOValue` object that can be passed is a 0-length `Value`, as per below. |
| 119 | +- `amaranth.hdl.IOPort(width, *, name, attrs={}, metadata=None)`: represents a top-level port. A subclass of `IOValue`. Analogous to `Signal`. |
| 120 | + - `metadata` is an opaque field on the port that is not used in any way by the HDL core, and can be used by the platform to hold arbitrary data. It is normally used to store associated constraints to be emitted to the constraint file. The value can be either a tuple of arbitrary Python objects with length equal to `width`, or `None`, in which case an all-`None` tuple of the right width will be filled in. |
| 121 | + |
| 122 | +The `Cat` function is changed to work on `IOValue`s in addition to plain `Value`s: |
| 123 | + |
| 124 | +- all arguments to `Cat` must be of the same kind (either all `Value`s or all `IOValue`s) |
| 125 | +- the result is the same kind as the arguments |
| 126 | +- if no arguments at all are passed, the result is a `Value` |
| 127 | + |
| 128 | +When `IOValue`s are sliced, the `metadata` attribute of the slicing result is likewise sliced in the same way from the source value. The same applies for concatenations. |
| 129 | + |
| 130 | +As a special allowance to avoid problems in generated code, `Cat()` (empty concatenation, which is defined to be a `Value`) is also allowed wherever an `IOValue` is allowed. |
| 131 | + |
| 132 | +### `IOBufferInstance` |
| 133 | + |
| 134 | +A new public class is added to the language: |
| 135 | + |
| 136 | +- `amaranth.hdl.IOBufferInstance(port, *, i=None, o=None, oe=None)` |
| 137 | + |
| 138 | +The `port` argument must be an `IOValue`. |
| 139 | + |
| 140 | +The `i` argument is used for the input half of the buffer. If `None`, the buffer is output-only. Otherwise, it must be an assignable `Value` of the same width as the `port`. Like for `Instance` outputs, the allowed kinds of `Value`s are `*Signal`s and slices or concatenations thereof. |
| 141 | + |
| 142 | +The `o` argument is used for the output half of the buffer. If `None`, the buffer is input-only. Otherwise, it must be a `Value` of the same width as the `port`. |
| 143 | + |
| 144 | +The `oe` argument is the output enable. If `o` is `None`, `oe` must also be `None`. Otherwise, it must be either a 1-bit `Value` or `None` (which is equivalent to `Const(1)`). |
| 145 | + |
| 146 | +At least one of `i` or `o` must be specified. |
| 147 | + |
| 148 | +The `IOBufferInstance`s are included in the hierarchy as submodules in the same way as `Instance` and `MemoryInstance`. |
| 149 | + |
| 150 | +### `Instance` |
| 151 | + |
| 152 | +The rules for instance ports are changed as follows: |
| 153 | + |
| 154 | +1. `io` ports can only be connected to `IOValue`s. |
| 155 | +2. `o` ports can be connected to either `IOValue`s or assignable `Value`s. Like now, acceptable `Value`s are limitted to `*Signal`s and their slices and concatenations. |
| 156 | +3. `i` ports can be connected to either `IOValue`s or `Value`s. |
| 157 | + |
| 158 | +A zero-width `IOValue` or `Value` can be connected to any port regardless of direction, and such connection is ignored. |
| 159 | + |
| 160 | +### Elaboration notes |
| 161 | + |
| 162 | +Every `IOPort` used in the design will become a top-level port in the output, with the given name (subject to the usual name deduplication). |
| 163 | + |
| 164 | +If the `IOPort` is used only as an input (connected to an `Instance` port with `i` directionality, or to `IOBufferInstance` without `o=` argument), it becomes a top-level `input` port. |
| 165 | + |
| 166 | +If the `IOPort` is used only as an output (connected to an `Instance` port with `o` directionality, or to `IOBufferInstance` without `i=` argument), it becomes a top-level `output` port. |
| 167 | + |
| 168 | +Otherwise, it becomes an `inout` port. |
| 169 | + |
| 170 | +Every bit of an `IOPort` can be used (ie. connected to `IOBufferInstance` or `Instance`) at most once. |
| 171 | + |
| 172 | +After a design is elaborated, the list of all `IOPort`s used can be obtained by some mechanism out of scope of this RFC. |
| 173 | + |
| 174 | +Calling `verilog.convert` or `rtlil.convert` without `ports` becomes legal. The full logic of determining top-level ports in `convert` is as follows: |
| 175 | + |
| 176 | +- `ports is not None`: the ports include the specified signals, all `IOPort`s in the design, and clock/reset signals of all autocreated domains |
| 177 | +- `ports is None` and top-level is `Component`: the ports include the signature members, all `IOPort`s in the design, and clock/reset signals of all autocreated domains |
| 178 | +- `ports is None`, top-level is not `Component`: the ports include all `IOPort`s in the design and clock/reset signals of all autocreated domains |
| 179 | + |
| 180 | +Using `IOPort`s together with other ways of creating top-level ports is not recommended. |
| 181 | + |
| 182 | +`IOPort`s are not supported in any way in simulation. |
| 183 | + |
| 184 | +### Platform interfaces |
| 185 | + |
| 186 | +The interface objects currently returned by `platform.request(dir="-")` are changed to have an empty signature, with the `io`, `p`, `n` attributes becoming `IOPort`s. |
| 187 | + |
| 188 | +## Drawbacks |
| 189 | +[drawbacks]: #drawbacks |
| 190 | + |
| 191 | +Arguably we have too many ways of creating top-level ports. However, it's not clear if we can remove any of them. |
| 192 | + |
| 193 | +The empty `Cat()` hack is a minor type system wart. |
| 194 | + |
| 195 | +## Rationale and alternatives |
| 196 | +[rationale-and-alternatives]: #rationale-and-alternatives |
| 197 | + |
| 198 | +The change is necessary, as the I/O and platform code is a pile of hacks that needs cleanup. |
| 199 | + |
| 200 | +The core `IOPort` + `IOValue` design codifies existing validity rules for `Instance` `io` ports. The `IOBufferInstance` already exists in current Amaranth as a private API used by the platforms. |
| 201 | + |
| 202 | +The change of allowed arguments for `Instance` `io` ports is done without a deprecation period. It is felt that this will have a minimal impact, as the proposed change to `platform.request` with `dir="-"` in RFC 55 will effectively fix the breakage for most well-formed instances of `io` port usage, and such usage is not common in the first place. |
| 203 | + |
| 204 | +## Prior art |
| 205 | +[prior-art]: #prior-art |
| 206 | + |
| 207 | +`IOValue` system is patterned after the current `Value` system. |
| 208 | + |
| 209 | +The port auto-creation essentially offloads parts of the current platform functionality into Amaranth core. |
| 210 | + |
| 211 | +## Unresolved questions |
| 212 | +[unresolved-questions]: #unresolved-questions |
| 213 | + |
| 214 | +Should we forbid mixing various ways of port creation? |
| 215 | + |
| 216 | +Traditional name bikeshedding. |
| 217 | + |
| 218 | +## Future possibilities |
| 219 | +[future-possibilities]: #future-possibilities |
| 220 | + |
| 221 | +Another RFC is planned that will overhaul `lib.io`, adding generic I/O buffer components. |
0 commit comments