diff --git a/rfcs/0001-aggregate-data-structures.html b/rfcs/0001-aggregate-data-structures.html index 991474f2..bf9a9cfc 100644 --- a/rfcs/0001-aggregate-data-structures.html +++ b/rfcs/0001-aggregate-data-structures.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0002-interfaces.html b/rfcs/0002-interfaces.html index 68feaab2..4c5da4c8 100644 --- a/rfcs/0002-interfaces.html +++ b/rfcs/0002-interfaces.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0003-enumeration-shapes.html b/rfcs/0003-enumeration-shapes.html index 4e21d846..9026992f 100644 --- a/rfcs/0003-enumeration-shapes.html +++ b/rfcs/0003-enumeration-shapes.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0004-const-castable-exprs.html b/rfcs/0004-const-castable-exprs.html index 0e4192a3..d4002f0b 100644 --- a/rfcs/0004-const-castable-exprs.html +++ b/rfcs/0004-const-castable-exprs.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0005-remove-const-normalize.html b/rfcs/0005-remove-const-normalize.html index ab12aa5a..3917a83d 100644 --- a/rfcs/0005-remove-const-normalize.html +++ b/rfcs/0005-remove-const-normalize.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0006-stdlib-crc.html b/rfcs/0006-stdlib-crc.html index 1c332910..88766c67 100644 --- a/rfcs/0006-stdlib-crc.html +++ b/rfcs/0006-stdlib-crc.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0008-aggregate-extensibility.html b/rfcs/0008-aggregate-extensibility.html index 02821018..3b7979cb 100644 --- a/rfcs/0008-aggregate-extensibility.html +++ b/rfcs/0008-aggregate-extensibility.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0009-const-init-shape-castable.html b/rfcs/0009-const-init-shape-castable.html index c65100cb..42b9826c 100644 --- a/rfcs/0009-const-init-shape-castable.html +++ b/rfcs/0009-const-init-shape-castable.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0010-move-repl-to-value.html b/rfcs/0010-move-repl-to-value.html index f3fdfc25..ad2f7881 100644 --- a/rfcs/0010-move-repl-to-value.html +++ b/rfcs/0010-move-repl-to-value.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0015-lifting-shape-castables.html b/rfcs/0015-lifting-shape-castables.html index 9b322a98..f43f6f11 100644 --- a/rfcs/0015-lifting-shape-castables.html +++ b/rfcs/0015-lifting-shape-castables.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0017-remove-log2-int.html b/rfcs/0017-remove-log2-int.html index 80ad3507..b5dc647f 100644 --- a/rfcs/0017-remove-log2-int.html +++ b/rfcs/0017-remove-log2-int.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0018-reorganize-vendor-platforms.html b/rfcs/0018-reorganize-vendor-platforms.html index b7951a36..84f73bcf 100644 --- a/rfcs/0018-reorganize-vendor-platforms.html +++ b/rfcs/0018-reorganize-vendor-platforms.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0019-remove-scheduler.html b/rfcs/0019-remove-scheduler.html index 6d35be6f..ea2fdacd 100644 --- a/rfcs/0019-remove-scheduler.html +++ b/rfcs/0019-remove-scheduler.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0020-deprecate-non-fwft-fifos.html b/rfcs/0020-deprecate-non-fwft-fifos.html index 9dc5bed5..12cd4fe5 100644 --- a/rfcs/0020-deprecate-non-fwft-fifos.html +++ b/rfcs/0020-deprecate-non-fwft-fifos.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0021-patch-releases.html b/rfcs/0021-patch-releases.html index a6aa9209..57ff058a 100644 --- a/rfcs/0021-patch-releases.html +++ b/rfcs/0021-patch-releases.html @@ -81,7 +81,7 @@ diff --git a/rfcs/0022-valuecastable-shape.html b/rfcs/0022-valuecastable-shape.html index 33268d98..b4272a90 100644 --- a/rfcs/0022-valuecastable-shape.html +++ b/rfcs/0022-valuecastable-shape.html @@ -81,7 +81,7 @@ @@ -191,7 +191,7 @@

Fut - @@ -205,7 +205,7 @@

Fut - diff --git a/rfcs/0027-simulator-testbenches.html b/rfcs/0027-simulator-testbenches.html new file mode 100644 index 00000000..0163cb19 --- /dev/null +++ b/rfcs/0027-simulator-testbenches.html @@ -0,0 +1,368 @@ + + + + + + 0027-simulator-testbenches - The Amaranth RFC Book + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+
+ +

