Skip to content

Commit f48554e

Browse files
committed
RFC #27: Update to address feedback.
1 parent c56b613 commit f48554e

File tree

1 file changed

+121
-10
lines changed

1 file changed

+121
-10
lines changed

text/0000-simulator-testbenches.md

Lines changed: 121 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
- 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)
2+
- RFC PR: [amaranth-lang/rfcs#27](https://github.com/amaranth-lang/rfcs/pull/27)
33
- Amaranth Issue: [amaranth-lang/amaranth#0000](https://github.com/amaranth-lang/amaranth/issues/0000)
44

55
# Testbench processes for the simulator
@@ -63,13 +63,13 @@ The code example above is rewritten as:
6363
dut = DUT()
6464
def testbench():
6565
yield dut.out.eq(1)
66-
yield
66+
yield Tick()
6767
print((yield dut.out))
6868
print((yield dut.outn))
6969

7070
sim = Simulator(dut)
7171
sim.add_clock(1e-6)
72-
sim.add_testbench(testbench, domain="sync")
72+
sim.add_testbench(testbench)
7373
sim.run()
7474
```
7575

@@ -80,29 +80,134 @@ When run, it prints:
8080
0
8181
```
8282

83-
Existing testbenches can be ported to use `Simulator.add_testbench` by removing extraneous `yield` or `yield Settle()` calls.
83+
Existing testbenches can be ported to use `Simulator.add_testbench` by removing extraneous `yield` or `yield Settle()` calls (and, in some cases, shifting other `yield` calls around).
8484

8585
Reusable abstractions can be built by defining generator functions on interfaces or components.
8686

87+
### Guidance on simulator modalities
88+
89+
There are two main simulator modalities: `add_testbench` and `add_sync_process`. They have completely disjoint purposes:
90+
91+
- `add_testbench` is used for testing logic (asynchronous or synchronous). It is not used for behavioral replacement of synchronous logic.
92+
- `add_sync_process` is used for behavioral replacement of synchronous logic. It is not for testing logic (except for legacy code), and a deprecation warning is shown when `yield Settle()` is executed in such a process.
93+
94+
Example of using `add_testbench` to test combinatorial logic:
95+
96+
```python
97+
m = Module()
98+
m.d.comb += a.eq(~b)
99+
100+
def testbench():
101+
yield b.eq(1)
102+
print((yield a)) # => 0
103+
104+
sim = Simulator(m)
105+
# No clock is required
106+
sim.add_testbench(testbench)
107+
sim.run()
108+
```
109+
110+
Example of using `add_testbench` to test synchronous logic:
111+
112+
```python
113+
m = Module()
114+
m.d.sync += a.eq(~b)
115+
116+
def testbench():
117+
yield b.eq(1)
118+
yield Tick() # same as Tick("sync")
119+
print((yield a)) # => 0
120+
121+
sim = Simulator(m)
122+
sim.add_clock(1e-6)
123+
sim.add_testbench(testbench)
124+
sim.run()
125+
```
126+
127+
Example of using `add_sync_process` to replace the flop above, and `add_testbench` to test the flop:
128+
129+
```python
130+
m = Module()
131+
132+
def flop():
133+
while True:
134+
yield b.eq(~(yield a))
135+
yield Tick()
136+
137+
def testbench():
138+
yield b.eq(1)
139+
yield Tick() # same as Tick("sync")
140+
print((yield a)) # => 0
141+
142+
sim = Simulator(m)
143+
sim.add_clock(1e-6)
144+
sim.add_sync_process(flop)
145+
sim.add_testbench(testbench)
146+
sim.run()
147+
```
148+
149+
### Why not replace `add_sync_process` with `add_testbench` entirely?
150+
151+
It is not possible to use `add_testbench` processes that drive signals in a race-free way. Consider this (behaviorally defined) circuit:
152+
153+
```python
154+
x = Signal(reset=1)
155+
y = Signal()
156+
157+
def proc_flop():
158+
yield Tick()
159+
yield y.eq(x)
160+
161+
def proc2():
162+
yield Tick()
163+
xv = yield x
164+
yv = yield y
165+
print(f"proc2 x={xv} y={yv}")
166+
167+
def proc3():
168+
yield Tick()
169+
yv = yield y
170+
xv = yield x
171+
print(f"proc3 x={xv} y={yv}")
172+
```
173+
174+
If these processes are added using `add_testbench`, the output is:
175+
176+
```
177+
proc3 x=1 y=0
178+
proc2 x=1 y=1
179+
```
180+
181+
If they are added using `add_sync_process`, the output is:
182+
183+
```
184+
proc2 x=1 y=0
185+
proc3 x=1 y=0
186+
```
187+
188+
Clearly, if `proc2` and `proc3` are other flops in the circuit, perhaps performing a computation on `x` and `y`, they must be simulated using `add_sync_process`.
189+
87190
## Reference-level explanation
88191
[reference-level-explanation]: #reference-level-explanation
89192

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)`.
193+
A new `Simulator.add_testbench(process)` 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()`.
194+
195+
`add_process` and `Settle` are deprecated and removed in a future version.
91196

92-
`Settle` is deprecated and removed in a future version.
197+
`yield Tick()` is deprecated within `add_sync_process` and the ability to use it as well as `yield Settle()` is removed in a future version.
93198

94199
## Drawbacks
95200
[drawbacks]: #drawbacks
96201

97-
Increase in API surface area and complexity. Churn.
202+
- Churn.
203+
- Testbench processes can race with each other, and it is not trivial to use multiple testbench processes in a design in a race-free way.
204+
- Processes using `Settle` can race as well.
98205

99206
## Rationale and alternatives
100207
[rationale-and-alternatives]: #rationale-and-alternatives
101208

102209
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.
103210

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-
106211
## Prior art
107212
[prior-art]: #prior-art
108213

@@ -116,4 +221,10 @@ None.
116221
## Future possibilities
117222
[future-possibilities]: #future-possibilities
118223

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.
224+
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.
225+
226+
As it is, every such helper function would have to take a `domain` argument, which can quickly get out of hand. We have `DomainRenamer` in the RTL sub-language and we may want to have something like that in the simulation sub-language. (@zyp)
227+
228+
A new `add_comb_process` function could be added, to replace combinatorial logic. This function would have to accept a list of all signals driven by the process, so that combinatorial loops could be detected. (The demand for this has not been high; as of right now, this is not possible anyway.)
229+
230+
The existing `add_sync_process` function could accept a list of all signals driven by the process. This could aid in error detection, especially as CXXRTL is integrated into the design, because if a simulator process is driving a signal at the same time as an RTL process, a silent race condition occurs.

0 commit comments

Comments
 (0)