Skip to content

Commit e159c9a

Browse files
authored
RFC #69: Add a lib.io.PortLike object usable in simulation
2 parents a54d3a2 + d981029 commit e159c9a

File tree

1 file changed

+173
-0
lines changed

1 file changed

+173
-0
lines changed

text/0069-simulation-port.md

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
- Start Date: 2024-07-01
2+
- RFC PR: [amaranth-lang/rfcs#69](https://github.com/amaranth-lang/rfcs/pull/69)
3+
- Amaranth Issue: [amaranth-lang/amaranth#1446](https://github.com/amaranth-lang/amaranth/issues/1446)
4+
5+
# Add a `lib.io.PortLike` object usable in simulation
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
A new library I/O port object `lib.io.SimulationPort` is introduced that can be used to simulate `lib.io.Buffer` and `lib.io.FFBuffer` components.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
End-to-end verification of I/O components requires simulating `lib.io.Buffer`, `lib.io.FFBuffer`, or `lib.io.DDRBuffer` objects, which is currently not possible because all defined library I/O ports (`lib.io.SingleEndedPort`, `lib.io.DifferentialPort`) require the use of unsimulatable core I/O values. Simulating these buffers can be done by adding a new library I/O port object with separate `i`, `o`, `oe` parts represented by normal `Signal`s, and modifying the lowering of `lib.io` buffer components to drive the individual parts. Simulation models could then be written using code similar to (for SPI):
16+
17+
```python
18+
await ctx.set(cipo_port.o, cipo_value)
19+
_, copi_value = await ctx.posedge(sck_port.i).sample(copi_port.i)
20+
```
21+
22+
This functionality is one of the last pieces required to build a library of reusable I/O interfaces, and the need for it became apparent while writing reusable I/O blocks for the Glasgow project.
23+
24+
## Guide-level explanation
25+
[guide-level-explanation]: #guide-level-explanation
26+
27+
Let's consider an I/O component, such as below, which takes several ports as its constructor argument:
28+
29+
```python
30+
class SPIController(wiring.Component):
31+
o_data: stream.Signature(8)
32+
i_data: stream.Signature(8)
33+
34+
def __init__(self, sck_port, copi_port, cipo_port):
35+
self._sck_port = sck_port
36+
self._copi_port = copi_port
37+
self._cipo_port = cipo_port
38+
39+
super().__init__()
40+
41+
def elaborate(self, platform):
42+
m = Module()
43+
44+
m.submodules.sck = sck_buffer = io.FFBuffer("o", self._sck_port)
45+
m.submodules.copi = copi_buffer = io.FFBuffer("o", self._copi_port)
46+
m.submodules.cipo = cipo_buffer = io.FFBuffer("i", self._cipo_port)
47+
48+
# ... wire it up to self.[io]_data...
49+
50+
return m
51+
```
52+
53+
To simulate such a component, instantiate `lib.io.SimulationPort` objects for each of the ports, and use their `.o`, `.oe`, and `.i` signals to verify the functionality:
54+
55+
```python
56+
sck_port = io.SimulationPort(1, direction="o")
57+
copi_port = io.SimulationPort(1, direction="o")
58+
cipo_port = io.SimulationPort(1, direction="i")
59+
60+
dut = SPIController(sck_port, copi_port, cipo_port)
61+
62+
async def testbench_peripheral(ctx):
63+
"""Simulates behavior of an SPI peripheral. Simplified for clarity: ignores chip select."""
64+
for copi_fixture, cipo_fixture in zip([0,1,0,1,0,1,0,1], [1,0,1,0,1,0,1,0]):
65+
await ctx.negedge(sck_port.o)
66+
ctx.set(cipo_port.i, cipo_fixture)
67+
_, copi_driven, copi_value = await ctx.posedge(sck_port.o).sample(copi_port.oe, copi_port.o)
68+
assert copi_driven == 0b1 and copi_value == copi_fixture
69+
70+
async def testbench_controller(ctx):
71+
"""Issues commands to the controller."""
72+
await stream_put(ctx, dut.o_data, 0x55)
73+
assert (await stream_get(ctx, dut.i_data)) == 0xAA
74+
75+
sim = Simulator(dut)
76+
sim.add_clock(1e-6)
77+
sim.add_testbench(testbench_peripheral)
78+
sim.add_testbench(testbench_controller)
79+
sim.run()
80+
```
81+
82+
Note that the peripheral testbench not only checks that the right values are being output by the I/O component, but that the port is driven (not tristated) at the time when values are sampled.
83+
84+
Note also that e.g. `copi_port.oe` is a multi-bit signal, with the same width as the port itself. This is unlike the `lib.io` buffers, whose `oe` port member is always 1-bit wide. This means that for multi-wire ports that are always fully driven or fully undriven, it is necessary to compare them with `0b00..0` or `0b11..1`, according to the width of the port.
85+
86+
## Reference-level explanation
87+
[reference-level-explanation]: #reference-level-explanation
88+
89+
The `amaranth.lib.io` module is expanded with a new subclass of `PortLike`, `SimulationPort(direction, width, *, invert=False)`:
90+
- `i`, `o`, `oe` are read-only properties each containing a new `Signal(width)`;
91+
- `invert` is a property equal to the `invert` parameter normalized to a tuple of `width` elements;
92+
- `direction` is a property equal to the `direction` parameter normalized to `Direction` enum;
93+
- `len(port)` returns `width`;
94+
- `port[...]` returns a new `SimulationPort` whose `i`, `o`, `oe` properties are slices of `port.i`, `port.o`, `port.oe` respectively;
95+
- `~port` returns a new `SimulationPort` whose `i`, `o`, `oe` properties are the same as for `port` and the `invert` property has each value inverted;
96+
- `port + other`, where `other` is a `SimulationPort`, returns a new `SimulationPort` whose `i`, `o`, `oe` properties are concatenations of the respective properties of `port` and `other`.
97+
98+
Since this is a third `PortLike` with a compatible `__add__` implementation, and it is weird to have slicing but not concatenation, the `__add__` method is added to the `PortLike` signature, with a deprecation warning in 0.5.1 and a hard requirement in 0.6.0.
99+
100+
The `amaranth.lib.io.Buffer` component is changed to accept `SimulationPort`s and performs the following connections (with the direction checks omitted for clarity):
101+
102+
```python
103+
m.d.comb += [
104+
buffer.i.eq(Cat(Mux(port.oe, o, i) for o, i in zip(port.o, port.i))),
105+
port.o.eq(buffer.o),
106+
port.oe.eq(buffer.oe.replicate(len(port))),
107+
]
108+
```
109+
110+
The `amaranth.lib.io.FFBuffer` component is changed to accept `SimulationPort`s and performs the following connections (with the direction checks omitted for clarity):
111+
112+
```python
113+
m.d[buffer.i_domain] += [
114+
buffer.i.eq(Cat(Mux(port.oe, o, i) for o, i in zip(port.o, port.i))),
115+
]
116+
m.d[buffer.o_domain] += [
117+
port.o.eq(buffer.o),
118+
port.oe.eq(buffer.oe.replicate(len(port))),
119+
]
120+
```
121+
122+
The `amaranth.lib.io.DDRBuffer` component is not changed.
123+
124+
None of the `get_io_buffer` functions in the vendor platforms are changed. Thus, `SimulationPort` is not usable with any vendor platform that implements `get_io_buffer`.
125+
126+
To improve the health of the Amaranth standard I/O library (which will make heavy use of simulation port objects), the changes are back-ported to the 0.5.x release branch.
127+
128+
## Drawbacks
129+
[drawbacks]: #drawbacks
130+
131+
* Bigger API surface.
132+
* Another concept to keep track of.
133+
134+
## Rationale and alternatives
135+
[rationale-and-alternatives]: #rationale-and-alternatives
136+
137+
Alternatives:
138+
139+
* The signature of the `SimulationPort()` constructor is designed to closely resemble that of `SingleEndedPort()` and `DifferentialPort()`. However, `SimulationPort()` is intended to be only constructed by the designer, and the latter two almost never are. Is it a good idea to make direction optional, in this case? Specifying the direction provides additional checks that are valuable for verification.
140+
* Alternative #0: `SimulationPort(width, *, invert=False, direction=Direction.Bidir)`. This alternative is a near-exact match to the other port-like objects, with the exception of `io` replaced by `width`.
141+
* Alternative #1: `SimulationPort(width, *, invert=False, direction)`. This alternative minimally changes the signature, but requires lengthy `direction=` at every call site. **Currently in the RFC text.**
142+
* Alternative #2: `SimulationPort(width, direction, *, invert=False)`. This alternative is a slightly less minimal change, but does not match the ordering of arguments of signature of `Buffer(direction, port)`.
143+
* Alternative #3: `SimulationPort(direction, width, *, invert=False)`. This alternative differs significantly from `SingleEndedPort()` etc, but matches `Buffer()` most closely.
144+
* It would be possible to make `SimulationPort` an interface object. Although the opportunities for connecting it are limited (since the signals are only synchronized to each other and not to any clock domains in the design), one can foresee the addition of a component that implements end-to-end or multi-drop buses using simulation-compatible logic, for example, to simulate multiple Amaranth components accessing a common SPI or I2C bus. In this context `connect` would be valuable.
145+
* This functionality could be prototyped out-of-tree (by wrapping `SimulationPort` or simply setting its `signature` property) and added upstream later if it proves to be useful.
146+
* It would be possible to add `o_init`, `i_init`, `oe_init` to the signature for a minor usability improvement in cases where the port is never driven in simulation, but the benefit seems low and the signature becomes much more cluttered.
147+
* It is no longer possible since Amaranth 0.5 to directly set `.init` attribute of signals, so without these arguments the initial value will always be 0.
148+
149+
Rejected alternatives:
150+
* ~~Multi-bit `oe` signal is a radical departure from the rest of Amaranth, where `oe` is always single-bit. However, note the proposed RFC #68, which suggests making `oe` optionally multi-bit for `io.Buffer` etc.~~
151+
* ~~It would be possible to make `oe` a single-bit signal and to forbid slicing of `SimulationPort` objects. However, the `PortLike` interface does not currently allow this, and it would have to be changed, with generic code having to be aware of this new error.~~
152+
* ~~It would also be possible to make `oe` a choice of multi-bit or single-bit signal during construction, similar to RFC #68.~~
153+
* Without multibit `oe` it is not possible to concatenate ports.
154+
155+
## Prior art
156+
[prior-art]: #prior-art
157+
158+
We have previously had, and deprecated, the `amaranth.lib.io.Pin` object, which is essentially the same triple of `i`, `o`, `oe`. However, in terms of its intended use (notwithstanding how it was actually used), the `Pin` object was closest to the interface created by `io.Buffer.Signature.create()`. The newly introduced `SimulationPort` is intended to represent data on the other side of the buffer than `Pin` does.
159+
160+
## Resolved questions
161+
[resolved-questions]: #resolved-questions
162+
163+
- ~~What should the signature of `SimulationPort()` be?~~
164+
- Alternative #3 (matching `io.Buffer`, etc) was decided.
165+
166+
## Future possibilities
167+
[future-possibilities]: #future-possibilities
168+
169+
- Add `DDRBuffer` support by using `port.o.eq(Mux(ClockSignal(), self.o[1], self.o[0]))`.
170+
- It is not yet clear if this can be safely used, or should be used, in simulations.
171+
- Make `SimulationPort` an interface object.
172+
- Add a `EndToEndBus` (naming TBD) class connecting pairs of ports to each other and ensuring that no wire is driven twice.
173+
- Add a `OpenDrainBus` (naming TBD) class connecting a set of ports to each other and providing a "pull-up resistor" equivalent functionality.

0 commit comments

Comments
 (0)