12afe7e85f6058a0506b4ce5f179b1734882ced6
[openpower-isa.git] / src / openpower / decoder / test / pysim.py
1 from contextlib import contextmanager
2 import itertools
3 from vcd import VCDWriter
4 from vcd.gtkw import GTKWSave
5
6 from nmigen.hdl import ClockSignal, ResetSignal
7 from nmigen.hdl.ast import SignalDict
8 from nmigen.sim._base import BaseSignalState, BaseSimulation, BaseEngine
9 from openpower.decoder.test._pyrtl import _FragmentCompiler
10 from nmigen.sim._pycoro import PyCoroProcess
11 from nmigen.sim._pyclock import PyClockProcess
12
13 import os
14 import sys
15 import shutil
16 import importlib
17 from cffi import FFI
18 from os.path import dirname, join
19 from collections import namedtuple
20
21
22 __all__ = ["PySimEngine"]
23
24
25 class _NameExtractor:
26 def __init__(self):
27 self.names = SignalDict()
28
29 def __call__(self, fragment, *, hierarchy=("top",)):
30 def add_signal_name(signal):
31 hierarchical_signal_name = (*hierarchy, signal.name)
32 if signal not in self.names:
33 self.names[signal] = {hierarchical_signal_name}
34 else:
35 self.names[signal].add(hierarchical_signal_name)
36
37 for domain_name, domain_signals in fragment.drivers.items():
38 if domain_name is not None:
39 domain = fragment.domains[domain_name]
40 add_signal_name(domain.clk)
41 if domain.rst is not None:
42 add_signal_name(domain.rst)
43
44 for statement in fragment.statements:
45 for signal in statement._lhs_signals() | statement._rhs_signals():
46 if not isinstance(signal, (ClockSignal, ResetSignal)):
47 add_signal_name(signal)
48
49 for subfragment_index, (subfragment, subfragment_name) in \
50 enumerate(fragment.subfragments):
51 if subfragment_name is None:
52 subfragment_name = "U${}".format(subfragment_index)
53 self(subfragment, hierarchy=(*hierarchy, subfragment_name))
54
55 return self.names
56
57
58 class _VCDWriter:
59 @staticmethod
60 def timestamp_to_vcd(timestamp):
61 return timestamp * (10 ** 10) # 1/(100 ps)
62
63 @staticmethod
64 def decode_to_vcd(signal, value):
65 return signal.decoder(value).expandtabs().replace(" ", "_")
66
67 def __init__(self, fragment, *, vcd_file, gtkw_file=None, traces=()):
68 if isinstance(vcd_file, str):
69 vcd_file = open(vcd_file, "wt")
70 if isinstance(gtkw_file, str):
71 gtkw_file = open(gtkw_file, "wt")
72
73 self.vcd_vars = SignalDict()
74 self.vcd_file = vcd_file
75 self.vcd_writer = vcd_file and VCDWriter(self.vcd_file,
76 timescale="100 ps", comment="Generated by nMigen")
77
78 self.gtkw_names = SignalDict()
79 self.gtkw_file = gtkw_file
80 self.gtkw_save = gtkw_file and GTKWSave(self.gtkw_file)
81
82 self.traces = []
83
84 signal_names = _NameExtractor()(fragment)
85
86 trace_names = SignalDict()
87 for trace in traces:
88 if trace not in signal_names:
89 trace_names[trace] = {("top", trace.name)}
90 self.traces.append(trace)
91
92 if self.vcd_writer is None:
93 return
94
95 for signal, names in (itertools.chain(signal_names.items(),
96 trace_names.items())):
97 if signal.decoder:
98 var_type = "string"
99 var_size = 1
100 var_init = self.decode_to_vcd(signal, signal.reset)
101 else:
102 var_type = "wire"
103 var_size = signal.width
104 var_init = signal.reset
105
106 for (*var_scope, var_name) in names:
107 suffix = None
108 while True:
109 try:
110 if suffix is None:
111 var_name_suffix = var_name
112 else:
113 var_name_suffix = "{}${}".format(var_name, suffix)
114 if signal not in self.vcd_vars:
115 vcd_var = self.vcd_writer.register_var(
116 scope=var_scope, name=var_name_suffix,
117 var_type=var_type, size=var_size, init=var_init)
118 self.vcd_vars[signal] = vcd_var
119 else:
120 self.vcd_writer.register_alias(
121 scope=var_scope, name=var_name_suffix,
122 var=self.vcd_vars[signal])
123 break
124 except KeyError:
125 suffix = (suffix or 0) + 1
126
127 if signal not in self.gtkw_names:
128 self.gtkw_names[signal] = (*var_scope, var_name_suffix)
129
130 def update(self, timestamp, signal, value):
131 vcd_var = self.vcd_vars.get(signal)
132 if vcd_var is None:
133 return
134
135 vcd_timestamp = self.timestamp_to_vcd(timestamp)
136 if signal.decoder:
137 var_value = self.decode_to_vcd(signal, value)
138 else:
139 var_value = value
140 self.vcd_writer.change(vcd_var, vcd_timestamp, var_value)
141
142 def close(self, timestamp):
143 if self.vcd_writer is not None:
144 self.vcd_writer.close(self.timestamp_to_vcd(timestamp))
145
146 if self.gtkw_save is not None:
147 self.gtkw_save.dumpfile(self.vcd_file.name)
148 self.gtkw_save.dumpfile_size(self.vcd_file.tell())
149
150 self.gtkw_save.treeopen("top")
151 for signal in self.traces:
152 if len(signal) > 1 and not signal.decoder:
153 suffix = "[{}:0]".format(len(signal) - 1)
154 else:
155 suffix = ""
156 self.gtkw_save.trace(".".join(self.gtkw_names[signal]) + suffix)
157
158 if self.vcd_file is not None:
159 self.vcd_file.close()
160 if self.gtkw_file is not None:
161 self.gtkw_file.close()
162
163
164 class _Timeline:
165 def __init__(self):
166 self.now = 0.0
167 self.deadlines = dict()
168
169 def reset(self):
170 self.now = 0.0
171 self.deadlines.clear()
172
173 def at(self, run_at, process):
174 assert process not in self.deadlines
175 self.deadlines[process] = run_at
176
177 def delay(self, delay_by, process):
178 if delay_by is None:
179 run_at = self.now
180 else:
181 run_at = self.now + delay_by
182 self.at(run_at, process)
183
184 def advance(self):
185 nearest_processes = set()
186 nearest_deadline = None
187 for process, deadline in self.deadlines.items():
188 if deadline is None:
189 if nearest_deadline is not None:
190 nearest_processes.clear()
191 nearest_processes.add(process)
192 nearest_deadline = self.now
193 break
194 elif nearest_deadline is None or deadline <= nearest_deadline:
195 assert deadline >= self.now
196 if nearest_deadline is not None and deadline < nearest_deadline:
197 nearest_processes.clear()
198 nearest_processes.add(process)
199 nearest_deadline = deadline
200
201 if not nearest_processes:
202 return False
203
204 for process in nearest_processes:
205 process.runnable = True
206 del self.deadlines[process]
207 self.now = nearest_deadline
208
209 return True
210
211
212 class _PySignalState:
213 __slots__ = ("signal", "waiters", "sim_state", "index")
214
215 def __init__(self, signal, index, sim_state):
216 self.signal = signal
217 self.waiters = dict()
218 self.index = index
219 self.sim_state = sim_state # Ugly. need it to have a reference to crtl.
220
221 def set(self, value):
222 self.sim_state.crtl.set(self.index, value)
223
224 def commit(self):
225 if self.sim_state.crtl.capture(self.index) == 0:
226 return False
227
228 # Waiters are not implemented in C yet.
229 awoken_any = False
230 for process, trigger in self.waiters.items():
231 if trigger is None or trigger == self.curr:
232 process.runnable = awoken_any = True
233
234 return awoken_any
235
236 @property
237 def curr(self):
238 return self.sim_state.crtl.get_curr(self.index)
239
240 @property
241 def next(self):
242 return self.sim_state.crtl.get_next(self.index)
243
244
245 class _PySimulation:
246 def __init__(self):
247 self.timeline = _Timeline()
248 self.signals = SignalDict()
249 self.slots = []
250 self.pending = set()
251 self.crtl = None # Initialized later.
252
253 def reset(self):
254 self.timeline.reset()
255 for signal, index in self.signals.items():
256 self.slots[index].curr = self.slots[index].next = signal.reset
257 self.pending.clear()
258
259 def get_signal(self, signal):
260 try:
261 return self.signals[signal]
262 except KeyError:
263 index = len(self.slots)
264 self.slots.append(_PySignalState(signal, index, self))
265 self.signals[signal] = index
266 return index
267
268 def add_trigger(self, process, signal, *, trigger=None):
269 index = self.get_signal(signal)
270 assert (process not in self.slots[index].waiters or
271 self.slots[index].waiters[process] == trigger)
272 self.slots[index].waiters[process] = trigger
273
274 def remove_trigger(self, process, signal):
275 index = self.get_signal(signal)
276 assert process in self.slots[index].waiters
277 del self.slots[index].waiters[process]
278
279 def wait_interval(self, process, interval):
280 self.timeline.delay(interval, process)
281
282 def commit(self, changed=None):
283 converged = True
284
285 for pending_index in range(self.crtl.pending_count):
286 index = self.crtl.pending[pending_index]
287 signal_state = self.slots[index]
288
289 if signal_state.commit():
290 converged = False
291
292 if changed is not None:
293 changed.add(signal_state)
294
295 self.crtl.clear_pending()
296 return converged
297
298
299 class PySimEngine(BaseEngine):
300 _crtl_counter = 1
301
302 def __init__(self, fragment):
303 self._state = _PySimulation()
304 self._timeline = self._state.timeline
305
306 self._fragment = fragment
307
308 # blow away and recreate crtl subdirectory. (hope like hell
309 # nobody is using this in their current working directory)
310 if PySimEngine._crtl_counter == 1:
311 shutil.rmtree("crtl", True)
312 try:
313 os.mkdir("crtl")
314 except FileExistsError:
315 pass
316
317 # "Processes" are the compiled modules. Each module ends up
318 # with its own run() function
319 self._processes = _FragmentCompiler(self._state)(self._fragment)
320
321 # get absolute path of this directory as the base
322 filedir = os.path.dirname(os.path.abspath(__file__))
323
324 # read header file template
325 chdr = os.path.join(filedir, "crtl_template.h")
326 with open(chdr) as cdef_file:
327 template = cdef_file.read()
328
329 # fill in the template
330 cdef = template % (len(self._state.slots), len(self._state.slots))
331 for process in self._processes:
332 cdef += f"void run_{process.name}(void);\n"
333
334 # write out the header file
335 with open("crtl/common.h", "w") as cdef_file :
336 cdef_file.write(cdef)
337
338 # same with c template: read template first
339 srcf = os.path.join(filedir, "crtl_template.c")
340 with open(srcf) as src_file:
341 template = src_file.read()
342
343 # fill it out
344 src = template % (len(self._state.slots), len(self._state.slots))
345
346 # write it out
347 with open("crtl/common.c", "w") as src_file:
348 src_file.write(src)
349
350 # build module named crtlNNN in crtl subdirectory
351 modulename = "crtl.crtl%d" % PySimEngine._crtl_counter
352 sources = ["crtl/common.c"]
353 sources += [f"crtl/{process.name}.c" for process in self._processes]
354 ffibuilder = FFI()
355 ffibuilder.cdef(cdef)
356 ffibuilder.set_source(modulename, cdef, sources=sources)
357 ffibuilder.compile(verbose=True)
358
359 # append search path of crtl directory before attempting import
360 sys.path.append(os.path.join(os.getcwd()))
361 self._state.crtl = importlib.import_module(modulename).lib
362
363 # Use a counter to generate unique names for modules, because Python
364 # won't reload C extension modules.
365 PySimEngine._crtl_counter += 1
366
367 # for each process (fragment/module) get its run() function
368 for process in self._processes:
369 process.crtl = self._state.crtl
370 process.run = getattr(process.crtl, f"run_{process.name}")
371
372 self._vcd_writers = []
373
374 def add_coroutine_process(self, process, *, default_cmd):
375 self._processes.add(PyCoroProcess(self._state, self._fragment.domains,
376 process,
377 default_cmd=default_cmd))
378
379 def add_clock_process(self, clock, *, phase, period):
380 self._processes.add(PyClockProcess(self._state, clock,
381 phase=phase, period=period))
382
383 def reset(self):
384 self._state.reset()
385 for process in self._processes:
386 process.reset()
387
388 def _step(self):
389 changed = set() if self._vcd_writers else None
390
391 # Performs the two phases of a delta cycle in a loop:
392 converged = False
393 while not converged:
394 # 1. eval: run and suspend every non-waiting process once,
395 # queueing signal changes
396 for process in self._processes:
397 if process.runnable:
398 process.runnable = False
399 process.run()
400
401 # 2. commit: apply every queued signal change,
402 # waking up any waiting processes
403 converged = self._state.commit(changed)
404
405 for vcd_writer in self._vcd_writers:
406 for signal_state in changed:
407 vcd_writer.update(self._timeline.now,
408 signal_state.signal, signal_state.curr)
409
410 def advance(self):
411 self._step()
412 self._timeline.advance()
413 return any(not process.passive for process in self._processes)
414
415 @property
416 def now(self):
417 return self._timeline.now
418
419 @contextmanager
420 def write_vcd(self, *, vcd_file, gtkw_file, traces):
421 vcd_writer = _VCDWriter(self._fragment,
422 vcd_file=vcd_file, gtkw_file=gtkw_file, traces=traces)
423 try:
424 self._vcd_writers.append(vcd_writer)
425 yield
426 finally:
427 vcd_writer.close(self._timeline.now)
428 self._vcd_writers.remove(vcd_writer)