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:
parent
9bdb7accc8
commit
b65e11f38f
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
from .core import *
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
|
67
nmigen/sim/_base.py
Normal file
67
nmigen/sim/_base.py
Normal 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
|
|
@ -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)"
|
|
|
@ -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
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
206
nmigen/sim/core.py
Normal 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)
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
|
@ -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 *
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue