Skip to content

Commit ea26684

Browse files
committed
RFC #36: Updated to address more feedback.
1 parent 54e6a60 commit ea26684

File tree

1 file changed

+94
-40
lines changed

1 file changed

+94
-40
lines changed

text/0036-async-testbench-functions.md

Lines changed: 94 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ A more expressive way to specify trigger/wait conditions allows the condition ch
1818

1919
Passing a simulator context to the testbench function provides a convenient place to gather all simulator operations.
2020

21-
~~Having `.get()` and `.set()` methods provides a convenient way for value castables to implement these in a type-specific manner.~~
22-
2321
## Guide-level explanation
2422
[guide-level-explanation]: #guide-level-explanation
2523

@@ -53,6 +51,10 @@ class StreamInterface(PureInterface):
5351

5452
`sim.tick()` replaces the existing `Tick()`. It returns a trigger object that either can be awaited directly, or made conditional through `.until()`.
5553

54+
> **Note**
55+
> This simplified example does not include any way of specifying the clock domain of the interface and as such is only directly applicable to single domain simulations.
56+
> A way to attach clock domain information to interfaces is desireable, but out of scope for this RFC.
57+
5658
Using this stream interface, let's consider a colorspace converter accepting a stream of RGB values and outputting a stream of YUV values:
5759

5860
```python
@@ -82,70 +84,131 @@ async def testbench(sim):
8284
Since `.send()` and `.recv()` invokes `sim.get()` and `sim.set()` that in turn will invoke the appropriate value conversions for a value castable (here `data.View`), it is general enough to work for streams with arbitrary shapes.
8385

8486
`Tick()` and `Delay()` are replaced by `sim.tick()` and `sim.delay()` respectively.
85-
In addition, `sim.changed()` is introduced that allows creating triggers from arbitrary signals.
87+
In addition, `sim.changed()` and `sim.edge()` is introduced that allows creating triggers from arbitrary signals.
8688
These all return a trigger object that can be made conditional through `.until()`.
8789

88-
`Active()` and `Passive()` are replaced by an `passive=False` keyword argument to `.add_process()` and `.add_testbench()`.
89-
To mark a passive testbench temporarily active, `sim.active()` is introduced, which is used as a context manager:
90+
`Active()` and `Passive()` are replaced by an `background=False` keyword argument to `.add_testbench()`.
91+
Processes created through `.add_process()` are always created as background processes.
92+
To allow a background process to ensure an operation is finished before end of simulation, `sim.critical()` is introduced, which is used as a context manager:
9093

9194
```python
9295
async def packet_reader(sim, stream):
9396
while True
9497
# Wait until stream has valid data.
9598
await sim.tick().until(stream.valid)
9699

