sim.pysim: in write_vcd(), close files if an exception is raised.
[nmigen.git] / nmigen / sim / pysim.py
1 from contextlib import contextmanager
2 import itertools
3 import inspect
4 from vcd import VCDWriter
5 from vcd.gtkw import GTKWSave
6
7 from .._utils import deprecated
8 from ..hdl import *
9 from ..hdl.ast import SignalDict
10 from ._cmds import *
11 from ._core import *
12 from ._pyrtl import _FragmentCompiler
13 from ._pycoro import PyCoroProcess
14 from ._pyclock import PyClockProcess
15
16
17 __all__ = ["Settle", "Delay", "Tick", "Passive", "Active", "Simulator"]
18
19
20 class _NameExtractor:
21 def __init__(self):
22 self.names = SignalDict()
23
24 def __call__(self, fragment, *, hierarchy=("top",)):
25 def add_signal_name(signal):
26 hierarchical_signal_name = (*hierarchy, signal.name)
27 if signal not in self.names:
28 self.names[signal] = {hierarchical_signal_name}
29 else:
30 self.names[signal].add(hierarchical_signal_name)
31
32 for domain_name, domain_signals in fragment.drivers.items():
33 if domain_name is not None:
34 domain = fragment.domains[domain_name]
35 add_signal_name(domain.clk)
36 if domain.rst is not None:
37 add_signal_name(domain.rst)
38
39 for statement in fragment.statements:
40 for signal in statement._lhs_signals() | statement._rhs_signals():
41 if not isinstance(signal, (ClockSignal, ResetSignal)):
42 add_signal_name(signal)
43
44 for subfragment_index, (subfragment, subfragment_name) in enumerate(fragment.subfragments):
45 if subfragment_name is None:
46 subfragment_name = "U${}".format(subfragment_index)
47 self(subfragment, hierarchy=(*hierarchy, subfragment_name))
48
49 return self.names
50
51
52 class _WaveformWriter:
53 def update(self, timestamp, signal, value):
54 raise NotImplementedError # :nocov:
55
56 def close(self, timestamp):
57 raise NotImplementedError # :nocov:
58
59
60 class _VCDWaveformWriter(_WaveformWriter):
61 @staticmethod
62 def timestamp_to_vcd(timestamp):
63 return timestamp * (10 ** 10) # 1/(100 ps)
64
65 @staticmethod
66 def decode_to_vcd(signal, value):
67 return signal.decoder(value).expandtabs().replace(" ", "_")
68
69 def __init__(self, fragment, *, vcd_file, gtkw_file=None, traces=()):
70 if isinstance(vcd_file, str):
71 vcd_file = open(vcd_file, "wt")
72 if isinstance(gtkw_file, str):
73 gtkw_file = open(gtkw_file, "wt")
74
75 self.vcd_vars = SignalDict()
76 self.vcd_file = vcd_file
77 self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
78 timescale="100 ps", comment="Generated by nMigen")
79
80 self.gtkw_names = SignalDict()
81 self.gtkw_file = gtkw_file
82 self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file)
83
84 self.traces = []
85
86 signal_names = _NameExtractor()(fragment)
87
88 trace_names = SignalDict()
89 for trace in traces:
90 if trace not in signal_names:
91 trace_names[trace] = {("top", trace.name)}
92 self.traces.append(trace)
93
94 if self.vcd_writer is None:
95 return
96
97 for signal, names in itertools.chain(signal_names.items(), trace_names.items()):
98 if signal.decoder:
99 var_type = "string"
100 var_size = 1
101 var_init = self.decode_to_vcd(signal, signal.reset)
102 else:
103 var_type = "wire"
104 var_size = signal.width
105 var_init = signal.reset
106
107 for (*var_scope, var_name) in names:
108 suffix = None
109 while True:
110 try:
111 if suffix is None:
112 var_name_suffix = var_name
113 else:
114 var_name_suffix = "{}${}".format(var_name, suffix)
115 if signal not in self.vcd_vars:
116 vcd_var = self.vcd_writer.register_var(
117 scope=var_scope, name=var_name_suffix,
118 var_type=var_type, size=var_size, init=var_init)
119 self.vcd_vars[signal] = vcd_var
120 else:
121 self.vcd_writer.register_alias(
122 scope=var_scope, name=var_name_suffix,
123 var=self.vcd_vars[signal])
124 break
125 except KeyError:
126 suffix = (suffix or 0) + 1
127
128 if signal not in self.gtkw_names:
129 self.gtkw_names[signal] = (*var_scope, var_name_suffix)
130
131 def update(self, timestamp, signal, value):
132 vcd_var = self.vcd_vars.get(signal)
133 if vcd_var is None:
134 return
135
136 vcd_timestamp = self.timestamp_to_vcd(timestamp)
137 if signal.decoder:
138 var_value = self.decode_to_vcd(signal, value)
139 else:
140 var_value = value
141 self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)
142
143 def close(self, timestamp):
144 if self.vcd_writer is not None:
145 self.vcd_writer.close(self.timestamp_to_vcd(timestamp))
146
147 if self.gtkw_save is not None:
148 self.gtkw_save.dumpfile(self.vcd_file.name)
149 self.gtkw_save.dumpfile_size(self.vcd_file.tell())
150
151 self.gtkw_save.treeopen("top")
152 for signal in self.traces:
153 if len(signal) > 1 and not signal.decoder:
154 suffix = "[{}:0]".format(len(signal) - 1)
155 else:
156 suffix = ""
157 self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix)
158
159 if self.vcd_file is not None:
160 self.vcd_file.close()
161 if self.gtkw_file is not None:
162 self.gtkw_file.close()
163
164
165 class _SignalState:
166 __slots__ = ("signal", "curr", "next", "waiters", "pending")
167
168 def __init__(self, signal, pending):
169 self.signal = signal
170 self.pending = pending
171 self.waiters = dict()
172 self.curr = self.next = signal.reset
173
174 def set(self, value):
175 if self.next == value:
176 return
177 self.next = value
178 self.pending.add(self)
179
180 def commit(self):
181 if self.curr == self.next:
182 return False
183 self.curr = self.next
184
185 awoken_any = False
186 for process, trigger in self.waiters.items():
187 if trigger is None or trigger == self.curr:
188 process.runnable = awoken_any = True
189 return awoken_any
190
191
192 class _SimulatorState:
193 def __init__(self):
194 self.timeline = Timeline()
195 self.signals = SignalDict()
196 self.slots = []
197 self.pending = set()
198
199 def reset(self):
200 self.timeline.reset()
201 for signal, index in self.signals.items():
202 self.slots[index].curr = self.slots[index].next = signal.reset
203 self.pending.clear()
204
205 def get_signal(self, signal):
206 try:
207 return self.signals[signal]
208 except KeyError:
209 index = len(self.slots)
210 self.slots.append(_SignalState(signal, self.pending))
211 self.signals[signal] = index
212 return index
213
214 def add_trigger(self, process, signal, *, trigger=None):
215 index = self.get_signal(signal)
216 assert (process not in self.slots[index].waiters or
217 self.slots[index].waiters[process] == trigger)
218 self.slots[index].waiters[process] = trigger
219
220 def remove_trigger(self, process, signal):
221 index = self.get_signal(signal)
222 assert process in self.slots[index].waiters
223 del self.slots[index].waiters[process]
224
225 def commit(self):
226 converged = True
227 for signal_state in self.pending:
228 if signal_state.commit():
229 converged = False
230 self.pending.clear()
231 return converged
232
233
234 class Simulator:
235 def __init__(self, fragment):
236 self._state = _SimulatorState()
237 self._fragment = Fragment.get(fragment, platform=None).prepare()
238 self._processes = _FragmentCompiler(self._state)(self._fragment)
239 self._clocked = set()
240 self._waveform_writers = []
241
242 def _check_process(self, process):
243 if not (inspect.isgeneratorfunction(process) or inspect.iscoroutinefunction(process)):
244 raise TypeError("Cannot add a process {!r} because it is not a generator function"
245 .format(process))
246 return process
247
248 def _add_coroutine_process(self, process, *, default_cmd):
249 self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process,
250 default_cmd=default_cmd))
251
252 def add_process(self, process):
253 process = self._check_process(process)
254 def wrapper():
255 # Only start a bench process after comb settling, so that the reset values are correct.
256 yield Settle()
257 yield from process()
258 self._add_coroutine_process(wrapper, default_cmd=None)
259
260 def add_sync_process(self, process, *, domain="sync"):
261 process = self._check_process(process)
262 def wrapper():
263 # Only start a sync process after the first clock edge (or reset edge, if the domain
264 # uses an asynchronous reset). This matches the behavior of synchronous FFs.
265 yield Tick(domain)
266 yield from process()
267 return self._add_coroutine_process(wrapper, default_cmd=Tick(domain))
268
269 def add_clock(self, period, *, phase=None, domain="sync", if_exists=False):
270 """Add a clock process.
271
272 Adds a process that drives the clock signal of ``domain`` at a 50% duty cycle.
273
274 Arguments
275 ---------
276 period : float
277 Clock period. The process will toggle the ``domain`` clock signal every ``period / 2``
278 seconds.
279 phase : None or float
280 Clock phase. The process will wait ``phase`` seconds before the first clock transition.
281 If not specified, defaults to ``period / 2``.
282 domain : str or ClockDomain
283 Driven clock domain. If specified as a string, the domain with that name is looked up
284 in the root fragment of the simulation.
285 if_exists : bool
286 If ``False`` (the default), raise an error if the driven domain is specified as
287 a string and the root fragment does not have such a domain. If ``True``, do nothing
288 in this case.
289 """
290 if isinstance(domain, ClockDomain):
291 pass
292 elif domain in self._fragment.domains:
293 domain = self._fragment.domains[domain]
294 elif if_exists:
295 return
296 else:
297 raise ValueError("Domain {!r} is not present in simulation"
298 .format(domain))
299 if domain in self._clocked:
300 raise ValueError("Domain {!r} already has a clock driving it"
301 .format(domain.name))
302
303 if phase is None:
304 # By default, delay the first edge by half period. This causes any synchronous activity
305 # to happen at a non-zero time, distinguishing it from the reset values in the waveform
306 # viewer.
307 phase = period / 2
308 self._processes.add(PyClockProcess(self._state, domain.clk, phase=phase, period=period))
309 self._clocked.add(domain)
310
311 def reset(self):
312 """Reset the simulation.
313
314 Assign the reset value to every signal in the simulation, and restart every user process.
315 """
316 self._state.reset()
317 for process in self._processes:
318 process.reset()
319
320 def _real_step(self):
321 """Step the simulation.
322
323 Run every process and commit changes until a fixed point is reached. If there is
324 an unstable combinatorial loop, this function will never return.
325 """
326 # Performs the two phases of a delta cycle in a loop:
327 converged = False
328 while not converged:
329 # 1. eval: run and suspend every non-waiting process once, queueing signal changes
330 for process in self._processes:
331 if process.runnable:
332 process.runnable = False
333 process.run()
334
335 for waveform_writer in self._waveform_writers:
336 for signal_state in self._state.pending:
337 waveform_writer.update(self._state.timeline.now,
338 signal_state.signal, signal_state.next)
339
340 # 2. commit: apply every queued signal change, waking up any waiting processes
341 converged = self._state.commit()
342
343 # TODO(nmigen-0.4): replace with _real_step
344 @deprecated("instead of `sim.step()`, use `sim.advance()`")
345 def step(self):
346 return self.advance()
347
348 def advance(self):
349 """Advance the simulation.
350
351 Run every process and commit changes until a fixed point is reached, then advance time
352 to the closest deadline (if any). If there is an unstable combinatorial loop,
353 this function will never return.
354
355 Returns ``True`` if there are any active processes, ``False`` otherwise.
356 """
357 self._real_step()
358 self._state.timeline.advance()
359 return any(not process.passive for process in self._processes)
360
361 def run(self):
362 """Run the simulation while any processes are active.
363
364 Processes added with :meth:`add_process` and :meth:`add_sync_process` are initially active,
365 and may change their status using the ``yield Passive()`` and ``yield Active()`` commands.
366 Processes compiled from HDL and added with :meth:`add_clock` are always passive.
367 """
368 while self.advance():
369 pass
370
371 def run_until(self, deadline, *, run_passive=False):
372 """Run the simulation until it advances to ``deadline``.
373
374 If ``run_passive`` is ``False``, the simulation also stops when there are no active
375 processes, similar to :meth:`run`. Otherwise, the simulation will stop only after it
376 advances to or past ``deadline``.
377
378 If the simulation stops advancing, this function will never return.
379 """
380 assert self._state.timeline.now <= deadline
381 while (self.advance() or run_passive) and self._state.timeline.now < deadline:
382 pass
383
384 @contextmanager
385 def write_vcd(self, vcd_file, gtkw_file=None, *, traces=()):
386 """Write waveforms to a Value Change Dump file, optionally populating a GTKWave save file.
387
388 This method returns a context manager. It can be used as: ::
389
390 sim = Simulator(frag)
391 sim.add_clock(1e-6)
392 with sim.write_vcd("dump.vcd", "dump.gtkw"):
393 sim.run_until(1e-3)
394
395 Arguments
396 ---------
397 vcd_file : str or file-like object
398 Verilog Value Change Dump file or filename.
399 gtkw_file : str or file-like object
400 GTKWave save file or filename.
401 traces : iterable of Signal
402 Signals to display traces for.
403 """
404 if self._state.timeline.now != 0.0:
405 for file in (vcd_file, gtkw_file):
406 if hasattr(file, "close"):
407 file.close()
408 raise ValueError("Cannot start writing waveforms after advancing simulation time")
409
410 waveform_writer = _VCDWaveformWriter(self._fragment,
411 vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
412 try:
413 self._waveform_writers.append(waveform_writer)
414 yield
415 finally:
416 waveform_writer.close(self._state.timeline.now)
417 self._waveform_writers.remove(waveform_writer)