Skip to content

Commit f9fe446

Browse files
committed
RFC #36: More clarifications. Split trigger objects into domain trigger objects and combinable trigger objects.
1 parent ae7edf3 commit f9fe446

File tree

1 file changed

+68
-42
lines changed

1 file changed

+68
-42
lines changed

text/0036-async-testbench-functions.md

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,29 +22,28 @@ Passing a simulator context to the testbench function provides a convenient plac
2222
[guide-level-explanation]: #guide-level-explanation
2323

2424
As an example, let's consider a simple stream interface with `valid`, `ready` and `data` members.
25-
On the interface class, we can then implement `.send()` and `.recv()` methods like this:
25+
We can then implement `stream_send()` and `stream_recv()` functions like this:
2626

2727
```python
28-
class StreamInterface(PureInterface):
29-
async def recv(self, sim):
30-
await sim.set(self.ready, 1)
31-
await sim.tick().until(self.valid)
28+
async def stream_recv(sim, stream):
29+
await sim.set(stream.ready, 1)
30+
await sim.tick().until(stream.valid)
3231

33-
value = await sim.get(self.data)
32+
value = await sim.get(stream.data)
3433

35-
await sim.tick()
36-
await sim.set(self.ready, 0)
34+
await sim.tick()
35+
await sim.set(stream.ready, 0)
3736

38-
return value
37+
return value
3938

40-
async def send(self, sim, value):
41-
await sim.set(self.data, value)
39+
async def stream_send(sim, stream, value):
40+
await sim.set(stream.data, value)
4241

43-
await sim.set(self.valid, 1)
44-
await sim.tick().until(self.ready)
42+
await sim.set(stream.valid, 1)
43+
await sim.tick().until(stream.ready)
4544

46-
await sim.tick()
47-
await sim.set(self.valid, 0)
45+
await sim.tick()
46+
await sim.set(stream.valid, 0)
4847
```
4948

5049
`await sim.get()` and `await sim.set()` replaces the existing operations `yield signal` and `yield signal.eq()` respectively.
@@ -68,8 +67,8 @@ A testbench could then look like this:
6867
```python
6968
async def test_rgb(sim, r, g, b):
7069
rgb = {'r': r, 'g': g, 'b': b}
71-
await dut.input.send(sim, rgb)
72-
yuv = await dut.output.recv(sim)
70+
await stream_send(sim, dut.input, rgb)
71+
yuv = await stream_recv(sim, dut.output)
7372

7473
print(rgb, yuv)
7574

@@ -81,11 +80,14 @@ async def testbench(sim):
8180
await test_rgb(sim, 255, 255, 255)
8281
```
8382

84-
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.
83+
Since `stream_send()` and `stream_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.
8584

8685
`Tick()` and `Delay()` are replaced by `sim.tick()` and `sim.delay()` respectively.
8786
In addition, `sim.changed()` and `sim.edge()` is introduced that allows creating triggers from arbitrary signals.
88-
These all return a trigger object that can be made conditional through `.until()`.
87+
88+
`sim.tick()` return a domain trigger object that can be made conditional through `.until()` or repeated through `.repeat()`.
89+
90+
`sim.delay()`, `sim.changed()` and `sim.edge()` return a combinable trigger object that can be used to add additional triggers.
8991

9092
`Active()` and `Passive()` are replaced by an `background=False` keyword argument to `.add_testbench()`.
9193
Processes created through `.add_process()` are always created as background processes.
@@ -103,7 +105,7 @@ async def packet_reader(sim, stream):
103105
print('Received packet:', packet.hex(' '))
104106
```
105107

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.
108+
When a combinable 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.
107109
Multiple triggers can be combined.
108110
Consider the following examples:
109111

@@ -147,62 +149,86 @@ The following `Simulator` methods have their signatures updated:
147149
* `add_process(process)`
148150
* `add_testbench(process, *, background=False)`
149151

150-
The new optional named argument `background` registers the testbench as a background process when true.
151-
152152
Both methods are updated to accept an async function passed as `process`.
153153
The async function must accept an argument `sim`, which will be passed a simulator context.
154154
(Argument name is just convention, will be passed positionally.)
155155

