If delta cycles are expanded (i.e. if the `fs_per_delta` argument to
`Simulator.write_vcd` is not zero), then create a string typed variable
for each testbench in the simulation, which reflects the current
command being executed by that testbench. To make all commands visible,
insert a (visual) delta cycle after each executed command, and ensure
that there is a change/crossing point in the waveform display each time
a command is executed, even if several identical ones in a row.
If delta cycles are not expanded, the behavior is unchanged.
This commit adds an option `fs_per_delta=` to `Simulator.write_vcd()`.
Specifying a positive integer value for it causes the simulator to
offset value change times by that many femtoseconds for each delta
cycle after the last timeline advancement.
This option is only suitable for debugging. If the timeline is advanced
by less than the combined duration of expanded delta cycles, an error
similar to the following will be raised:
vcd.writer.VCDPhaseError: Out of order timestamp: 62490
Typically `fs_per_delta=1` is best, since it allows thousands of delta
cycles to be expanded without risking a VCD phase error, but bigger
values can be used for an exaggerated visual effect.
Also, the VCD writer is changed to use 1 fs as the timebase instead of
1 ps. This change is largely invisible to designers, resulting only in
slightly larger VCD files due to longer timestamps.
Since the `fs_per_delta=` option is per VCD writer, it is possible to
simultaneously dump two VCDs, one with and one without delta cycle
expansion:
with sim.write_vcd("sim.vcd"), sim.write_vcd("sim.d.vcd", fs_per_delta=1):
sim.run()
Before this commit, testbenches (generators added with `add_testbench`)
were not only preemptible after any `yield`, but were *guaranteed* to
be preempted by another testbench after *every* yield. This is evil:
if you have any race condition between testbenches, which is common,
this scheduling strategy will maximize the resulting nondeterminism by
interleaving your testbench with every other one as much as possible.
This behavior is an outcome of the way `add_testbench` is implemented,
which is by yielding `Settle()` after every command.
One can observe that:
- `yield value_like` should never preempt;
- `yield assignable.eq()` in `add_process()` should not preempt, since
it only sets a `next` signal state, or appends to `write_queue` of
a memory state, and never wakes up processes;
- `yield assignable.eq()` in `add_testbench()` should only preempt if
changing `assignable` wakes up an RTL process. (It could potentially
also preempt if that wakes up another testbench, but this has no
benefit and requires `sim.set()` from RFC 36 to be awaitable, which
is not desirable.)
After this commit, `PySimEngine._step()` is implemented with two nested
loops instead of one. The outer loop iterates through every testbench
and runs it until an explicit wait point (`Settle()`, `Delay()`, or
`Tick()`), terminating when no testbenches are runnable. The inner loop
is the usual eval/commit loop, running whenever a testbench changes
design state.
`PySimEngine._processes` is a `set`, which doesn't have a deterministic
iteration order. This does not matter for processes, where determinism
is guaranteed by the eval/commit loop, but causes racy testbenches to
pass or fail nondeterministically (in practice depending on the memory
layout of the Python process). While it is best to not have races in
the testbenches, this commit makes `PySimEngine._testbenches` a `list`,
making the outcome of a race deterministic, and enabling a hacky work-
around to make them work: reordering calls to `add_testbench()`.
A potential future improvement is a simulation mode that, instead,
randomizes the scheduling of testbenches, exposing race conditions
early.
The abbreviated form was initially added to match `lib.fifo`, but it
looks very out of place on `lib.memory`, and we may be moving away from
such heavy use of abbreviations anyway.
While technically a breaking change, these attributes have very narrow
usefulness and so this change qualifies as "minor".
Display `shape` and `depth` also. `depth` is redundant although useful
for ease of reading (there are always `depth` elements shown), but
`shape` was just lost.
This attribute is fully redundant with `.__len__()`, and is out of place
on a `list`-like container like `Memory.Init`.
The `.shape` attribute, however, provides a unique function.
The premise of `tcl_escape` is incorrect: it is not possible, by design,
to escape a single backslash inside of a Tcl {}-quoted string:
$ tclsh
% puts {\\}
\\
`tcl_quote` should be used instead since it can escape arbitrary strings
(and uses the right algorithm already).