Skip to content

Commit 70e7525

Browse files
committed
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 will be raised like: 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("dsim.vcd", fs_per_delta=1): sim.run()
1 parent 520e934 commit 70e7525

File tree

3 files changed

+41
-31
lines changed

3 files changed

+41
-31
lines changed

amaranth/sim/_pycoro.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@ def run(self):
122122
return False # did not change state
123123

124124
elif type(command) is Delay:
125-
# Internal timeline is in 1ps integeral units, intervals are public API and in floating point
126-
interval = int(command.interval * 1e12) if command.interval is not None else None
125+
# Internal timeline is in 1 fs integeral units, intervals are public API and in floating point
126+
interval = int(command.interval * 1e15) if command.interval is not None else None
127127
self.state.wait_interval(self, interval)
128128
return False # did not change state
129129

amaranth/sim/core.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,16 @@ def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
179179
raise ValueError("Domain {!r} already has a clock driving it"
180180
.format(domain.name))
181181

182-
# We represent times internally in 1 ps units, but users supply float quantities of seconds
183-
period = int(period * 1e12)
182+
# We represent times internally in 1 fs units, but users supply float quantities of seconds
183+
period = int(period * 1e15)
184184

185185
if phase is None:
186186
# By default, delay the first edge by half period. This causes any synchronous activity
187187
# to happen at a non-zero time, distinguishing it from the initial values in the waveform
188188
# viewer.
189189
phase = period // 2
190190
else:
191-
phase = int(phase * 1e12) + period // 2
191+
phase = int(phase * 1e15) + period // 2
192192
self._engine.add_clock_process(domain.clk, phase=phase, period=period)
193193
self._clocked.add(domain)
194194

@@ -229,13 +229,13 @@ def run_until(self, deadline, *, run_passive=False):
229229
230230
If the simulation stops advancing, this function will never return.
231231
"""
232-
# Convert deadline in seconds into internal 1 ps units
233-
deadline = deadline * 1e12
232+
# Convert deadline in seconds into internal 1 fs units
233+
deadline = deadline * 1e15
234234
assert self._engine.now <= deadline
235235
while (self.advance() or run_passive) and self._engine.now < deadline:
236236
pass
237237

238-
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
238+
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=(), fs_per_delta=0):
239239
"""Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
240240
241241
This method returns a context manager. It can be used as: ::
@@ -260,4 +260,5 @@ def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
260260
file.close()
261261
raise ValueError("Cannot start writing waveforms after advancing simulation time")
262262

263-
return self._engine.write_vcd(vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
263+
return self._engine.write_vcd(vcd_file=vcd_file, gtkw_file=gtkw_file,
264+
traces=traces, fs_per_delta=fs_per_delta)

amaranth/sim/pysim.py

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def eval_field(field, signal, value):
3636
else:
3737
raise NotImplementedError
3838

39-
def __init__(self, design, *, vcd_file, gtkw_file=None, traces=()):
39+
def __init__(self, design, *, vcd_file, gtkw_file=None, traces=(), fs_per_delta=0):
40+
self.fs_per_delta = fs_per_delta
41+
4042
# Although pyvcd is a mandatory dependency, be resilient and import it as needed, so that
4143
# the simulator is still usable if it's not installed for some reason.
4244
import vcd, vcd.gtkw
@@ -54,7 +56,7 @@ def __init__(self, design, *, vcd_file, gtkw_file=None, traces=()):
5456
self.vcd_memory_vars = {}
5557
self.vcd_file = vcd_file
5658
self.vcd_writer = vcd_file and vcd.VCDWriter(self.vcd_file,
57-
timescale="1 ps", comment="Generated by Amaranth")
59+
timescale="1 fs", comment="Generated by Amaranth")
5860

5961
self.gtkw_signal_names = SignalDict()
6062
self.gtkw_memory_names = {}
@@ -410,6 +412,7 @@ def __init__(self, design):
410412
self._design = design
411413
self._processes = _FragmentCompiler(self._state)(self._design.fragment)
412414
self._testbenches = []
415+
self._delta_cycles = 0
413416
self._vcd_writers = []
414417

415418
def add_clock_process(self, clock, *, phase, period):
@@ -429,10 +432,12 @@ def reset(self):
429432
for process in self._processes:
430433
process.reset()
431434

432-
def _step_rtl(self, changed):
435+
def _step_rtl(self):
433436
# Performs the two phases of a delta cycle in a loop:
434437
converged = False
435438
while not converged:
439+
changed = set() if self._vcd_writers else None
440+
436441
# 1. eval: run and suspend every non-waiting process once, queueing signal changes
437442
for process in self._processes:
438443
if process.runnable:
@@ -442,11 +447,24 @@ def _step_rtl(self, changed):
442447
# 2. commit: apply every queued signal change, waking up any waiting processes
443448
converged = self._state.commit(changed)
444449

445-
def _step_tb(self):
446-
changed = set() if self._vcd_writers else None
450+
for vcd_writer in self._vcd_writers:
451+
now_plus_deltas = self._now_plus_deltas(vcd_writer)
452+
for change in changed:
453+
if isinstance(change, _PySignalState):
454+
signal_state = change
455+
vcd_writer.update_signal(now_plus_deltas,
456+
signal_state.signal, signal_state.curr)
457+
elif isinstance(change, _PyMemoryChange):
458+
vcd_writer.update_memory(now_plus_deltas, change.state.memory,
459+
change.addr, change.state.data[change.addr])
460+
else:
461+
assert False # :nocov:
462+
463+
self._delta_cycles += 1
447464

465+
def _step_tb(self):
448466
# Run processes waiting for an interval to expire (mainly `add_clock_process()``)
449-
self._step_rtl(changed)
467+
self._step_rtl()
450468

451469
# Run testbenches waiting for an interval to expire, or for a signal to change state
452470
converged = False
@@ -459,19 +477,7 @@ def _step_tb(self):
459477
while testbench.run():
460478
# Testbench has changed simulation state; run processes triggered by that
461479
converged = False
462-
self._step_rtl(changed)
463-
464-
for vcd_writer in self._vcd_writers:
465-
for change in changed:
466-
if isinstance(change, _PySignalState):
467-
signal_state = change
468-
vcd_writer.update_signal(self._timeline.now,
469-
signal_state.signal, signal_state.curr)
470-
elif isinstance(change, _PyMemoryChange):
471-
vcd_writer.update_memory(self._timeline.now, change.state.memory,
472-
change.addr, change.state.data[change.addr])
473-
else:
474-
assert False # :nocov:
480+
self._step_rtl()
475481

476482
def advance(self):
477483
self._step_tb()
@@ -482,13 +488,16 @@ def advance(self):
482488
def now(self):
483489
return self._timeline.now
484490

491+
def _now_plus_deltas(self, vcd_writer):
492+
return self._timeline.now + self._delta_cycles * vcd_writer.fs_per_delta
493+
485494
@contextmanager
486-
def write_vcd(self, *, vcd_file, gtkw_file, traces):
495+
def write_vcd(self, *, vcd_file, gtkw_file, traces, fs_per_delta):
487496
vcd_writer = _VCDWriter(self._design,
488-
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
497+
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces, fs_per_delta=fs_per_delta)
489498
try:
490499
self._vcd_writers.append(vcd_writer)
491500
yield
492501
finally:
493-
vcd_writer.close(self._timeline.now)
502+
vcd_writer.close(self._now_plus_deltas(vcd_writer))
494503
self._vcd_writers.remove(vcd_writer)

0 commit comments

Comments
 (0)