sim: allow visualizing delta cycles in VCD dumps.
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()
This commit is contained in:
parent
0cb71f8c57
commit
36fb9035e4
|
@ -84,5 +84,5 @@ class BaseEngine:
|
||||||
def advance(self):
|
def advance(self):
|
||||||
raise NotImplementedError # :nocov:
|
raise NotImplementedError # :nocov:
|
||||||
|
|
||||||
def write_vcd(self, *, vcd_file, gtkw_file, traces):
|
def write_vcd(self, *, vcd_file, gtkw_file, traces, fs_per_delta):
|
||||||
raise NotImplementedError # :nocov:
|
raise NotImplementedError # :nocov:
|
||||||
|
|
|
@ -115,8 +115,8 @@ class PyCoroProcess(BaseProcess):
|
||||||
return False # no assignments
|
return False # no assignments
|
||||||
|
|
||||||
elif type(command) is Delay:
|
elif type(command) is Delay:
|
||||||
# Internal timeline is in 1ps integeral units, intervals are public API and in floating point
|
# Internal timeline is in 1 fs integeral units, intervals are public API and in floating point
|
||||||
interval = int(command.interval * 1e12) if command.interval is not None else None
|
interval = int(command.interval * 1e15) if command.interval is not None else None
|
||||||
self.state.wait_interval(self, interval)
|
self.state.wait_interval(self, interval)
|
||||||
return False # no assignments
|
return False # no assignments
|
||||||
|
|
||||||
|
|
|
@ -157,8 +157,8 @@ class Simulator:
|
||||||
raise ValueError("Domain {!r} already has a clock driving it"
|
raise ValueError("Domain {!r} already has a clock driving it"
|
||||||
.format(domain.name))
|
.format(domain.name))
|
||||||
|
|
||||||
# We represent times internally in 1 ps units, but users supply float quantities of seconds
|
# We represent times internally in 1 fs units, but users supply float quantities of seconds
|
||||||
period = int(period * 1e12)
|
period = int(period * 1e15)
|
||||||
|
|
||||||
if phase is None:
|
if phase is None:
|
||||||
# By default, delay the first edge by half period. This causes any synchronous activity
|
# By default, delay the first edge by half period. This causes any synchronous activity
|
||||||
|
@ -166,7 +166,7 @@ class Simulator:
|
||||||
# viewer.
|
# viewer.
|
||||||
phase = period // 2
|
phase = period // 2
|
||||||
else:
|
else:
|
||||||
phase = int(phase * 1e12) + period // 2
|
phase = int(phase * 1e15) + period // 2
|
||||||
self._engine.add_clock_process(domain.clk, phase=phase, period=period)
|
self._engine.add_clock_process(domain.clk, phase=phase, period=period)
|
||||||
self._clocked.add(domain)
|
self._clocked.add(domain)
|
||||||
|
|
||||||
|
@ -207,13 +207,13 @@ class Simulator:
|
||||||
|
|
||||||
If the simulation stops advancing, this function will never return.
|
If the simulation stops advancing, this function will never return.
|
||||||
"""
|
"""
|
||||||
# Convert deadline in seconds into internal 1 ps units
|
# Convert deadline in seconds into internal 1 fs units
|
||||||
deadline = deadline * 1e12
|
deadline = deadline * 1e15
|
||||||
assert self._engine.now <= deadline
|
assert self._engine.now <= deadline
|
||||||
while (self.advance() or run_passive) and self._engine.now < deadline:
|
while (self.advance() or run_passive) and self._engine.now < deadline:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
|
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=(), fs_per_delta=0):
|
||||||
"""Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
|
"""Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
|
||||||
|
|
||||||
This method returns a context manager. It can be used as: ::
|
This method returns a context manager. It can be used as: ::
|
||||||
|
@ -238,4 +238,5 @@ class Simulator:
|
||||||
file.close()
|
file.close()
|
||||||
raise ValueError("Cannot start writing waveforms after advancing simulation time")
|
raise ValueError("Cannot start writing waveforms after advancing simulation time")
|
||||||
|
|
||||||
return self._engine.write_vcd(vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
|
return self._engine.write_vcd(vcd_file=vcd_file, gtkw_file=gtkw_file,
|
||||||
|
traces=traces, fs_per_delta=fs_per_delta)
|
||||||
|
|
|
@ -34,9 +34,11 @@ class _VCDWriter:
|
||||||
sub = _VCDWriter.eval_field(field.operands[0], signal, value)
|
sub = _VCDWriter.eval_field(field.operands[0], signal, value)
|
||||||
return Const(sub, field.shape()).value
|
return Const(sub, field.shape()).value
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError # :nocov:
|
||||||
|
|
||||||
|
def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta=0):
|
||||||
|
self.fs_per_delta = fs_per_delta
|
||||||
|
|
||||||
def __init__(self, design, *, vcd_file, gtkw_file=None, traces=()):
|
|
||||||
# Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that
|
# Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that
|
||||||
# the simulator is still usable if it's not installed for some reason.
|
# the simulator is still usable if it's not installed for some reason.
|
||||||
import vcd, vcd.gtkw
|
import vcd, vcd.gtkw
|
||||||
|
@ -54,7 +56,7 @@ class _VCDWriter:
|
||||||
self.vcd_memory_vars = {}
|
self.vcd_memory_vars = {}
|
||||||
self.vcd_file = vcd_file
|
self.vcd_file = vcd_file
|
||||||
self.vcd_writer = vcd_file and vcd.VCDWriter(self.vcd_file,
|
self.vcd_writer = vcd_file and vcd.VCDWriter(self.vcd_file,
|
||||||
timescale="1 ps", comment="Generated by Amaranth")
|
timescale="1 fs", comment="Generated by Amaranth")
|
||||||
|
|
||||||
self.gtkw_signal_names = SignalDict()
|
self.gtkw_signal_names = SignalDict()
|
||||||
self.gtkw_memory_names = {}
|
self.gtkw_memory_names = {}
|
||||||
|
@ -410,6 +412,7 @@ class PySimEngine(BaseEngine):
|
||||||
self._design = design
|
self._design = design
|
||||||
self._processes = _FragmentCompiler(self._state)(self._design.fragment)
|
self._processes = _FragmentCompiler(self._state)(self._design.fragment)
|
||||||
self._testbenches = []
|
self._testbenches = []
|
||||||
|
self._delta_cycles = 0
|
||||||
self._vcd_writers = []
|
self._vcd_writers = []
|
||||||
|
|
||||||
def add_clock_process(self, clock, *, phase, period):
|
def add_clock_process(self, clock, *, phase, period):
|
||||||
|
@ -429,10 +432,12 @@ class PySimEngine(BaseEngine):
|
||||||
for process in self._processes:
|
for process in self._processes:
|
||||||
process.reset()
|
process.reset()
|
||||||
|
|
||||||
def _step_rtl(self, changed):
|
def _step_rtl(self):
|
||||||
# Performs the two phases of a delta cycle in a loop:
|
# Performs the two phases of a delta cycle in a loop:
|
||||||
converged = False
|
converged = False
|
||||||
while not converged:
|
while not converged:
|
||||||
|
changed = set() if self._vcd_writers else None
|
||||||
|
|
||||||
# 1. eval: run and suspend every non-waiting process once, queueing signal changes
|
# 1. eval: run and suspend every non-waiting process once, queueing signal changes
|
||||||
for process in self._processes:
|
for process in self._processes:
|
||||||
if process.runnable:
|
if process.runnable:
|
||||||
|
@ -442,11 +447,24 @@ class PySimEngine(BaseEngine):
|
||||||
# 2. commit: apply every queued signal change, waking up any waiting processes
|
# 2. commit: apply every queued signal change, waking up any waiting processes
|
||||||
converged = self._state.commit(changed)
|
converged = self._state.commit(changed)
|
||||||
|
|
||||||
def _step_tb(self):
|
for vcd_writer in self._vcd_writers:
|
||||||
changed = set() if self._vcd_writers else None
|
now_plus_deltas = self._now_plus_deltas(vcd_writer)
|
||||||
|
for change in changed:
|
||||||
|
if isinstance(change, _PySignalState):
|
||||||
|
signal_state = change
|
||||||
|
vcd_writer.update_signal(now_plus_deltas,
|
||||||
|
signal_state.signal, signal_state.curr)
|
||||||
|
elif isinstance(change, _PyMemoryChange):
|
||||||
|
vcd_writer.update_memory(now_plus_deltas, change.state.memory,
|
||||||
|
change.addr, change.state.data[change.addr])
|
||||||
|
else:
|
||||||
|
assert False # :nocov:
|
||||||
|
|
||||||
|
self._delta_cycles += 1
|
||||||
|
|
||||||
|
def _step_tb(self):
|
||||||
# Run processes waiting for an interval to expire (mainly `add_clock_process()``)
|
# Run processes waiting for an interval to expire (mainly `add_clock_process()``)
|
||||||
self._step_rtl(changed)
|
self._step_rtl()
|
||||||
|
|
||||||
# Run testbenches waiting for an interval to expire, or for a signal to change state
|
# Run testbenches waiting for an interval to expire, or for a signal to change state
|
||||||
converged = False
|
converged = False
|
||||||
|
@ -459,19 +477,7 @@ class PySimEngine(BaseEngine):
|
||||||
while testbench.run():
|
while testbench.run():
|
||||||
# Testbench has changed simulation state; run processes triggered by that
|
# Testbench has changed simulation state; run processes triggered by that
|
||||||
converged = False
|
converged = False
|
||||||
self._step_rtl(changed)
|
self._step_rtl()
|
||||||
|
|
||||||
for vcd_writer in self._vcd_writers:
|
|
||||||
for change in changed:
|
|
||||||
if isinstance(change, _PySignalState):
|
|
||||||
signal_state = change
|
|
||||||
vcd_writer.update_signal(self._timeline.now,
|
|
||||||
signal_state.signal, signal_state.curr)
|
|
||||||
elif isinstance(change, _PyMemoryChange):
|
|
||||||
vcd_writer.update_memory(self._timeline.now, change.state.memory,
|
|
||||||
change.addr, change.state.data[change.addr])
|
|
||||||
else:
|
|
||||||
assert False # :nocov:
|
|
||||||
|
|
||||||
def advance(self):
|
def advance(self):
|
||||||
self._step_tb()
|
self._step_tb()
|
||||||
|
@ -482,13 +488,16 @@ class PySimEngine(BaseEngine):
|
||||||
def now(self):
|
def now(self):
|
||||||
return self._timeline.now
|
return self._timeline.now
|
||||||
|
|
||||||
|
def _now_plus_deltas(self, vcd_writer):
|
||||||
|
return self._timeline.now + self._delta_cycles * vcd_writer.fs_per_delta
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def write_vcd(self, *, vcd_file, gtkw_file, traces):
|
def write_vcd(self, *, vcd_file, gtkw_file, traces, fs_per_delta):
|
||||||
vcd_writer = _VCDWriter(self._design,
|
vcd_writer = _VCDWriter(self._design,
|
||||||
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
|
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces, fs_per_delta=fs_per_delta)
|
||||||
try:
|
try:
|
||||||
self._vcd_writers.append(vcd_writer)
|
self._vcd_writers.append(vcd_writer)
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
vcd_writer.close(self._timeline.now)
|
vcd_writer.close(self._now_plus_deltas(vcd_writer))
|
||||||
self._vcd_writers.remove(vcd_writer)
|
self._vcd_writers.remove(vcd_writer)
|
||||||
|
|
|
@ -1188,7 +1188,8 @@ class SimulatorIntegrationTestCase(FHDLTestCase):
|
||||||
sim = Simulator(Module())
|
sim = Simulator(Module())
|
||||||
sim.add_testbench(testbench_1)
|
sim.add_testbench(testbench_1)
|
||||||
sim.add_testbench(testbench_2)
|
sim.add_testbench(testbench_2)
|
||||||
sim.run()
|
with sim.write_vcd("test.vcd", fs_per_delta=1):
|
||||||
|
sim.run()
|
||||||
|
|
||||||
|
|
||||||
class SimulatorRegressionTestCase(FHDLTestCase):
|
class SimulatorRegressionTestCase(FHDLTestCase):
|
||||||
|
|
Loading…
Reference in a new issue