Skip to content

Commit 1510920

Browse files
authored
RFC #55: New lib.io components.
2 parents 699bf4d + 090b6bc commit 1510920

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

text/0055-lib-io.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)