From cb998d891b73e5db8569aaac62f3ce8858a89070 Mon Sep 17 00:00:00 2001 From: whitequark Date: Fri, 14 Dec 2018 07:26:26 +0000 Subject: [PATCH] back.pysim: explain how delta cycles work. --- nmigen/back/pysim.py | 119 ++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/nmigen/back/pysim.py b/nmigen/back/pysim.py index 478a738..894d37b 100644 --- a/nmigen/back/pysim.py +++ b/nmigen/back/pysim.py @@ -184,9 +184,11 @@ class _StatementCompiler(StatementTransformer): class Simulator: def __init__(self, fragment=None, vcd_file=None): self._fragments = {} # fragment -> hierarchy + self._domains = {} # str/domain -> ClockDomain self._domain_triggers = ValueDict() # Signal -> str/domain self._domain_signals = {} # str/domain -> {Signal} + self._signals = ValueSet() # {Signal} self._comb_signals = ValueSet() # {Signal} self._sync_signals = ValueSet() # {Signal} @@ -194,6 +196,7 @@ class Simulator: self._started = False self._timestamp = 0. + self._epsilon = 1e-10 self._state = _State() self._processes = set() # {process} @@ -202,7 +205,7 @@ class Simulator: self._wait_deadline = {} # process -> float/timestamp self._wait_tick = {} # process -> str/domain - self._handlers = ValueDict() # Signal -> set(lambda) + self._funclets = ValueDict() # Signal -> set(lambda) self._vcd_file = vcd_file self._vcd_writer = None @@ -255,10 +258,10 @@ class Simulator: return "{}_{}".format(name, signal.name) return signal.name - def _add_handler(self, signal, handler): - if signal not in self._handlers: - self._handlers[signal] = set() - self._handlers[signal].add(handler) + def _add_funclet(self, signal, funclet): + if signal not in self._funclets: + self._funclets[signal] = set() + self._funclets[signal].add(funclet) def __enter__(self): if self._vcd_file: @@ -298,51 +301,92 @@ class Simulator: self._domain_signals[domain].update(signals) compiler = _StatementCompiler() - handler = compiler(fragment.statements) + funclet = compiler(fragment.statements) for signal in compiler.sensitivity: - self._add_handler(signal, handler) + self._add_funclet(signal, funclet) for domain, cd in fragment.domains.items(): - self._add_handler(cd.clk, handler) + self._add_funclet(cd.clk, funclet) if cd.rst is not None: - self._add_handler(cd.rst, handler) + self._add_funclet(cd.rst, funclet) self._user_signals = self._signals - self._comb_signals - self._sync_signals - def _commit_signal(self, signal): + def _update_dirty_signals(self): + """Perform the statement part of IR processes (aka RTLIL case).""" + # First, for all dirty signals, use sensitivity lists to determine the set of fragments + # that need their statements to be reevaluated because the signals changed at the previous + # delta cycle. + funclets = set() + while self._state.curr_dirty: + signal = self._state.curr_dirty.pop() + if signal in self._funclets: + funclets.update(self._funclets[signal]) + + # Second, compute the values of all signals at the start of the next delta cycle, by + # running precompiled statements. + for funclet in funclets: + funclet(self._state) + + def _commit_signal(self, signal, domains): + """Perform the driver part of IR processes (aka RTLIL sync), for individual signals.""" + # Take the computed value (at the start of this delta cycle) of a signal (that could have + # come from an IR process that ran earlier, or modified by a simulator process) and update + # the value for this delta cycle. old, new = self._state.commit(signal) - if (old, new) == (0, 1) and signal in self._domain_triggers: - domain = self._domain_triggers[signal] - for sync_signal in self._state.next_dirty: - if sync_signal in self._domain_signals[domain]: - self._commit_signal(sync_signal) - for proc, wait_domain in list(self._wait_tick.items()): - if domain == wait_domain: - del self._wait_tick[proc] - self._suspended.remove(proc) + # If the signal is a clock that triggers synchronous logic, record that fact. + if (old, new) == (0, 1) and signal in self._domain_triggers: + domains.add(self._domain_triggers[signal]) if self._vcd_writer: + # Finally, dump the new value to the VCD file. for vcd_signal in self._vcd_signals[signal]: - self._vcd_writer.change(vcd_signal, self._timestamp * 1e10, new) - - def _handle_event(self): - handlers = set() - while self._state.curr_dirty: - signal = self._state.curr_dirty.pop() - if signal in self._handlers: - handlers.update(self._handlers[signal]) - - for handler in handlers: - handler(self._state) + self._vcd_writer.change(vcd_signal, self._timestamp / self._epsilon, new) + def _commit_comb_signals(self, domains): + """Perform the comb part of IR processes (aka RTLIL always).""" + # Take the computed value (at the start of this delta cycle) of every comb signal and + # update the value for this delta cycle. for signal in self._state.next_dirty: if signal in self._comb_signals or signal in self._user_signals: - self._commit_signal(signal) + self._commit_signal(signal, domains) + + def _commit_sync_signals(self, domains): + """Perform the sync part of IR processes (aka RTLIL posedge).""" + # At entry, `domains` contains a list of every simultaneously triggered sync update. + while domains: + # Advance the timeline a bit (purely for observational purposes) and commit all of them + # at the same timestamp. + self._timestamp += self._epsilon + curr_domains, domains = domains, set() + + while curr_domains: + domain = curr_domains.pop() + + # Take the computed value (at the start of this delta cycle) of every sync signal + # in this domain and update the value for this delta cycle. This can trigger more + # synchronous logic, so record that. + for signal in self._state.next_dirty: + if signal in self._domain_signals[domain]: + self._commit_signal(signal, domains) + + # Wake up any simulator processes that wait for a domain tick. + for proc, wait_domain in list(self._wait_tick.items()): + if domain == wait_domain: + del self._wait_tick[proc] + self._suspended.remove(proc) + + # Unless handling synchronous logic above has triggered more synchronous logic (which + # can happen e.g. if a domain is clocked off a clock divisor in fabric), we're done. + # Otherwise, do one more round of updates. def _force_signal(self, signal, value): assert signal in self._user_signals self._state.set_next(signal, value) - self._commit_signal(signal) + + domains = set() + self._commit_signal(signal, domains) + self._commit_sync_signals(domains) def _run_process(self, proc): try: @@ -371,8 +415,12 @@ class Simulator: def step(self, run_passive=False): # Are there any delta cycles we should run? while self._state.curr_dirty: - self._timestamp += 1e-10 - self._handle_event() + self._timestamp += self._epsilon + + domains = set() + self._update_dirty_signals() + self._commit_comb_signals(domains) + self._commit_sync_signals(domains) # Are there any processes that haven't had a chance to run yet? if len(self._processes) > len(self._suspended): @@ -400,8 +448,9 @@ class Simulator: while self._timestamp < deadline: if not self.step(run_passive): return False + return True def __exit__(self, *args): if self._vcd_writer: - self._vcd_writer.close(self._timestamp * 1e10) + self._vcd_writer.close(self._timestamp / self._epsilon) -- 2.30.2