97-
# Go active to ensure simulation doesn't end in the middle of a packet.
98-
async with sim.active():
100+
# Ensure simulation doesn't end in the middle of a packet.
101+
async with sim.critical():
99102
packet = await stream.read_packet()
100103
print('Received packet:', packet.hex(' '))
101104
```
102105

106+
When a trigger object is awaited, it'll return the value(s) of the trigger(s), and it can also be used as an async generator to repeatedly await the same trigger.
107+
Multiple triggers can be combined.
108+
Consider the following examples:
109+
110+
Combinational adder as a process:
111+
```python
112+
a = Signal(); b = Signal(); o = Signal()
113+
async def adder(sim):
114+
async for a_val, b_val in sim.changed(a, b):
115+
await sim.set(o, a_val + b_val)
116+
sim.add_process(adder)
117+
```
118+
119+
DDR IO buffer as a process:
120+
```python
121+
o = Signal(2); pin = Signal()
122+
async def ddr_buffer(sim):
123+
while True: # could be extended to pre-capture next `o` on posedge
124+
await sim.negedge()
125+
await sim.set(pin, o[0])
126+
await sim.posedge()
127+
await sim.set(pin, o[1])
128+
sim.add_process(ddr_buffer)
129+
```
130+
131+
Flop with configurable edge reset and posedge clock as a process:
132+
```python
133+
clk = Signal(); rst = Signal(); d = Signal(); q = Signal()
134+
def dff(rst_edge):
135+
async def process(sim):
136+
async for clk_val, rst_val in sim.posedge(clk).edge(rst, rst_edge):
137+
await sim.set(q, 0 if rst_val == rst_edge else await sim.get(d))
138+
return process
139+
sim.add_process(dff(rst_edge=0))
140+
```
141+
103142
## Reference-level explanation
104143
[reference-level-explanation]: #reference-level-explanation
105144

106145
The following `Simulator` methods have their signatures updated:
107146

108-
* `add_process(process, *, passive=False)`
109-
* `add_testbench(process, *, passive=False)`
147+
* `add_process(process)`
148+
* `add_testbench(process, *, background=False)`
110149

111-
The new optional named argument `passive` registers the testbench as passive when true.
150+
The new optional named argument `background` registers the testbench as a background process when true.
112151

113152
Both methods are updated to accept an async function passed as `process`.
114-
The async function must accept a named argument `sim`, which will be passed a simulator context.
153+
The async function must accept an argument `sim`, which will be passed a simulator context.
154+
(Argument name is just convention, will be passed positionally.)
115155

116156
The simulator context have the following methods:
117-
- `get(signal)`
118-
- Returns the value of `signal` when awaited.
119-
When `signal` is a value-castable, the value will be converted through `.from_bits()`. (Pending RFC #51)
120-
- `set(signal, value)`
121-
- Set `signal` to `value` when awaited.
122-
When `signal` is a value-castable, the value will be converted through `.const()`.
157+
- `get(expr: Value) -> int`
158+
- `get(expr: ValueCastable) -> any`
159+
- Returns the value of `expr` when awaited.
160+
When `expr` is a value-castable, the value will be converted through `.from_bits()`.
161+
- `set(expr: Value, value: ConstLike)`
162+
- `set(expr: ValueCastable, value: any)`
163+
- Set `expr` to `value` when awaited.
164+
When `expr` is a value-castable, the value will be converted through `.const()`.
165+
- `memory_read(instance: MemoryInstance, address)`
166+
- Read the value from `address` in `instance` when awaited.
167+
- `memory_write(instance: MemoryInstance, address, value, mask=None)`
168+
- Write `value` to `address` in `instance` when awaited. If `mask` is given, only the corresponding bits are written.
123169
- `delay(interval)`
124-
- Return a trigger object for advancing simulation by `interval` seconds.
170+
- Create a trigger object for advancing simulation by `interval` seconds.
125171
- `tick(domain="sync", *, context=None)`
126-
- Return a trigger object for advancing simulation by one tick of `domain`.
172+
- Create a trigger object for advancing simulation by one tick of `domain`.
127173
When an elaboratable is passed to `context`, `domain` will be resolved from its perspective.
128-
- `changed(signal, value=None)`
129-
- Return a trigger object for advancing simulation until `signal` is changed to `value`. `None` is a wildcard and will trigger on any change.
130-
- `active()`
131-
- Return a context manager that temporarily marks the testbench as active for the duration.
132-
- `time()`
133-
- Return the current simulation time.
174+
- If `domain` is asynchronously reset while this is being awaited, `AsyncReset` is raised.
175+
- `changed(*signals)`
176+
- Create a trigger object for advancing simulation until any signal in `signals` changes.
177+
- `edge(signal, value)`
178+
- Create a trigger object for advancing simulation until `signal` is changed to `value`.
179+
`signal` must be a 1-bit signal or a 1-bit slice of a signal.
180+
- `posedge(signal)`
181+
- `negedge(signal)`
182+
- Aliases for `edge(signal, 1)` and `edge(signal, 0)` respectively.
183+
184+
`signal` is changed to `value`. `None` is a wildcard and will trigger on any change.
185+
- `critical()`
186+
- Return a context manager that ensures simulation won't terminate in the middle of the enclosed scope.
134187

135188
A trigger object has the following methods:
189+
- `__await__()`
190+
- Advance simulation and return the value(s) of the trigger(s).
191+
- `delay` and `tick` triggers return `True` when they are hit, otherwise `False`.
192+
- `changed` and `edge` triggers return the current value of the signals they are monitoring.
193+
- `__aiter__()`
194+
- Return an async generator that repeatedly invokes `__await__()` and yields the returned values.
195+
- `delay(interval)`
196+
- `tick(domain="sync", *, context=None)`
197+
- `changed(*signals)`
198+
- `edge(signal, value)`
199+
- `posedge(signal)`
200+
- `negedge(signal)`
201+
- Create a new trigger object by copying the current object and appending another trigger.
136202
- `until(condition)`
137203
- Repeat the trigger until `condition` is true.
138204
`condition` is an arbitrary Amaranth expression.
139205
If `condition` is initially true, `await` will return immediately without advancing simulation.
140206

141-
~~`Value`, `data.View` and `enum.EnumView` have `.get()` and `.set()` methods added.~~
142-
143207
`Tick()`, `Delay()`, `Active()` and `Passive()` as well as the ability to pass generator coroutines as `process` are deprecated and removed in a future version.
144208

145209
## Drawbacks
146210
[drawbacks]: #drawbacks
147211

148-
- ~~Reserves two new names on `Value` and value castables~~
149212
- Increase in API surface area and complexity.
150213
- Churn.
151214

@@ -154,8 +217,6 @@ A trigger object has the following methods:
154217

155218
- Do nothing. Keep the existing interface, add `Changed()` alongside `Delay()` and `Tick()`, use `yield from` when calling functions.
156219

157-
- ~~Don't introduce `.get()` and `.set()`. Instead require a value castable and the return value of its `.eq()` to be awaitable so `await value` and `await value.eq(foo)` is possible.~~
158-
159220
## Prior art
160221
[prior-art]: #prior-art
161222

@@ -164,18 +225,11 @@ Other python libraries like [cocotb](https://docs.cocotb.org/en/stable/coroutine
164225
## Unresolved questions
165226
[unresolved-questions]: #unresolved-questions
166227

167-
- It should be possible to combine triggers, e.g. when we have a set of signals and are waiting for either of them to change.
168-
Simulating combinational logic with `add_process` would be one use case for this.
169-
Simulating sync logic with async reset could be another.
170-
What would be a good syntax to combine triggers?
171-
- Is there any other functionality that's natural to have on the simulator context?
172-
- (@wanda-phi) `sim.memory_read(memory, address)`, `sim.memory_write(memory, address, value[, mask])`?
173-
- Is there any other functionality that's natural to have on the trigger object?
174-
- Maybe a way to skip a given number of triggers? We still lack a way to say «advance by n cycles».
175228
- Bikeshed all the names.
176-
- (@whitequark) We should consider different naming for `active`/`passive`.
177229

178230
## Future possibilities
179231
[future-possibilities]: #future-possibilities
180232

181-
Add simulation helpers in the manner of `.send()` and `.recv()` to standard interfaces where it makes sense.
233+
- Add simulation helpers in the manner of `.send()` and `.recv()` to standard interfaces where it makes sense.
234+
- There is a desire for a `sim.time()` method that returns the current simulation time, but it needs a suitable return type to represent seconds with femtosecond resolution and that is out of the scope for this RFC.
235+
- We ought to have a way to skip a given number of triggers, so that we can tell the simulation engine to e.g. «advance by n cycles».

0 commit comments

Comments
 (0)