Testbench processes for the simulator

+

Summary

+

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.

+

Motivation

+

Consider the following code:

+
from amaranth import *
+from amaranth.sim import Simulator
+
+
+class DUT(Elaboratable):
+    def __init__(self):
+        self.out  = Signal()
+        self.outn = Signal()
+
+    def elaborate(self, platform):
+        m = Module()
+        m.d.sync += self.outn.eq(~self.out)
+        return m
+
+
+dut = DUT()
+def testbench():
+    yield dut.out.eq(1)
+    yield
+    print((yield dut.out))
+    print((yield dut.outn))
+
+sim = Simulator(dut)
+sim.add_clock(1e-6)
+sim.add_sync_process(testbench)
+sim.run()
+
+

This code prints:

+
1
+1
+
+

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.

+

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

+

Guide-level explanation

+

The code example above is rewritten as:

+
dut = DUT()
+def testbench():
+    yield dut.out.eq(1)
+    yield Tick()
+    print((yield dut.out))
+    print((yield dut.outn))
+
+sim = Simulator(dut)
+sim.add_clock(1e-6)
+sim.add_testbench(testbench)
+sim.run()
+
+

When run, it prints:

+
1
+0
+
+

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

+

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

+

Guidance on simulator modalities

+

There are two main simulator modalities: add_testbench and add_sync_process. They have completely disjoint purposes:

+
    +
  • add_testbench is used for testing logic (asynchronous or synchronous). It is not used for behavioral replacement of synchronous logic.
  • +
  • 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.
  • +
+

Example of using add_testbench to test combinatorial logic:

+
m = Module()
+m.d.comb += a.eq(~b)
+
+def testbench():
+    yield b.eq(1)
+    print((yield a)) # => 0
+
+sim = Simulator(m)
+# No clock is required
+sim.add_testbench(testbench)
+sim.run()
+
+

Example of using add_testbench to test synchronous logic:

+
m = Module()
+m.d.sync += a.eq(~b)
+
+def testbench():
+    yield b.eq(1)
+    yield Tick() # same as Tick("sync")
+    print((yield a)) # => 0
+
+sim = Simulator(m)
+sim.add_clock(1e-6)
+sim.add_testbench(testbench)
+sim.run()
+
+

Example of using add_sync_process to replace the flop above, and add_testbench to test the flop:

+
m = Module()
+
+def flop():
+    while True:
+        yield b.eq(~(yield a))
+        yield Tick()
+
+def testbench():
+    yield b.eq(1)
+    yield Tick() # same as Tick("sync")
+    print((yield a)) # => 0
+
+sim = Simulator(m)
+sim.add_clock(1e-6)
+sim.add_sync_process(flop)
+sim.add_testbench(testbench)
+sim.run()
+
+

Why not replace add_sync_process with add_testbench entirely?

+

It is not possible to use add_testbench processes that drive signals in a race-free way. Consider this (behaviorally defined) circuit:

+
x = Signal(reset=1)
+y = Signal()
+
+def proc_flop():
+    yield Tick()
+    yield y.eq(x)
+
+def proc2():
+    yield Tick()
+    xv = yield x
+    yv = yield y
+    print(f"proc2 x={xv} y={yv}")
+
+def proc3():
+    yield Tick()
+    yv = yield y
+    xv = yield x
+    print(f"proc3 x={xv} y={yv}")
+
+

If these processes are added using add_testbench, the output is:

+
proc3 x=1 y=0
+proc2 x=1 y=1
+
+

If they are added using add_sync_process, the output is:

+
proc2 x=1 y=0
+proc3 x=1 y=0
+
+

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.

+

Reference-level explanation

+

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().

+

add_process and Settle are deprecated and removed in a future version.

+

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.

+

Drawbacks

+
    +
  • Churn.
  • +
  • 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. +
      +
    • Processes using Settle can race as well.
    • +
    +
  • +
+

Rationale and alternatives

+

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.

+

Prior art

+

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.

+

Unresolved questions

+

None.

+

Future possibilities

+

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.

+

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)

+

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

+

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.

+ +
+ + +
+
+ + + +
+ + + + + + + + + + + + + + + + + + +
+ + diff --git a/rfcs/0028-override-value-operators.html b/rfcs/0028-override-value-operators.html index 6aa0f158..c5c55b77 100644 --- a/rfcs/0028-override-value-operators.html +++ b/rfcs/0028-override-value-operators.html @@ -81,7 +81,7 @@ @@ -189,7 +189,7 @@

Fut