Skip to content

Commit a846faf

Browse files
committed
Add an RFC for introducing the concept of testbench processes to the simulator.
1 parent 4edf8cc commit a846faf

File tree

1 file changed

+119
-0
lines changed

1 file changed

+119
-0
lines changed

text/0000-simulator-testbenches.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
- Start Date: (fill me in with today's date, YYYY-MM-DD)
2+
- RFC PR: [amaranth-lang/rfcs#0000](https://github.com/amaranth-lang/rfcs/pull/0000)
3+
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)
4+
5+
# Testbench processes for the simulator
6+
7+
## Summary
8+
[summary]: #summary
9+
10+
The existing `Simulator.add_sync_process` method causes the process function to observe the design in a state before combinational settling, something that is actively unhelpful in testbenches. A new `Simulator.add_testbench` method will only return control to the process function after combinational settling.
11+
12+
## Motivation
13+
[motivation]: #motivation
14+
15+
Consider the following code:
16+
17+
```python
18+
from amaranth import *
19+
from amaranth.sim import Simulator
20+
21+
22+
class DUT(Elaboratable):
23+
def __init__(self):
24+
self.out = Signal()
25+
self.outn = Signal()
26+
27+
def elaborate(self, platform):
28+
m = Module()
29+
m.d.sync += self.outn.eq(~self.out)
30+
return m
31+
32+
33+
dut = DUT()
34+
def testbench():
35+
yield dut.out.eq(1)
36+
yield
37+
print((yield dut.out))
38+
print((yield dut.outn))
39+
40+
sim = Simulator(dut)
41+
sim.add_clock(1e-6)
42+
sim.add_sync_process(testbench)
43+
sim.run()
44+
```
45+
46+
This code prints:
47+
48+
```
49+
1
50+
1
51+
```
52+
53+
While this result is sensible in a behavioral implementation of an elaboratable (where observing the state of the outputs of combinational cells before they transition to the new state is required for such an implementation to function as a drop-in replacement for a register transfer level one), it is not something a testbench should ever print; it clearly contradicts the netlist. Because there are no alternatives to using `add_sync_process`, testbenches (where such result is completely inappropriate) keep using it, and Amaranth designers are left to sprinkle `yield` over the testbenches until the result works.
54+
55+
In addition to the direct impact of this issue, it also prevents building reusable abstractions, including something as simple as `yield from fifo.read()`, since in order to work for back-to-back reads that would first have to `yield Settle()` to observe the updated value of `fifo.r_rdy`, which isn't appropriate for a function in the standard library as it changes the observable behavior (and thus breaks the abstraction).
56+
57+
## Guide-level explanation
58+
[guide-level-explanation]: #guide-level-explanation
59+
60+
The code example above is rewritten as:
61+
62+
```python
63+
dut = DUT()
64+
def testbench():
65+
yield dut.out.eq(1)
66+
yield
67+
print((yield dut.out))
68+
print((yield dut.outn))
69+
70+
sim = Simulator(dut)
71+
sim.add_clock(1e-6)
72+
sim.add_testbench(testbench, domain="sync")
73+
sim.run()
74+
```
75+
76+
When run, it prints:
77+
78+
```
79+
1
80+
0
81+
```
82+
83+
Existing testbenches can be ported to use `Simulator.add_testbench` by removing extraneous `yield` or `yield Settle()` calls.
84+
85+
Reusable abstractions can be built by defining generator functions on interfaces or components.
86+
87+
## Reference-level explanation
88+
[reference-level-explanation]: #reference-level-explanation
89+
90+
A new `Simulator.add_testbench(process, *, domain=None)` is added. This function schedules `process` similarly to `add_process`, except that before returning control to the coroutine `process` it performs the equivalent of `yield Settle()`. If `domain` is not `None`, then calling `yield` within `add_testbench` performs the equivalent of `yield Tick(domain)`.
91+
92+
`Settle` is deprecated and removed in a future version.
93+
94+
## Drawbacks
95+
[drawbacks]: #drawbacks
96+
97+
Increase in API surface area and complexity. Churn.
98+
99+
## Rationale and alternatives
100+
[rationale-and-alternatives]: #rationale-and-alternatives
101+
102+
The motivating issue has no known alternative resolution besides introducing this (or a very similar) API. The status quo has proved deeply unsatisfactory over many years, and the `add_testbench` process has been trialed in 2020 and found usable.
103+
104+
The `domain` argument of `add_testbench` could default to "sync", as for `add_sync_process`. Since a testbench does not inherently have a "default" domain (unlike a behavioral replacement for a register transfer level module, where `sync` is the default), this does not appear appropriate.
105+
106+
## Prior art
107+
[prior-art]: #prior-art
108+
109+
Other simulators experience similar challenges with event scheduling. In Verilog, this is one of the reasons for the use of blocking assignment `=`. Where the decision of the scheduling primitive is left to the point of use (rather than the point of declaration, as proposed in this RFC) it leads to complexity in teaching the concept.
110+
111+
## Unresolved questions
112+
[unresolved-questions]: #unresolved-questions
113+
114+
None.
115+
116+
## Future possibilities
117+
[future-possibilities]: #future-possibilities
118+
119+
In the standard library, `fifo.read()` and `fifo.write()` functions could be defined that aid in testing designs with FIFOs. Such functions will only work correctly within testbench processes.

0 commit comments

Comments
 (0)