sim: split into base, core, and engines.

Before this commit, each simulation engine (which is only pysim at
the moment, but also cxxsim soon) was a subclass of SimulatorCore,
and every simulation engine module would essentially duplicate
the complete structure of a simulator, with code partially shared.

This was a really bad idea: it was inconvenient to use, with
downstream code having to branch between e.g. PySettle and CxxSettle;
it had no well-defined external interface; it had multiple virtually
identical entry points; and it had no separation between simulation
algorithms and glue code.

This commit completely rearranges simulation code.
  1. sim._base defines internal simulation interfaces. The clarity of
     these internal interfaces is important because simulation
     engines mix and match components to provide a consistent API
     regardless of the chosen engine.
  2. sim.core defines the external simulation interface: the commands
     and the simulator facade. The facade provides a single entry
     point and, when possible, validates or lowers user input.
     It also imports built-in simulation engines by their symbolic
     name, avoiding eager imports of pyvcd or ctypes.
  3. sim.xxxsim (currently, only sim.pysim) defines the simulator
     implementation: time and state management, process scheduling,
     and waveform dumping.

The new simulator structure has none of the downsides of the old one.

See #324.
This commit is contained in:
whitequark 2020-08-27 10:17:02 +00:00
parent 9bdb7accc8
commit b65e11f38f
19 changed files with 396 additions and 301 deletions

View file

@ -1,5 +1,6 @@
from nmigen import * from nmigen import *
from nmigen.back import rtlil, verilog, pysim from nmigen.sim import *
from nmigen.back import rtlil, verilog
class Counter(Elaboratable): class Counter(Elaboratable):
@ -19,7 +20,7 @@ ctr = Counter(width=16)
print(verilog.convert(ctr, ports=[ctr.o, ctr.en])) print(verilog.convert(ctr, ports=[ctr.o, ctr.en]))
sim = pysim.Simulator(ctr) sim = Simulator(ctr)
sim.add_clock(1e-6) sim.add_clock(1e-6)
def ce_proc(): def ce_proc():
yield; yield; yield yield; yield; yield

View file

@ -1,11 +1,11 @@
import warnings import warnings
from ..sim.pysim import * from ..sim import *
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"] __all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
# TODO(nmigen-0.4): remove # TODO(nmigen-0.4): remove
warnings.warn("instead of back.pysim, use sim.pysim", warnings.warn("instead of nmigen.back.pysim.*, use nmigen.sim.*",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)

View file

@ -2,8 +2,8 @@ import functools
import inspect import inspect
from collections.abc import Iterable from collections.abc import Iterable
from ...hdl.cd import ClockDomain from ...hdl.cd import ClockDomain
from ...back.pysim import *
from ...hdl.ir import Fragment from ...hdl.ir import Fragment
from ...sim import *
__all__ = ["run_simulation", "passive"] __all__ = ["run_simulation", "passive"]

View file

@ -0,0 +1,4 @@
from .core import *
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]

67
nmigen/sim/_base.py Normal file
View file

@ -0,0 +1,67 @@
__all__ = ["BaseProcess", "BaseSignalState", "BaseSimulation", "BaseEngine"]
class BaseProcess:
__slots__ = ()
def __init__(self):
self.reset()
def reset(self):
self.runnable = False
self.passive = True
def run(self):
raise NotImplementedError
class BaseSignalState:
__slots__ = ()
signal = NotImplemented
curr = NotImplemented
next = NotImplemented
def set(self, value):
raise NotImplementedError
class BaseSimulation:
def reset(self):
raise NotImplementedError
def get_signal(self, signal):
raise NotImplementedError
slots = NotImplemented
def add_trigger(self, process, signal, *, trigger=None):
raise NotImplementedError
def remove_trigger(self, process, signal):
raise NotImplementedError
def wait_interval(self, process, interval):
raise NotImplementedError
class BaseEngine:
def add_coroutine_process(self, process, *, default_cmd):
raise NotImplementedError
def add_clock_process(self, clock, *, phase, period):
raise NotImplementedError
def reset(self):
raise NotImplementedError
@property
def now(self):
raise NotImplementedError
def advance(self):
raise NotImplementedError
def write_vcd(self, *, vcd_file, gtkw_file, traces):
raise NotImplementedError