156-
The simulator context have the following methods:
156+
The new optional named argument `background` registers the testbench as a background process when true.
157+
Processes created through `add_process` are always registered as background processes (except when registering legacy non-async generator functions).
158+
159+
The simulator context has the following properties and methods:
160+
- `process_type`
161+
- Property, `ProcessType`, indicates the type of the current process.
162+
This property allows a generic simulation helper function to assert that it's being run under the appropriate process type.
157163
- `get(expr: Value) -> int`
158164
- `get(expr: ValueCastable) -> any`
159165
- Returns the value of `expr` when awaited.
160-
When `expr` is a value-castable, the value will be converted through `.from_bits()`.
166+
When `expr` is a value-castable, and its `shape()` is a `ShapeCastable`, the value will be converted through the shape's `.from_bits()`.
167+
Otherwise, a plain integer is returned.
161168
- `set(expr: Value, value: ConstLike)`
162169
- `set(expr: ValueCastable, value: any)`
163170
- 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: int)`
171+
When `expr` is a value-castable, and its `shape()` is a `ShapeCastable`, the value will be converted through the shape's `.const()`.
172+
Otherwise, it must be a const-castable `ValueLike`.
173+
- `memory_read(instance: MemoryIdentity, address: int)`
166174
- Read the value from `address` in `instance` when awaited.
167-
- `memory_write(instance: MemoryInstance, address: int, value: int, mask:int = None)`
175+
- `memory_write(instance: MemoryIdentity, address: int, value: int, mask:int = None)`
168176
- Write `value` to `address` in `instance` when awaited. If `mask` is given, only the corresponding bits are written.
169-
- `delay(interval: float)`
170-
- Create a trigger object for advancing simulation by `interval` seconds.
177+
Like `MemoryInstance`, these two functions are an internal interface that will be usually only used via `lib.Memory`.
178+
It comes without a stability guarantee.
171179
- `tick(domain="sync", *, context=None)`
172-
- Create a trigger object for advancing simulation until the next active edge of the `domain` clock.
180+
- Create a domain trigger object for advancing simulation until the next active edge of the `domain` clock.
173181
When an elaboratable is passed to `context`, `domain` will be resolved from its perspective.
174182
- If `domain` is asynchronously reset while this is being awaited, `amaranth.sim.AsyncReset` is raised.
183+
- `delay(interval: float)`
184+
- Create a combinable trigger object for advancing simulation by `interval` seconds.
175185
- `changed(*signals)`
176-
- Create a trigger object for advancing simulation until any signal in `signals` changes.
186+
- Create a combinable trigger object for advancing simulation until any signal in `signals` changes.
177187
- `edge(signal, value)`
178-
- Create a trigger object for advancing simulation until `signal` is changed to `value`.
188+
- Create a combinable trigger object for advancing simulation until `signal` is changed to `value`.
179189
`signal` must be a 1-bit signal or a 1-bit slice of a signal.
190+
`value` is a boolean where true indicates rising edge and false indicates falling edge.
180191
- `posedge(signal)`
181192
- `negedge(signal)`
182193
- Aliases for `edge(signal, 1)` and `edge(signal, 0)` respectively.
183194
- `critical()`
184195
- Context manager.
185196
If the current process is a background process, `async with sim.critical():` makes it a non-background process for the duration of the statement.
186197

187-
A trigger object has the following methods:
198+
The `ProcessType` enum has the following members:
199+
- `BikeshedProcess`
200+
- The current process is a process added by `add_process`.
201+
- `BikeshedTestbench`
202+
- The current process is a testbench process added by `add_testbench`.
203+
204+
A domain trigger object has the following methods:
205+
- `__await__()`
206+
- Advance simulation. No value is returned.
207+
- `until(condition)`
208+
- Repeat the trigger until `condition` is true.
209+
`condition` is an arbitrary Amaranth expression.
210+
If `condition` is initially true, `await` will return immediately without advancing simulation.
211+
The return value is an unspecified awaitable with `await` as the only defined operation.
212+
- `repeat(times: int)`
213+
- Repeat the trigger `times` times.
214+
The return value is an unspecified awaitable with `await` as the only defined operation.
215+
216+
A combinable trigger object has the following methods:
188217
- `__await__()`
189218
- Advance simulation and return the value(s) of the trigger(s).
190-
- `delay`, `tick` and `edge` triggers return `True` when they are hit, otherwise `False`.
219+
- `delay` and `edge` triggers return `True` when they are hit, otherwise `False`.
191220
- `changed` triggers return the current value of the signals they are monitoring.
221+
- At least one of the triggers hit will be reflected in the return value.
222+
In case of multiple triggers occuring at the same time step, it is unspecified which of these will show up in the return value beyond “at least one”.
192223
- `__aiter__()`
193224
- Return an async generator that is equivalent to repeatedly awaiting the trigger object in an infinite loop.
194225
- `delay(interval: float)`
195-
- `tick(domain="sync", *, context=None)`
196226
- `changed(*signals)`
197227
- `edge(signal, value)`
198228
- `posedge(signal)`
199229
- `negedge(signal)`
200230
- Create a new trigger object by copying the current object and appending another trigger.
201-
- `until(condition)`
202-
- Repeat the trigger until `condition` is true.
203-
`condition` is an arbitrary Amaranth expression.
204-
If `condition` is initially true, `await` will return immediately without advancing simulation.
205-
The return value is an unspecified awaitable with `await` as the only defined operation.
231+
- Awaiting the returned trigger object pauses the process until the first of the combined triggers hit, i.e. the triggers are combined using OR semantics.
206232

207233
`Tick()`, `Delay()`, `Active()` and `Passive()` as well as the ability to pass generator coroutines as `process` are deprecated and removed in a future version.
208234

@@ -231,6 +257,6 @@ Other python libraries like [cocotb](https://docs.cocotb.org/en/stable/coroutine
231257
## Future possibilities
232258
[future-possibilities]: #future-possibilities
233259

234-
- Add simulation helpers in the manner of `.send()` and `.recv()` to standard interfaces where it makes sense.
260+
- Add simulation helper methods to standard interfaces where it makes sense.
261+
- This includes `lib.memory.Memory`.
235262
- 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.
236-
- 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)