sim: split into base, core, and engines.
[nmigen.git] / nmigen / sim / pysim.py
1 from contextlib import contextmanager
2 import itertools
3 from vcd import VCDWriter
4 from vcd.gtkw import GTKWSave
5
6 from ..hdl import *
7 from ..hdl.ast import SignalDict
8 from ._base import *
9 from ._pyrtl import _FragmentCompiler
10 from ._pycoro import PyCoroProcess
11 from ._pyclock import PyClockProcess
12
13
14 __all__ = ["PySimEngine"]
15
16
17 class _NameExtractor:
18 def __init__(self):
19 self.names = SignalDict()
20
21 def __call__(self, fragment, *, hierarchy=("top",)):
22 def add_signal_name(signal):
23 hierarchical_signal_name = (*hierarchy, signal.name)
24 if signal not in self.names:
25 self.names[signal] = {hierarchical_signal_name}
26 else:
27 self.names[signal].add(hierarchical_signal_name)
28
29 for domain_name, domain_signals in fragment.drivers.items():
30 if domain_name is not None:
31 domain = fragment.domains[domain_name]
32 add_signal_name(domain.clk)
33 if domain.rst is not None:
34 add_signal_name(domain.rst)
35
36 for statement in fragment.statements:
37 for signal in statement._lhs_signals() | statement._rhs_signals():
38 if not isinstance(signal, (ClockSignal, ResetSignal)):
39 add_signal_name(signal)
40
41 for subfragment_index, (subfragment, subfragment_name) in enumerate(fragment.subfragments):
42 if subfragment_name is None:
43 subfragment_name = "U${}".format(subfragment_index)
44 self(subfragment, hierarchy=(*hierarchy, subfragment_name))
45
46 return self.names
47
48
49 class _VCDWriter:
50 @staticmethod
51 def timestamp_to_vcd(timestamp):
52 return timestamp * (10 ** 10) # 1/(100 ps)
53
54 @staticmethod
55 def decode_to_vcd(signal, value):
56 return signal.decoder(value).expandtabs().replace(" ", "_")
57
58 def __init__(self, fragment, *, vcd_file, gtkw_file=None, traces=()):
59 if isinstance(vcd_file, str):
60 vcd_file = open(vcd_file, "wt")
61 if isinstance(gtkw_file, str):
62 gtkw_file = open(gtkw_file, "wt")
63
64 self.vcd_vars = SignalDict()
65 self.vcd_file = vcd_file
66 self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
67 timescale="100 ps", comment="Generated by nMigen")
68
69 self.gtkw_names = SignalDict()
70 self.gtkw_file = gtkw_file
71 self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file)
72
73 self.traces = []
74
75 signal_names = _NameExtractor()(fragment)
76
77 trace_names = SignalDict()
78 for trace in traces:
79 if trace not in signal_names:
80 trace_names[trace] = {("top", trace.name)}
81 self.traces.append(trace)
82
83 if self.vcd_writer is None:
84 return
85
86 for signal, names in itertools.chain(signal_names.items(), trace_names.items()):
87 if signal.decoder:
88 var_type = "string"
89 var_size = 1
90 var_init = self.decode_to_vcd(signal, signal.reset)
91 else:
92 var_type = "wire"
93 var_size = signal.width
94 var_init = signal.reset
95
96 for (*var_scope, var_name) in names:
97 suffix = None
98 while True:
99 try:
100 if suffix is None:
101 var_name_suffix = var_name
102 else:
103 var_name_suffix = "{}${}".format(var_name, suffix)
104 if signal not in self.vcd_vars:
105 vcd_var = self.vcd_writer.register_var(
106 scope=var_scope, name=var_name_suffix,
107 var_type=var_type, size=var_size, init=var_init)
108 self.vcd_vars[signal] = vcd_var
109 else:
110 self.vcd_writer.register_alias(
111 scope=var_scope, name=var_name_suffix,
112 var=self.vcd_vars[signal])
113 break
114 except KeyError:
115 suffix = (suffix or 0) + 1
116
117 if signal not in self.gtkw_names:
118 self.gtkw_names[signal] = (*var_scope, var_name_suffix)
119
120 def update(self, timestamp, signal, value):
121 vcd_var = self.vcd_vars.get(signal)
122 if vcd_var is None:
123 return
124
125 vcd_timestamp = self.timestamp_to_vcd(timestamp)
126 if signal.decoder:
127 var_value = self.decode_to_vcd(signal, value)
128 else:
129 var_value = value
130 self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)
131
132 def close(self, timestamp):
133 if self.vcd_writer is not None:
134 self.vcd_writer.close(self.timestamp_to_vcd(timestamp))
135
136 if self.gtkw_save is not None:
137 self.gtkw_save.dumpfile(self.vcd_file.name)
138 self.gtkw_save.dumpfile_size(self.vcd_file.tell())
139
140 self.gtkw_save.treeopen("top")
141 for signal in self.traces:
142 if len(signal) > 1 and not signal.decoder:
143 suffix = "[{}:0]".format(len(signal) - 1)
144 else:
145 suffix = ""
146 self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix)
147
148 if self.vcd_file is not None:
149 self.vcd_file.close()
150 if self.gtkw_file is not None:
151 self.gtkw_file.close()
152
153
154 class _Timeline:
155 def __init__(self):
156 self.now = 0.0
157 self.deadlines = dict()
158
159 def reset(self):
160 self.now = 0.0
161 self.deadlines.clear()
162
163 def at(self, run_at, process):
164 assert process not in self.deadlines
165 self.deadlines[process] = run_at
166
167 def delay(self, delay_by, process):
168 if delay_by is None:
169 run_at = self.now
170 else:
171 run_at = self.now + delay_by
172 self.at(run_at, process)
173
174 def advance(self):
175 nearest_processes = set()
176 nearest_deadline = None
177 for process, deadline in self.deadlines.items():
178 if deadline is None:
179 if nearest_deadline is not None:
180 nearest_processes.clear()
181 nearest_processes.add(process)
182 nearest_deadline = self.now
183 break
184 elif nearest_deadline is None or deadline <= nearest_deadline:
185 assert deadline >= self.now
186 if nearest_deadline is not None and deadline < nearest_deadline:
187 nearest_processes.clear()
188 nearest_processes.add(process)
189 nearest_deadline = deadline
190
191 if not nearest_processes:
192 return False
193
194 for process in nearest_processes:
195 process.runnable = True
196 del self.deadlines[process]
197 self.now = nearest_deadline
198
199 return True
200
201
202 class _PySignalState(BaseSignalState):
203 __slots__ = ("signal", "curr", "next", "waiters", "pending")
204
205 def __init__(self, signal, pending):
206 self.signal = signal
207 self.pending = pending
208 self.waiters = dict()
209 self.curr = self.next = signal.reset
210
211 def set(self, value):
212 if self.next == value:
213 return
214 self.next = value
215 self.pending.add(self)
216
217 def commit(self):
218 if self.curr == self.next:
219 return False
220 self.curr = self.next
221
222 awoken_any = False
223 for process, trigger in self.waiters.items():
224 if trigger is None or trigger == self.curr:
225 process.runnable = awoken_any = True
226 return awoken_any
227
228
229 class _PySimulation(BaseSimulation):
230 def __init__(self):
231 self.timeline = _Timeline()
232 self.signals = SignalDict()
233 self.slots = []
234 self.pending = set()
235
236 def reset(self):
237 self.timeline.reset()
238 for signal, index in self.signals.items():
239 self.slots[index].curr = self.slots[index].next = signal.reset
240 self.pending.clear()
241
242 def get_signal(self, signal):
243 try:
244 return self.signals[signal]
245 except KeyError:
246 index = len(self.slots)
247 self.slots.append(_PySignalState(signal, self.pending))
248 self.signals[signal] = index
249 return index
250
251 def add_trigger(self, process, signal, *, trigger=None):
252 index = self.get_signal(signal)
253 assert (process not in self.slots[index].waiters or
254 self.slots[index].waiters[process] == trigger)
255 self.slots[index].waiters[process] = trigger
256
257 def remove_trigger(self, process, signal):
258 index = self.get_signal(signal)
259 assert process in self.slots[index].waiters
260 del self.slots[index].waiters[process]
261
262 def wait_interval(self, process, interval):
263 self.timeline.delay(interval, process)
264
265 def commit(self):
266 converged = True
267 for signal_state in self.pending:
268 if signal_state.commit():
269 converged = False
270 self.pending.clear()
271 return converged
272
273
274 class PySimEngine(BaseEngine):
275 def __init__(self, fragment):
276 self._state = _PySimulation()
277 self._timeline = self._state.timeline
278
279 self._fragment = fragment
280 self._processes = _FragmentCompiler(self._state)(self._fragment)
281 self._vcd_writers = []
282
283 def add_coroutine_process(self, process, *, default_cmd):
284 self._processes.add(PyCoroProcess(self._state, self._fragment.domains, process,
285 default_cmd=default_cmd))
286
287 def add_clock_process(self, clock, *, phase, period):
288 self._processes.add(PyClockProcess(self._state, clock,
289 phase=phase, period=period))
290
291 def reset(self):
292 self._state.reset()
293 for process in self._processes:
294 process.reset()
295
296 def _step(self):
297 # Performs the two phases of a delta cycle in a loop:
298 converged = False
299 while not converged:
300 # 1. eval: run and suspend every non-waiting process once, queueing signal changes
301 for process in self._processes:
302 if process.runnable:
303 process.runnable = False
304 process.run()
305
306 for vcd_writer in self._vcd_writers:
307 for signal_state in self._state.pending:
308 vcd_writer.update(self._timeline.now,
309 signal_state.signal, signal_state.next)
310
311 # 2. commit: apply every queued signal change, waking up any waiting processes
312 converged = self._state.commit()
313
314 def advance(self):
315 self._step()
316 self._timeline.advance()
317 return any(not process.passive for process in self._processes)
318
319 @property
320 def now(self):
321 return self._timeline.now
322
323 @contextmanager
324 def write_vcd(self, *, vcd_file, gtkw_file, traces):
325 vcd_writer = _VCDWriter(self._fragment,
326 vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
327 try:
328 self._vcd_writers.append(vcd_writer)
329 yield
330 finally:
331 vcd_writer.close(self._timeline.now)
332 self._vcd_writers.remove(vcd_writer)