|
| 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