CIRCT 23.0.0git
Loading...
Searching...
No Matches
simulator.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5import os
6import re
7import signal
8import socket
9import subprocess
10import time
11from pathlib import Path
12from typing import Dict, List, Optional, Callable, IO
13import threading
14
15_thisdir = Path(__file__).parent
16CosimCollateralDir = _thisdir
17
18
19def is_port_open(port) -> bool:
20 """Check if a TCP port is open locally."""
21 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
22 result = sock.connect_ex(('127.0.0.1', port))
23 sock.close()
24 return True if result == 0 else False
25
26
28
29 def __init__(self, top: str) -> None:
30 # User source files.
31 self.user: List[Path] = []
32 # DPI shared objects.
33 self.dpi_so: List[str] = ["EsiCosimDpiServer"]
34 # DPI SV files.
35 self.dpi_sv: List[Path] = [
36 CosimCollateralDir / "Cosim_DpiPkg.sv",
37 CosimCollateralDir / "Cosim_Endpoint.sv",
38 CosimCollateralDir / "Cosim_CycleCount.sv",
39 CosimCollateralDir / "Cosim_Manifest.sv",
40 ]
41 # Name of the top module.
42 self.top = top
43
44 def add_file(self, file: Path):
45 """Add a single RTL file to the source list."""
46 if file.is_file():
47 self.user.append(file)
48 else:
49 raise FileNotFoundError(f"File {file} does not exist")
50
51 def add_dir(self, dir: Path):
52 """Add all the RTL files in a directory to the source list."""
53 for file in sorted(dir.iterdir()):
54 if file.is_file() and (file.suffix == ".sv" or file.suffix == ".v"):
55 self.user.append(file)
56 elif file.is_dir():
57 self.add_dir(file)
58
59 def dpi_so_paths(self) -> List[Path]:
60 """Return a list of all the DPI shared object files."""
61
62 def find_so(name: str) -> Path:
63 for path in Simulator.get_env().get("LD_LIBRARY_PATH", "").split(":"):
64 if os.name == "nt":
65 so = Path(path) / f"{name}.dll"
66 else:
67 so = Path(path) / f"lib{name}.so"
68 if so.exists():
69 return so
70 raise FileNotFoundError(f"Could not find {name}.so in LD_LIBRARY_PATH")
71
72 return [find_so(name) for name in self.dpi_so]
73
74 @property
75 def rtl_sources(self) -> List[Path]:
76 """Return a list of all the RTL source files."""
77 return self.dpi_sv + self.user
78
79
81
82 def __init__(self,
83 proc: subprocess.Popen,
84 port: int,
85 threads: Optional[List[threading.Thread]] = None,
86 gui: bool = False):
87 self.proc = proc
88 self.port = port
89 self.threads: List[threading.Thread] = threads or []
90 self.gui = gui
91
92 def force_stop(self):
93 """Make sure to stop the simulation no matter what."""
94 if self.proc:
95 os.killpg(os.getpgid(self.proc.pid), signal.SIGINT)
96 # Allow the simulation time to flush its outputs.
97 try:
98 self.proc.wait(timeout=1.0)
99 except subprocess.TimeoutExpired:
100 # If the simulation doesn't exit of its own free will, kill it.
101 self.proc.kill()
102
103 # Join reader threads (they should exit once pipes are closed).
104 for t in self.threads:
105 t.join()
106
107
109
110 # Some RTL simulators don't use stderr for error messages. Everything goes to
111 # stdout. Boo! They should feel bad about this. Also, they can specify that
112 # broken behavior by overriding this.
113 UsesStderr = True
114
115 def __init__(self,
116 sources: SourceFiles,
117 run_dir: Path,
118 debug: bool,
119 save_waveform: bool = False,
120 run_stdout_callback: Optional[Callable[[str], None]] = None,
121 run_stderr_callback: Optional[Callable[[str], None]] = None,
122 compile_stdout_callback: Optional[Callable[[str], None]] = None,
123 compile_stderr_callback: Optional[Callable[[str], None]] = None,
124 make_default_logs: bool = True,
125 macro_definitions: Optional[Dict[str, str]] = None):
126 """Simulator base class.
127
128 Optional sinks can be provided for capturing output. If not provided,
129 the simulator will write to log files in `run_dir`.
130
131 Args:
132 sources: SourceFiles describing RTL/DPI inputs.
133 run_dir: Directory where build/run artifacts are placed.
134 debug: Enable cosim debug mode.
135 save_waveform: When True and debug=True, dump simulator waveforms to a
136 waveform file. The exact format depends on the backend (e.g. FST for
137 Verilator, VCD for Questa). Requires debug to be enabled.
138 run_stdout_callback: Line-based callback for runtime stdout.
139 run_stderr_callback: Line-based callback for runtime stderr.
140 compile_stdout_callback: Line-based callback for compile stdout.
141 compile_stderr_callback: Line-based callback for compile stderr.
142 make_default_logs: If True and corresponding callback is not supplied,
143 create log file and emit via internally-created callback.
144 macro_definitions: Optional dictionary of macro definitions to be defined
145 during compilation.
146 """
147 self.sources = sources
148 self.run_dir = run_dir
149 self.debug = debug
150 self.save_waveform = save_waveform
151 self.macro_definitions = macro_definitions
152
153 # Unified list of any log file handles we opened.
154 self._default_files: List[IO[str]] = []
155
156 def _ensure_default(cb: Optional[Callable[[str], None]], filename: str):
157 """Return (callback, file_handle_or_None) with optional file creation.
158
159 Behavior:
160 * If a callback is provided, return it unchanged with no file.
161 * If no callback and make_default_logs is False, return (None, None).
162 * If no callback and make_default_logs is True, create a log file and
163 return a writer callback plus the opened file handle.
164 """
165 if cb is not None:
166 return cb, None
167 if not make_default_logs:
168 return None, None
169 p = self.run_dir / filename
170 p.parent.mkdir(parents=True, exist_ok=True)
171 logf = p.open("w+")
172 self._default_files.append(logf)
173
174 def _writer(line: str, _lf=logf):
175 _lf.write(line + "\n")
176 _lf.flush()
177
178 return _writer, logf
179
180 # Initialize all four (compile/run stdout/stderr) uniformly.
181 self._compile_stdout_cb, self._compile_stdout_log = _ensure_default(
182 compile_stdout_callback, 'compile_stdout.log')
183 self._compile_stderr_cb, self._compile_stderr_log = _ensure_default(
184 compile_stderr_callback, 'compile_stderr.log')
185 self._run_stdout_cb, self._run_stdout_log = _ensure_default(
186 run_stdout_callback, 'sim_stdout.log')
187 self._run_stderr_cb, self._run_stderr_log = _ensure_default(
188 run_stderr_callback, 'sim_stderr.log')
189
190 @staticmethod
191 def get_env() -> Dict[str, str]:
192 """Get the environment variables to locate shared objects."""
193
194 env = os.environ.copy()
195 env["LIBRARY_PATH"] = env.get("LIBRARY_PATH", "") + ":" + str(
196 _thisdir.parent / "lib")
197 env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH", "") + ":" + str(
198 _thisdir.parent / "lib")
199 return env
200
201 def compile_commands(self) -> List[List[str]]:
202 """Compile the sources. Returns the exit code of the simulation compiler."""
203 assert False, "Must be implemented by subclass"
204
205 def compile(self) -> int:
206 cmds = self.compile_commands()
207 self.run_dir.mkdir(parents=True, exist_ok=True)
208 for cmd in cmds:
210 cmd,
211 env=Simulator.get_env(),
212 cwd=None,
213 stdout_cb=self._compile_stdout_cb,
214 stderr_cb=self._compile_stderr_cb,
215 wait=True)
216 if isinstance(ret, int) and ret != 0:
217 print("====== Compilation failure")
218
219 # If we have the default file loggers, print the compilation logs to
220 # console. Else, assume that the user has already captured them.
221 if self.UsesStderr:
222 if self._compile_stderr_log is not None:
223 self._compile_stderr_log.seek(0)
224 print(self._compile_stderr_log.read())
225 else:
226 if self._compile_stdout_log is not None:
227 self._compile_stdout_log.seek(0)
228 print(self._compile_stdout_log.read())
229
230 return ret
231 return 0
232
233 def run_command(self, gui: bool) -> List[str]:
234 """Return the command to run the simulation."""
235 assert False, "Must be implemented by subclass"
236
237 @property
238 def waveform_extension(self) -> str:
239 """File extension for waveform dumps.
240
241 Subclasses should override if their format differs. The Verilator C++
242 driver writes FST (via ``VerilatedFstC``); the generic SV driver uses
243 ``$dumpfile/$dumpvars`` which produces VCD.
244 """
245 return ".vcd"
246
247 def run_proc(self, gui: bool = False) -> SimProcess:
248 """Run the simulation process. Returns the Popen object and the port which
249 the simulation is listening on.
250
251 If user-provided stdout/stderr sinks were supplied in the constructor,
252 they are used. Otherwise, log files are created in `run_dir`.
253 """
254 self.run_dir.mkdir(parents=True, exist_ok=True)
255
256 env_gui = os.environ.get("COSIM_GUI", "0")
257 if env_gui != "0":
258 gui = True
259
260 # Erase the config file if it exists. We don't want to read
261 # an old config.
262 portFileName = self.run_dir / "cosim.cfg"
263 if os.path.exists(portFileName):
264 os.remove(portFileName)
265
266 # Run the simulation.
267 simEnv = Simulator.get_env()
268 if self.debug:
269 debug_file = (self.run_dir / "cosim_debug.log").resolve()
270 simEnv["COSIM_DEBUG_FILE"] = str(debug_file)
271 if "DEBUG_PERIOD" not in simEnv:
272 # Slow the simulation down to one tick per millisecond.
273 simEnv["DEBUG_PERIOD"] = "1"
274 if self.save_waveform:
275 waveform_file = (self.run_dir /
276 f"cosim_waveform{self.waveform_extension}").resolve()
277 simEnv["SAVE_WAVE"] = str(waveform_file)
278 rcmd = self.run_command(gui)
279 # Start process with asynchronous output capture.
280 proc, threads = self._start_process_with_callbacks(
281 rcmd,
282 env=simEnv,
283 cwd=self.run_dir,
284 stdout_cb=self._run_stdout_cb,
285 stderr_cb=self._run_stderr_cb,
286 wait=False)
287
288 # Get the port which the simulation RPC selected.
289 checkCount = 0
290 while (not os.path.exists(portFileName)) and \
291 proc.poll() is None:
292 time.sleep(0.1)
293 checkCount += 1
294 if checkCount > 500 and not gui:
295 raise Exception(f"Cosim never wrote cfg file: {portFileName}")
296 port = -1
297 while port < 0:
298 portFile = open(portFileName, "r")
299 for line in portFile.readlines():
300 m = re.match("port: (\\d+)", line)
301 if m is not None:
302 port = int(m.group(1))
303 portFile.close()
304
305 # Wait for the simulation to start accepting RPC connections.
306 checkCount = 0
307 while not is_port_open(port):
308 checkCount += 1
309 if checkCount > 200:
310 raise Exception(f"Cosim RPC port ({port}) never opened")
311 if proc.poll() is not None:
312 raise Exception("Simulation exited early")
313 time.sleep(0.05)
314 return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
315
317 self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
318 stdout_cb: Optional[Callable[[str],
319 None]], stderr_cb: Optional[Callable[[str],
320 None]],
321 wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
322 """Start a subprocess and stream its stdout/stderr to callbacks.
323
324 If wait is True, blocks until process completes and returns its exit code.
325 If wait is False, returns the Popen object (threads keep streaming).
326 """
327 if os.name == "posix":
328 proc = subprocess.Popen(cmd,
329 stdout=subprocess.PIPE,
330 stderr=subprocess.PIPE,
331 env=env,
332 cwd=cwd,
333 text=True,
334 preexec_fn=os.setsid)
335 else: # windows
336 proc = subprocess.Popen(cmd,
337 stdout=subprocess.PIPE,
338 stderr=subprocess.PIPE,
339 env=env,
340 cwd=cwd,
341 text=True,
342 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
343
344 def _reader(pipe, cb):
345 if pipe is None:
346 return
347 for raw in pipe:
348 if raw.endswith('\n'):
349 raw = raw[:-1]
350 if cb:
351 try:
352 cb(raw)
353 except Exception as e:
354 print(f"Exception in simulator output callback: {e}")
355
356 threads: List[threading.Thread] = [
357 threading.Thread(target=_reader,
358 args=(proc.stdout, stdout_cb),
359 daemon=True),
360 threading.Thread(target=_reader,
361 args=(proc.stderr, stderr_cb),
362 daemon=True),
363 ]
364 for t in threads:
365 t.start()
366 if wait:
367 for t in threads:
368 t.join()
369 return proc.wait()
370 return proc, threads
371
372 def run(self,
373 inner_command: str,
374 gui: bool = False,
375 server_only: bool = False) -> int:
376 """Start the simulation then run the command specified. Kill the simulation
377 when the command exits."""
378
379 # 'simProc' is accessed in the finally block. Declare it here to avoid
380 # syntax errors in that block.
381 simProc = None
382 try:
383 simProc = self.run_proc(gui=gui)
384 if server_only:
385 # wait for user input to kill the server
386 input(
387 f"Running in server-only mode on port {simProc.port} - Press anything to kill the server..."
388 )
389 return 0
390 else:
391 # Run the inner command, passing the connection info via environment vars.
392 testEnv = os.environ.copy()
393 testEnv["ESI_COSIM_PORT"] = str(simProc.port)
394 testEnv["ESI_COSIM_HOST"] = "localhost"
395 ret = subprocess.run(inner_command, cwd=os.getcwd(),
396 env=testEnv).returncode
397 if simProc.gui:
398 print("GUI mode - waiting for simulator to exit...")
399 simProc.proc.wait()
400 return ret
401 finally:
402 if simProc and simProc.proc.poll() is None:
403 simProc.force_stop()
404
405
406def get_simulator(name: str,
407 sources: SourceFiles,
408 rundir: Path,
409 debug: bool,
410 save_waveform: bool = False) -> Simulator:
411 name = name.lower()
412 if name == "verilator":
413 from .verilator import Verilator
414 return Verilator(sources, rundir, debug, save_waveform=save_waveform)
415 elif name == "questa":
416 from .questa import Questa
417 return Questa(sources, rundir, debug, save_waveform=save_waveform)
418 else:
419 raise ValueError(f"Unknown simulator: {name}")
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
__init__(self, subprocess.Popen proc, int port, Optional[List[threading.Thread]] threads=None, bool gui=False)
Definition simulator.py:86
int|tuple[subprocess.Popen, List[threading.Thread]] _start_process_with_callbacks(self, List[str] cmd, Optional[Dict[str, str]] env, Optional[Path] cwd, Optional[Callable[[str], None]] stdout_cb, Optional[Callable[[str], None]] stderr_cb, bool wait)
Definition simulator.py:321
__init__(self, SourceFiles sources, Path run_dir, bool debug, bool save_waveform=False, Optional[Callable[[str], None]] run_stdout_callback=None, Optional[Callable[[str], None]] run_stderr_callback=None, Optional[Callable[[str], None]] compile_stdout_callback=None, Optional[Callable[[str], None]] compile_stderr_callback=None, bool make_default_logs=True, Optional[Dict[str, str]] macro_definitions=None)
Definition simulator.py:125
int run(self, str inner_command, bool gui=False, bool server_only=False)
Definition simulator.py:375
str waveform_extension(self)
Definition simulator.py:238
List[List[str]] compile_commands(self)
Definition simulator.py:201
Dict[str, str] get_env()
Definition simulator.py:191
List[str] run_command(self, bool gui)
Definition simulator.py:233
SimProcess run_proc(self, bool gui=False)
Definition simulator.py:247
List[Path] rtl_sources(self)
Definition simulator.py:75
add_file(self, Path file)
Definition simulator.py:44
None __init__(self, str top)
Definition simulator.py:29
List[Path] dpi_so_paths(self)
Definition simulator.py:59
add_dir(self, Path dir)
Definition simulator.py:51
bool is_port_open(port)
Definition simulator.py:19