View file

@ -1,46 +0,0 @@
from ..hdl.cd import *
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active"]
class Command:
pass
class Settle(Command):
def __repr__(self):
return "(settle)"
class Delay(Command):
def __init__(self, interval=None):
self.interval = None if interval is None else float(interval)
def __repr__(self):
if self.interval is None:
return "(delay ε)"
else:
return "(delay {:.3}us)".format(self.interval * 1e6)
class Tick(Command):
def __init__(self, domain="sync"):
if not isinstance(domain, (str, ClockDomain)):
raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}"
.format(domain))
assert domain != "comb"
self.domain = domain
def __repr__(self):
return "(tick {})".format(self.domain)
class Passive(Command):
def __repr__(self):
return "(passive)"
class Active(Command):
def __repr__(self):
return "(active)"

View file

@ -1,63 +0,0 @@
__all__ = ["Process", "Timeline"]
class Process:
def __init__(self, *, is_comb):
self.is_comb = is_comb
self.reset()
def reset(self):
self.runnable = self.is_comb
self.passive = True
def run(self):
raise NotImplementedError
class Timeline:
def __init__(self):
self.now = 0.0
self.deadlines = dict()
def reset(self):
self.now = 0.0
self.deadlines.clear()
def at(self, run_at, process):
assert process not in self.deadlines
self.deadlines[process] = run_at
def delay(self, delay_by, process):
if delay_by is None:
run_at = self.now
else:
run_at = self.now + delay_by
self.at(run_at, process)
def advance(self):
nearest_processes = set()
nearest_deadline = None
for process, deadline in self.deadlines.items():
if deadline is None:
if nearest_deadline is not None:
nearest_processes.clear()
nearest_processes.add(process)
nearest_deadline = self.now
break
elif nearest_deadline is None or deadline <= nearest_deadline:
assert deadline >= self.now
if nearest_deadline is not None and deadline < nearest_deadline:
nearest_processes.clear()
nearest_processes.add(process)
nearest_deadline = deadline
if not nearest_processes:
return False
for process in nearest_processes:
process.runnable = True
del self.deadlines[process]
self.now = nearest_deadline
return True

View file

@ -1,12 +1,12 @@
import inspect import inspect
from ._core import Process from ._base import BaseProcess
__all__ = ["PyClockProcess"] __all__ = ["PyClockProcess"]
class PyClockProcess(Process): class PyClockProcess(BaseProcess):
def __init__(self, state, signal, *, phase, period): def __init__(self, state, signal, *, phase, period):
assert len(signal) == 1 assert len(signal) == 1
@ -20,16 +20,17 @@ class PyClockProcess(Process):
def reset(self): def reset(self):
self.runnable = True self.runnable = True
self.passive = True self.passive = True
self.initial = True self.initial = True
def run(self): def run(self):
self.runnable = False
if self.initial: if self.initial:
self.initial = False self.initial = False
self.state.timeline.delay(self.phase, self) self.state.wait_interval(self, self.phase)
else: else:
clk_state = self.state.slots[self.slot] clk_state = self.state.slots[self.slot]
clk_state.set(not clk_state.curr) clk_state.set(not clk_state.curr)
self.state.timeline.delay(self.period / 2, self) self.state.wait_interval(self, self.period / 2)
self.runnable = False

View file

