From d33779cabee53973330d458c27491898cb74a9e3 Mon Sep 17 00:00:00 2001 From: whitequark Date: Thu, 27 Aug 2020 10:17:02 +0000 Subject: [PATCH] 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. --- examples/basic/ctr_en.py | 5 +- nmigen/back/pysim.py | 4 +- nmigen/compat/sim/__init__.py | 2 +- nmigen/sim/__init__.py | 4 + nmigen/sim/_base.py | 67 +++++++++ nmigen/sim/_cmds.py | 46 ------ nmigen/sim/_core.py | 63 -------- nmigen/sim/_pyclock.py | 13 +- nmigen/sim/_pycoro.py | 11 +- nmigen/sim/_pyrtl.py | 15 +- nmigen/sim/core.py | 206 ++++++++++++++++++++++++++ nmigen/sim/pysim.py | 247 ++++++++++--------------------- nmigen/vendor/lattice_machxo2.py | 2 +- tests/test_lib_cdc.py | 2 +- tests/test_lib_coding.py | 2 +- tests/test_lib_fifo.py | 2 +- tests/test_lib_io.py | 2 +- tests/test_lib_scheduler.py | 2 +- tests/test_sim.py | 2 +- 19 files changed, 396 insertions(+), 301 deletions(-) create mode 100644 nmigen/sim/_base.py delete mode 100644 nmigen/sim/_cmds.py delete mode 100644 nmigen/sim/_core.py create mode 100644 nmigen/sim/core.py diff --git a/examples/basic/ctr_en.py b/examples/basic/ctr_en.py index becc7c9..d5e3279 100644 --- a/examples/basic/ctr_en.py +++ b/examples/basic/ctr_en.py @@ -1,5 +1,6 @@ from nmigen import * -from nmigen.back import rtlil, verilog, pysim +from nmigen.sim import * +from nmigen.back import rtlil, verilog class Counter(Elaboratable): @@ -19,7 +20,7 @@ ctr = Counter(width=16) print(verilog.convert(ctr, ports=[ctr.o, ctr.en])) -sim = pysim.Simulator(ctr) +sim = Simulator(ctr) sim.add_clock(1e-6) def ce_proc(): yield; yield; yield diff --git a/nmigen/back/pysim.py b/nmigen/back/pysim.py index 039265a..c35f073 100644 --- a/nmigen/back/pysim.py +++ b/nmigen/back/pysim.py @@ -1,11 +1,11 @@ import warnings -from ..sim.pysim import * +from ..sim import * __all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"] # 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) diff --git a/nmigen/compat/sim/__init__.py b/nmigen/compat/sim/__init__.py index b2cc87e..4c3d063 100644 --- a/nmigen/compat/sim/__init__.py +++ b/nmigen/compat/sim/__init__.py @@ -2,8 +2,8 @@ import functools import inspect from collections.abc import Iterable from ...hdl.cd import ClockDomain -from ...back.pysim import * from ...hdl.ir import Fragment +from ...sim import * __all__ = ["run_simulation", "passive"] diff --git a/nmigen/sim/__init__.py b/nmigen/sim/__init__.py index e69de29..c239c52 100644 --- a/nmigen/sim/__init__.py +++ b/nmigen/sim/__init__.py @@ -0,0 +1,4 @@ +from .core import * + + +__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"] diff --git a/nmigen/sim/_base.py b/nmigen/sim/_base.py new file mode 100644 index 0000000..ee1061f --- /dev/null +++ b/nmigen/sim/_base.py @@ -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 diff --git a/nmigen/sim/_cmds.py b/nmigen/sim/_cmds.py deleted file mode 100644 index a1cea3e..0000000 --- a/nmigen/sim/_cmds.py +++ /dev/null @@ -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)" diff --git a/nmigen/sim/_core.py b/nmigen/sim/_core.py deleted file mode 100644 index e1af946..0000000 --- a/nmigen/sim/_core.py +++ /dev/null @@ -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 diff --git a/nmigen/sim/_pyclock.py b/nmigen/sim/_pyclock.py index 3e1e6ab..b360802 100644 --- a/nmigen/sim/_pyclock.py +++ b/nmigen/sim/_pyclock.py @@ -1,12 +1,12 @@ import inspect -from ._core import Process +from ._base import BaseProcess __all__ = ["PyClockProcess"] -class PyClockProcess(Process): +class PyClockProcess(BaseProcess): def __init__(self, state, signal, *, phase, period): assert len(signal) == 1 @@ -20,16 +20,17 @@ class PyClockProcess(Process): def reset(self): self.runnable = True self.passive = True + self.initial = True def run(self): + self.runnable = False + if self.initial: self.initial = False - self.state.timeline.delay(self.phase, self) + self.state.wait_interval(self, self.phase) else: clk_state = self.state.slots[self.slot] clk_state.set(not clk_state.curr) - self.state.timeline.delay(self.period / 2, self) - - self.runnable = False + self.state.wait_interval(self, self.period / 2) diff --git a/nmigen/sim/_pycoro.py b/nmigen/sim/_pycoro.py index dbd7601..554187c 100644 --- a/nmigen/sim/_pycoro.py +++ b/nmigen/sim/_pycoro.py @@ -2,15 +2,15 @@ import inspect from ..hdl import * from ..hdl.ast import Statement, SignalSet -from ._cmds import * -from ._core import Process +from .core import Tick, Settle, Delay, Passive, Active +from ._base import BaseProcess from ._pyrtl import _ValueCompiler, _RHSValueCompiler, _StatementCompiler __all__ = ["PyCoroProcess"] -class PyCoroProcess(Process): +class PyCoroProcess(BaseProcess): def __init__(self, state, domains, constructor, *, default_cmd=None): self.state = state self.domains = domains @@ -22,6 +22,7 @@ class PyCoroProcess(Process): def reset(self): self.runnable = True self.passive = False + self.coroutine = self.constructor() self.exec_locals = { "slots": self.state.slots, @@ -90,11 +91,11 @@ class PyCoroProcess(Process): return elif type(command) is Settle: - self.state.timeline.delay(None, self) + self.state.wait_interval(self, None) return elif type(command) is Delay: - self.state.timeline.delay(command.interval, self) + self.state.wait_interval(self, command.interval) return elif type(command) is Passive: diff --git a/nmigen/sim/_pyrtl.py b/nmigen/sim/_pyrtl.py index baf9a5b..d7b4326 100644 --- a/nmigen/sim/_pyrtl.py +++ b/nmigen/sim/_pyrtl.py @@ -5,14 +5,23 @@ from contextlib import contextmanager from ..hdl import * from ..hdl.ast import SignalSet from ..hdl.xfrm import ValueVisitor, StatementVisitor, LHSGroupFilter -from ._core import * +from ._base import BaseProcess __all__ = ["PyRTLProcess"] -class PyRTLProcess(Process): - pass +class PyRTLProcess(BaseProcess): + __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: diff --git a/nmigen/sim/core.py b/nmigen/sim/core.py new file mode 100644 index 0000000..886cea1 --- /dev/null +++ b/nmigen/sim/core.py @@ -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) diff --git a/nmigen/sim/pysim.py b/nmigen/sim/pysim.py index 847a5a1..c3fc176 100644 --- a/nmigen/sim/pysim.py +++ b/nmigen/sim/pysim.py @@ -1,20 +1,17 @@ from contextlib import contextmanager import itertools -import inspect from vcd import VCDWriter from vcd.gtkw import GTKWSave -from .._utils import deprecated from ..hdl import * from ..hdl.ast import SignalDict -from ._cmds import * -from ._core import * +from ._base import * from ._pyrtl import _FragmentCompiler from ._pycoro import PyCoroProcess from ._pyclock import PyClockProcess -__all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"] +__all__ = ["PySimEngine"] class _NameExtractor: @@ -49,15 +46,7 @@ class _NameExtractor: return self.names -class _WaveformWriter: - def update(self, timestamp, signal, value): - raise NotImplementedError # :nocov: - - def close(self, timestamp): - raise NotImplementedError # :nocov: - - -class _VCDWaveformWriter(_WaveformWriter): +class _VCDWriter: @staticmethod def timestamp_to_vcd(timestamp): return timestamp * (10 ** 10) # 1/(100 ps) @@ -162,7 +151,55 @@ class _VCDWaveformWriter(_WaveformWriter): 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") def __init__(self, signal, pending): @@ -189,9 +226,9 @@ class _SignalState: return awoken_any -class _SimulatorState: +class _PySimulation(BaseSimulation): def __init__(self): - self.timeline = Timeline() + self.timeline = _Timeline() self.signals = SignalDict() self.slots = [] self.pending = set() @@ -207,7 +244,7 @@ class _SimulatorState: return self.signals[signal] except KeyError: index = len(self.slots) - self.slots.append(_SignalState(signal, self.pending)) + self.slots.append(_PySignalState(signal, self.pending)) self.signals[signal] = index return index @@ -222,6 +259,9 @@ class _SimulatorState: assert process in self.slots[index].waiters del self.slots[index].waiters[process] + def wait_interval(self, process, interval): + self.timeline.delay(interval, process) + def commit(self): converged = True for signal_state in self.pending: @@ -231,98 +271,29 @@ class _SimulatorState: return converged -class Simulator: +class PySimEngine(BaseEngine): def __init__(self, fragment): - self._state = _SimulatorState() - self._fragment = Fragment.get(fragment, platform=None).prepare() - self._processes = _FragmentCompiler(self._state)(self._fragment) - self._clocked = set() - self._waveform_writers = [] + self._state = _PySimulation() + self._timeline = self._state.timeline - 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 + self._fragment = fragment + self._processes = _FragmentCompiler(self._state)(self._fragment) + self._vcd_writers = [] - def _add_coroutine_process(self, process, *, default_cmd): + def add_coroutine_process(self, process, *, default_cmd): self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process, default_cmd=default_cmd)) - 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._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 add_clock_process(self, clock, *, phase, period): + self._processes.add(PyClockProcess(self._state, clock, + phase=phase, period=period)) def reset(self): - """Reset the simulation. - - Assign the reset value to every signal in the simulation, and restart every user process. - """ self._state.reset() for process in self._processes: process.reset() - def _real_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. - """ + def _step(self): # Performs the two phases of a delta cycle in a loop: converged = False while not converged: @@ -332,86 +303,30 @@ class Simulator: process.runnable = False process.run() - for waveform_writer in self._waveform_writers: + for vcd_writer in self._vcd_writers: 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) # 2. commit: apply every queued signal change, waking up any waiting processes 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): - """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. - """ - self._real_step() - self._state.timeline.advance() + self._step() + self._timeline.advance() return any(not process.passive for process in self._processes) - 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._state.timeline.now <= deadline - while (self.advance() or run_passive) and self._state.timeline.now < deadline: - pass + @property + def now(self): + return self._timeline.now @contextmanager - 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._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, + def write_vcd(self, *, vcd_file, gtkw_file, traces): + vcd_writer = _VCDWriter(self._fragment, vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces) try: - self._waveform_writers.append(waveform_writer) + self._vcd_writers.append(vcd_writer) yield finally: - waveform_writer.close(self._state.timeline.now) - self._waveform_writers.remove(waveform_writer) + vcd_writer.close(self._timeline.now) + self._vcd_writers.remove(vcd_writer) diff --git a/nmigen/vendor/lattice_machxo2.py b/nmigen/vendor/lattice_machxo2.py index 1d01506..b9fadab 100644 --- a/nmigen/vendor/lattice_machxo2.py +++ b/nmigen/vendor/lattice_machxo2.py @@ -7,5 +7,5 @@ __all__ = ["LatticeMachXO2Platform"] # 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) diff --git a/tests/test_lib_cdc.py b/tests/test_lib_cdc.py index f29052a..1ada46d 100644 --- a/tests/test_lib_cdc.py +++ b/tests/test_lib_cdc.py @@ -1,7 +1,7 @@ # nmigen: UnusedElaboratable=no from nmigen.hdl import * -from nmigen.back.pysim import * +from nmigen.sim import * from nmigen.lib.cdc import * from .utils import * diff --git a/tests/test_lib_coding.py b/tests/test_lib_coding.py index c914e9e..f981a13 100644 --- a/tests/test_lib_coding.py +++ b/tests/test_lib_coding.py @@ -1,6 +1,6 @@ from nmigen.hdl import * from nmigen.asserts import * -from nmigen.back.pysim import * +from nmigen.sim import * from nmigen.lib.coding import * from .utils import * diff --git a/tests/test_lib_fifo.py b/tests/test_lib_fifo.py index 83dc825..4722318 100644 --- a/tests/test_lib_fifo.py +++ b/tests/test_lib_fifo.py @@ -2,7 +2,7 @@ from nmigen.hdl import * from nmigen.asserts import * -from nmigen.back.pysim import * +from nmigen.sim import * from nmigen.lib.fifo import * from .utils import * diff --git a/tests/test_lib_io.py b/tests/test_lib_io.py index 234df1d..5970650 100644 --- a/tests/test_lib_io.py +++ b/tests/test_lib_io.py @@ -1,6 +1,6 @@ from nmigen.hdl import * from nmigen.hdl.rec import * -from nmigen.back.pysim import * +from nmigen.sim import * from nmigen.lib.io import * from .utils import * diff --git a/tests/test_lib_scheduler.py b/tests/test_lib_scheduler.py index 3128ad1..a3780ed 100644 --- a/tests/test_lib_scheduler.py +++ b/tests/test_lib_scheduler.py @@ -4,7 +4,7 @@ import unittest from nmigen.hdl import * from nmigen.asserts import * -from nmigen.sim.pysim import * +from nmigen.sim import * from nmigen.lib.scheduler import * from .utils import * diff --git a/tests/test_sim.py b/tests/test_sim.py index 6c125ba..bb806f2 100644 --- a/tests/test_sim.py +++ b/tests/test_sim.py @@ -8,7 +8,7 @@ from nmigen.hdl.mem import * from nmigen.hdl.rec import * from nmigen.hdl.dsl import * from nmigen.hdl.ir import * -from nmigen.back.pysim import * +from nmigen.sim import * from .utils import * -- 2.30.2