@ -2,15 +2,15 @@ import inspect
from ..hdl import * from ..hdl import *
from ..hdl.ast import Statement, SignalSet from ..hdl.ast import Statement, SignalSet
from ._cmds import * from .core import Tick, Settle, Delay, Passive, Active
from ._core import Process from ._base import BaseProcess
from ._pyrtl import _ValueCompiler, _RHSValueCompiler, _StatementCompiler from ._pyrtl import _ValueCompiler, _RHSValueCompiler, _StatementCompiler
__all__ = ["PyCoroProcess"] __all__ = ["PyCoroProcess"]
class PyCoroProcess(Process): class PyCoroProcess(BaseProcess):
def __init__(self, state, domains, constructor, *, default_cmd=None): def __init__(self, state, domains, constructor, *, default_cmd=None):
self.state = state self.state = state
self.domains = domains self.domains = domains
@ -22,6 +22,7 @@ class PyCoroProcess(Process):
def reset(self): def reset(self):
self.runnable = True self.runnable = True
self.passive = False self.passive = False
self.coroutine = self.constructor() self.coroutine = self.constructor()
self.exec_locals = { self.exec_locals = {
"slots": self.state.slots, "slots": self.state.slots,
@ -90,11 +91,11 @@ class PyCoroProcess(Process):
return return
elif type(command) is Settle: elif type(command) is Settle:
self.state.timeline.delay(None, self) self.state.wait_interval(self, None)
return return
elif type(command) is Delay: elif type(command) is Delay:
self.state.timeline.delay(command.interval, self) self.state.wait_interval(self, command.interval)
return return
elif type(command) is Passive: elif type(command) is Passive:

View file

@ -5,14 +5,23 @@ from contextlib import contextmanager
from ..hdl import * from ..hdl import *
from ..hdl.ast import SignalSet from ..hdl.ast import SignalSet
from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter
from ._core import * from ._base import BaseProcess
__all__ = ["PyRTLProcess"] __all__ = ["PyRTLProcess"]
class PyRTLProcess(Process): class PyRTLProcess(BaseProcess):
pass __slots__ = ("is_comb", "runnable", "passive", "run")
def __init__(self, *, is_comb):
self.is_comb = is_comb
self.reset()
def reset(self):
self.runnable = self.is_comb
self.passive = True
class _PythonEmitter: class _PythonEmitter:

206
nmigen/sim/core.py Normal file
View file

@ -0,0 +1,206 @@
import inspect
from .._utils import deprecated
from ..hdl.cd import *
from ..hdl.ir import *
from ._base import BaseEngine
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
class Command:
pass
class Settle(Command):
def __repr__(self):
return "(settle)"
class Delay(Command):
def __init__(self, interval=None):
self.interval = None if interval is None else float(interval)
def __repr__(self):
if self.interval is None:
return "(delay ε)"
else:
return "(delay {:.3}us)".format(self.interval * 1e6)
class Tick(Command):
def __init__(self, domain="sync"):
if not isinstance(domain, (str, ClockDomain)):
raise TypeError("Domain must be a string or a ClockDomain instance, not {!r}"
.format(domain))
assert domain != "comb"
self.domain = domain
def __repr__(self):
return "(tick {})".format(self.domain)
class Passive(Command):
def __repr__(self):
return "(passive)"
class Active(Command):
def __repr__(self):
return "(active)"
class Simulator:
def __init__(self, fragment, *, engine="pysim"):
if isinstance(engine, type) and issubclass(engine, BaseEngine):
pass
elif engine == "pysim":
from .pysim import PySimEngine
engine = PySimEngine
else:
raise TypeError("Value '{!r}' is not a simulation engine class or "
"a simulation engine name"
.format(engine))
self._fragment = Fragment.get(fragment, platform=None).prepare()
self._engine = engine(self._fragment)
self._clocked = set()
def _check_process(self, process):
if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
raise TypeError("Cannot add a process {!r} because it is not a generator function"
.format(process))
return process
def add_process(self, process):
process = self._check_process(process)
def wrapper():
# Only start a bench process after comb settling, so that the reset values are correct.
yield Settle()
yield from process()
self._engine.add_coroutine_process(wrapper, default_cmd=None)
def add_sync_process(self, process, *, domain="sync"):
process = self._check_process(process)
def wrapper():
# Only start a sync process after the first clock edge (or reset edge, if the domain
# uses an asynchronous reset). This matches the behavior of synchronous FFs.
yield Tick(domain)
yield from process()
self._engine.add_coroutine_process(wrapper, default_cmd=Tick(domain))
def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
"""Add a clock process.
Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.
Arguments
---------
period : float
Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
seconds.
phase : None or float
Clock phase. The process will wait ``phase`` seconds before the first clock transition.
If not specified, defaults to ``period / 2``.
domain : str or ClockDomain
Driven clock domain. If specified as a string, the domain with that name is looked up
in the root fragment of the simulation.
if_exists : bool
If ``False`` (the default), raise an error if the driven domain is specified as
a string and the root fragment does not have such a domain. If ``True``, do nothing
in this case.
"""
if isinstance(domain, ClockDomain):
pass
elif domain in self._fragment.domains:
domain = self._fragment.domains[domain]
elif if_exists:
return
else:
raise ValueError("Domain {!r} is not present in simulation"
.format(domain))
if domain in self._clocked:
raise ValueError("Domain {!r} already has a clock driving it"
.format(domain.name))
if phase is None:
# By default, delay the first edge by half period. This causes any synchronous activity
# to happen at a non-zero time, distinguishing it from the reset values in the waveform
# viewer.
phase = period / 2
self._engine.add_clock_process(domain.clk, phase=phase, period=period)
self._clocked.add(domain)
def reset(self):
"""Reset the simulation.
Assign the reset value to every signal in the simulation, and restart every user process.
"""
self._engine.reset()
# TODO(nmigen-0.4): replace with _real_step
@deprecated("instead of `sim.step()`, use `sim.advance()`")
def step(self):
return self.advance()
def advance(self):
"""Advance the simulation.
Run every process and commit changes until a fixed point is reached, then advance time
to the closest deadline (if any). If there is an unstable combinatorial loop,
this function will never return.
Returns ``True`` if there are any active processes, ``False`` otherwise.
"""
return self._engine.advance()
def run(self):
"""Run the simulation while any processes are active.
Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
Processes compiled from HDL and added with :meth:`add_clock` are always passive.
"""
while self.advance():
pass
def run_until(self, deadline, *, run_passive=False):
"""Run the simulation until it advances to ``deadline``.
If ``run_passive`` is ``False``, the simulation also stops when there are no active
processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
advances to or past ``deadline``.
If the simulation stops advancing, this function will never return.
"""
assert self._engine.now <= deadline
while (self.advance() or run_passive) and self._engine.now < deadline:
pass
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
"""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: ::
sim = Simulator(frag)
sim.add_clock(1e-6)
with sim.write_vcd("dump.vcd", "dump.gtkw"):
sim.run_until(1e-3)
Arguments
---------
vcd_file : str or file-like object
Verilog Value Change Dump file or filename.
gtkw_file : str or file-like object
GTKWave save file or filename.
traces : iterable of Signal
Signals to display traces for.
"""
if self._engine.now != 0.0:
for file in (vcd_file, gtkw_file):
if hasattr(file, "close"):
file.close()
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)

View file

@ -1,20 +1,17 @@
from contextlib import contextmanager from contextlib import contextmanager
import itertools import itertools
import inspect
from vcd import VCDWriter from vcd import VCDWriter
from vcd.gtkw import GTKWSave from vcd.gtkw import GTKWSave
from .._utils import deprecated
from ..hdl import * from ..hdl import *
from ..hdl.ast import SignalDict from ..hdl.ast import SignalDict
from ._cmds import * from ._base import *
from ._core import *
from ._pyrtl import _FragmentCompiler from ._pyrtl import _FragmentCompiler
from ._pycoro import PyCoroProcess from ._pycoro import PyCoroProcess
from ._pyclock import PyClockProcess from ._pyclock import PyClockProcess
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"] __all__ = ["PySimEngine"]
class _NameExtractor: class _NameExtractor:
@ -49,15 +46,7 @@ class _NameExtractor:
return self.names return self.names
class _WaveformWriter: class _VCDWriter:
def update(self, timestamp, signal, value):
raise NotImplementedError # :nocov:
def close(self, timestamp):
raise NotImplementedError # :nocov:
class _VCDWaveformWriter(_WaveformWriter):
@staticmethod @staticmethod
def timestamp_to_vcd(timestamp): def timestamp_to_vcd(timestamp):
return timestamp * (10 ** 10) # 1/(100 ps) return timestamp * (10 ** 10) # 1/(100 ps)
@ -162,7 +151,55 @@ class _VCDWaveformWriter(_WaveformWriter):
self.gtkw_file.close() self.gtkw_file.close()
class _SignalState: class _Timeline:
def __init__(self):
self.now = 0.0
self.deadlines = dict()
def reset(self):
self.now = 0.0
self.deadlines.clear()
def at(self, run_at, process):
assert process not in self.deadlines
self.deadlines[process] = run_at
def delay(self, delay_by, process):
if delay_by is None:
run_at = self.now
else:
run_at = self.now + delay_by
self.at(run_at, process)
def advance(self):
nearest_processes = set()
nearest_deadline = None
for process, deadline in self.deadlines.items():
if deadline is None:
if nearest_deadline is not None:
nearest_processes.clear()
nearest_processes.add(process)
nearest_deadline = self.now
break
elif nearest_deadline is None or deadline <= nearest_deadline:
assert deadline >= self.now
if nearest_deadline is not None and deadline < nearest_deadline:
nearest_processes.clear()
nearest_processes.add(process)
nearest_deadline = deadline
if not nearest_processes:
return False
for process in nearest_processes:
process.runnable = True
del self.deadlines[process]
self.now = nearest_deadline
return True
class _PySignalState(BaseSignalState):
__slots__ = ("signal", "curr", "next", "waiters", "pending") __slots__ = ("signal", "curr", "next", "waiters", "pending")
def __init__(self, signal, pending): def __init__(self, signal, pending):
@ -189,9 +226,9 @@ class _SignalState:
return awoken_any return awoken_any
class _SimulatorState: class _PySimulation(BaseSimulation):
def __init__(self): def __init__(self):
self.timeline = Timeline() self.timeline = _Timeline()
self.signals = SignalDict() self.signals = SignalDict()
self.slots = [] self.slots = []
self.pending = set() self.pending = set()
@ -207,7 +244,7 @@ class _SimulatorState:
return self.signals[signal] return self.signals[signal]
except KeyError: except KeyError:
index = len(self.slots) index = len(self.slots)
self.slots.append(_SignalState(signal, self.pending)) self.slots.append(_PySignalState(signal, self.pending))
self.signals[signal] = index self.signals[signal] = index
return index return index
@ -222,6 +259,9 @@ class _SimulatorState:
assert process in self.slots[index].waiters assert process in self.slots[index].waiters
del self.slots[index].waiters[process] del self.slots[index].waiters[process]
def wait_interval(self, process, interval):
self.timeline.delay(interval, process)
def commit(self): def commit(self):
converged = True converged = True
for signal_state in self.pending: for signal_state in self.pending:
@ -231,98 +271,29 @@ class _SimulatorState:
return converged return converged
class Simulator: class PySimEngine(BaseEngine):
def __init__(self, fragment): def __init__(self, fragment):
self._state = _SimulatorState() self._state = _PySimulation()
self._fragment = Fragment.get(fragment, platform=None).prepare() self._timeline = self._state.timeline
self._fragment = fragment
self._processes = _FragmentCompiler(self._state)(self._fragment) self._processes = _FragmentCompiler(self._state)(self._fragment)
self._clocked = set() self._vcd_writers = []
self._waveform_writers = []
def _check_process(self, process): def add_coroutine_process(self, process, *, default_cmd):
if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
raise TypeError("Cannot add a process {!r} because it is not a generator function"
.format(process))
return process
def _add_coroutine_process(self, process, *, default_cmd):
self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process, self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process,
default_cmd=default_cmd)) default_cmd=default_cmd))
def add_process(self, process): def add_clock_process(self, clock, *, phase, period):
process = self._check_process(process) self._processes.add(PyClockProcess(self._state, clock,
def wrapper(): phase=phase, period=period))
# Only start a bench process after comb settling, so that the reset values are correct.
yield Settle()
yield from process()
self._add_coroutine_process(wrapper, default_cmd=None)
def add_sync_process(self, process, *, domain="sync"):
process = self._check_process(process)
def wrapper():
# Only start a sync process after the first clock edge (or reset edge, if the domain
# uses an asynchronous reset). This matches the behavior of synchronous FFs.
yield Tick(domain)
yield from process()
return self._add_coroutine_process(wrapper, default_cmd=Tick(domain))
def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
"""Add a clock process.
Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.
Arguments
---------
period : float
Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
seconds.
phase : None or float
Clock phase. The process will wait ``phase`` seconds before the first clock transition.
If not specified, defaults to ``period / 2``.
domain : str or ClockDomain
Driven clock domain. If specified as a string, the domain with that name is looked up
in the root fragment of the simulation.
if_exists : bool
If ``False`` (the default), raise an error if the driven domain is specified as
a string and the root fragment does not have such a domain. If ``True``, do nothing
in this case.
"""
if isinstance(domain, ClockDomain):
pass
elif domain in self._fragment.domains:
domain = self._fragment.domains[domain]
elif if_exists:
return
else:
raise ValueError("Domain {!r} is not present in simulation"
.format(domain))
if domain in self._clocked:
raise ValueError("Domain {!r} already has a clock driving it"
.format(domain.name))
if phase is None:
# By default, delay the first edge by half period. This causes any synchronous activity
# to happen at a non-zero time, distinguishing it from the reset values in the waveform
# viewer.
phase = period / 2
self._processes.add(PyClockProcess(self._state, domain.clk, phase=phase, period=period))
self._clocked.add(domain)
def reset(self): def reset(self):
"""Reset the simulation.
Assign the reset value to every signal in the simulation, and restart every user process.
"""
self._state.reset() self._state.reset()
for process in self._processes: for process in self._processes:
process.reset() process.reset()
def _real_step(self): def _step(self):
"""Step the simulation.
Run every process and commit changes until a fixed point is reached. If there is
an unstable combinatorial loop, this function will never return.
"""
# 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:
@ -332,86 +303,30 @@ class Simulator:
process.runnable = False process.runnable = False
process.run() process.run()
for waveform_writer in self._waveform_writers: for vcd_writer in self._vcd_writers:
for signal_state in self._state.pending: for signal_state in self._state.pending:
waveform_writer.update(self._state.timeline.now, vcd_writer.update(self._timeline.now,
signal_state.signal, signal_state.next) signal_state.signal, signal_state.next)
# 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() converged = self._state.commit()
# TODO(nmigen-0.4): replace with _real_step
@deprecated("instead of `sim.step()`, use `sim.advance()`")
def step(self):
return self.advance()
def advance(self): def advance(self):
"""Advance the simulation. self._step()
self._timeline.advance()
Run every process and commit changes until a fixed point is reached, then advance time
to the closest deadline (if any). If there is an unstable combinatorial loop,
this function will never return.
Returns ``True`` if there are any active processes, ``False`` otherwise.
"""
self._real_step()
self._state.timeline.advance()
return any(not process.passive for process in self._processes) return any(not process.passive for process in self._processes)
def run(self): @property
"""Run the simulation while any processes are active. def now(self):
return self._timeline.now
Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
Processes compiled from HDL and added with :meth:`add_clock` are always passive.
"""
while self.advance():
pass
def run_until(self, deadline, *, run_passive=False):
"""Run the simulation until it advances to ``deadline``.
If ``run_passive`` is ``False``, the simulation also stops when there are no active
processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
advances to or past ``deadline``.
If the simulation stops advancing, this function will never return.
"""
assert self._state.timeline.now <= deadline
while (self.advance() or run_passive) and self._state.timeline.now < deadline:
pass
@contextmanager @contextmanager
def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()): def write_vcd(self, *, vcd_file, gtkw_file, traces):
"""Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file. vcd_writer = _VCDWriter(self._fragment,
This method returns a context manager. It can be used as: ::
sim = Simulator(frag)
sim.add_clock(1e-6)
with sim.write_vcd("dump.vcd", "dump.gtkw"):
sim.run_until(1e-3)
Arguments
---------
vcd_file : str or file-like object
Verilog Value Change Dump file or filename.
gtkw_file : str or file-like object
GTKWave save file or filename.
traces : iterable of Signal
Signals to display traces for.
"""
if self._state.timeline.now != 0.0:
for file in (vcd_file, gtkw_file):
if hasattr(file, "close"):
file.close()
raise ValueError("Cannot start writing waveforms after advancing simulation time")
waveform_writer = _VCDWaveformWriter(self._fragment,
vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces) vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
try: try:
self._waveform_writers.append(waveform_writer) self._vcd_writers.append(vcd_writer)
yield yield
finally: finally:
waveform_writer.close(self._state.timeline.now) vcd_writer.close(self._timeline.now)
self._waveform_writers.remove(waveform_writer) self._vcd_writers.remove(vcd_writer)

View file

@ -7,5 +7,5 @@ __all__ = ["LatticeMachXO2Platform"]
# TODO(nmigen-0.4): remove # TODO(nmigen-0.4): remove
warnings.warn("instead of vendor.lattice_machxo2, use vendor.lattice_machxo_2_3l", warnings.warn("instead of nmigen.vendor.lattice_machxo2, use nmigen.vendor.lattice_machxo_2_3l",
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)

View file

@ -1,7 +1,7 @@
# nmigen: UnusedElaboratable=no # nmigen: UnusedElaboratable=no
from nmigen.hdl import * from nmigen.hdl import *
from nmigen.back.pysim import * from nmigen.sim import *
from nmigen.lib.cdc import * from nmigen.lib.cdc import *
from .utils import * from .utils import *

View file

@ -1,6 +1,6 @@
from nmigen.hdl import * from nmigen.hdl import *
from nmigen.asserts import * from nmigen.asserts import *
from nmigen.back.pysim import * from nmigen.sim import *
from nmigen.lib.coding import * from nmigen.lib.coding import *
from .utils import * from .utils import *

View file

@ -2,7 +2,7 @@
from nmigen.hdl import * from nmigen.hdl import *
from nmigen.asserts import * from nmigen.asserts import *
from nmigen.back.pysim import * from nmigen.sim import *
from nmigen.lib.fifo import * from nmigen.lib.fifo import *
from .utils import * from .utils import *

View file

@ -1,6 +1,6 @@
from nmigen.hdl import * from nmigen.hdl import *
from nmigen.hdl.rec import * from nmigen.hdl.rec import *
from nmigen.back.pysim import * from nmigen.sim import *
from nmigen.lib.io import * from nmigen.lib.io import *
from .utils import * from .utils import *

View file

@ -4,7 +4,7 @@ import unittest
from nmigen.hdl import * from nmigen.hdl import *
from nmigen.asserts import * from nmigen.asserts import *
from nmigen.sim.pysim import * from nmigen.sim import *
from nmigen.lib.scheduler import * from nmigen.lib.scheduler import *
from .utils import * from .utils import *

View file

@ -8,7 +8,7 @@ from nmigen.hdl.mem import *
from nmigen.hdl.rec import * from nmigen.hdl.rec import *
from nmigen.hdl.dsl import * from nmigen.hdl.dsl import *
from nmigen.hdl.ir import * from nmigen.hdl.ir import *
from nmigen.back.pysim import * from nmigen.sim import *
from .utils import * from .utils import *