CIRCT 21.0.0git
Loading...
Searching...
No Matches
esi-cosim.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2
3# ===- esi-cosim.py - ESI cosimulation launch utility --------*- python -*-===//
4#
5# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
6# See https://llvm.org/LICENSE.txt for license information.
7# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
8#
9# ===----------------------------------------------------------------------===//
10#
11# Utility script to start a simulation and launch a command to interact with it
12# via ESI cosimulation.
13#
14# ===----------------------------------------------------------------------===//
15
16import argparse
17import os
18from pathlib import Path
19import re
20import signal
21import socket
22import subprocess
23import sys
24import textwrap
25import time
26from typing import Dict, List
27
28_thisdir = Path(__file__).parent
29CosimCollateralDir = _thisdir.parent / "cosim"
30
31
32def is_port_open(port) -> bool:
33 """Check if a TCP port is open locally."""
34 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
35 result = sock.connect_ex(('127.0.0.1', port))
36 sock.close()
37 return True if result == 0 else False
38
39
41
42 def __init__(self, top: str) -> None:
43 # User source files.
44 self.user: List[Path] = []
45 # DPI shared objects.
46 self.dpi_so: List[str] = ["EsiCosimDpiServer"]
47 # DPI SV files.
48 self.dpi_sv: List[Path] = [
49 CosimCollateralDir / "Cosim_DpiPkg.sv",
50 CosimCollateralDir / "Cosim_Endpoint.sv",
51 CosimCollateralDir / "Cosim_Manifest.sv",
52 ]
53 # Name of the top module.
54 self.top = top
55
56 def add_dir(self, dir: Path):
57 """Add all the RTL files in a directory to the source list."""
58 for file in sorted(dir.iterdir()):
59 if file.is_file() and (file.suffix == ".sv" or file.suffix == ".v"):
60 self.user.append(file)
61 elif file.is_dir():
62 self.add_dir(file)
63
64 def dpi_so_paths(self) -> List[Path]:
65 """Return a list of all the DPI shared object files."""
66
67 def find_so(name: str) -> Path:
68 for path in Simulator.get_env().get("LD_LIBRARY_PATH", "").split(":"):
69 if os.name == "nt":
70 so = Path(path) / f"{name}.dll"
71 else:
72 so = Path(path) / f"lib{name}.so"
73 if so.exists():
74 return so
75 raise FileNotFoundError(f"Could not find {name}.so in LD_LIBRARY_PATH")
76
77 return [find_so(name) for name in self.dpi_so]
78
79 @property
80 def rtl_sources(self) -> List[Path]:
81 """Return a list of all the RTL source files."""
82 return self.dpi_sv + self.user
83
84
86
87 # Some RTL simulators don't use stderr for error messages. Everything goes to
88 # stdout. Boo! They should feel bad about this. Also, they can specify that
89 # broken behavior by overriding this.
90 UsesStderr = True
91
92 def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
93 self.sources = sources
94 self.run_dir = run_dir
95 self.debug = debug
96
97 @staticmethod
98 def get_env() -> Dict[str, str]:
99 """Get the environment variables to locate shared objects."""
100
101 env = os.environ.copy()
102 env["LIBRARY_PATH"] = env.get("LIBRARY_PATH", "") + ":" + str(
103 _thisdir.parent / "lib")
104 env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH", "") + ":" + str(
105 _thisdir.parent / "lib")
106 return env
107
108 def compile_commands(self) -> List[List[str]]:
109 """Compile the sources. Returns the exit code of the simulation compiler."""
110 assert False, "Must be implemented by subclass"
111
112 def compile(self) -> int:
113 cmds = self.compile_commands()
114 self.run_dir.mkdir(parents=True, exist_ok=True)
115 with (self.run_dir / "compile_stdout.log").open("w") as stdout, (
116 self.run_dir / "compile_stderr.log").open("w") as stderr:
117 for cmd in cmds:
118 stderr.write(" ".join(cmd) + "\n")
119 cp = subprocess.run(cmd,
120 env=Simulator.get_env(),
121 capture_output=True,
122 text=True)
123 stdout.write(cp.stdout)
124 stderr.write(cp.stderr)
125 if cp.returncode != 0:
126 print("====== Compilation failure:")
127 if self.UsesStderr:
128 print(cp.stderr)
129 else:
130 print(cp.stdout)
131 return cp.returncode
132 return 0
133
134 def run_command(self, gui: bool) -> List[str]:
135 """Return the command to run the simulation."""
136 assert False, "Must be implemented by subclass"
137
138 def run(self,
139 inner_command: str,
140 gui: bool = False,
141 server_only: bool = False) -> int:
142 """Start the simulation then run the command specified. Kill the simulation
143 when the command exits."""
144
145 # 'simProc' is accessed in the finally block. Declare it here to avoid
146 # syntax errors in that block.
147 simProc = None
148 try:
149 # Open log files
150 self.run_dir.mkdir(parents=True, exist_ok=True)
151 simStdout = open(self.run_dir / "sim_stdout.log", "w")
152 simStderr = open(self.run_dir / "sim_stderr.log", "w")
153
154 # Erase the config file if it exists. We don't want to read
155 # an old config.
156 portFileName = self.run_dir / "cosim.cfg"
157 if os.path.exists(portFileName):
158 os.remove(portFileName)
159
160 # Run the simulation.
161 simEnv = Simulator.get_env()
162 if self.debug:
163 simEnv["COSIM_DEBUG_FILE"] = "cosim_debug.log"
164 if "DEBUG_PERIOD" not in simEnv:
165 # Slow the simulation down to one tick per millisecond.
166 simEnv["DEBUG_PERIOD"] = "1"
167 simProc = subprocess.Popen(self.run_command(gui),
168 stdout=simStdout,
169 stderr=simStderr,
170 env=simEnv,
171 cwd=self.run_dir,
172 preexec_fn=os.setsid)
173 simStderr.close()
174 simStdout.close()
175
176 # Get the port which the simulation RPC selected.
177 checkCount = 0
178 while (not os.path.exists(portFileName)) and \
179 simProc.poll() is None:
180 time.sleep(0.1)
181 checkCount += 1
182 if checkCount > 200 and not gui:
183 raise Exception(f"Cosim never wrote cfg file: {portFileName}")
184 port = -1
185 while port < 0:
186 portFile = open(portFileName, "r")
187 for line in portFile.readlines():
188 m = re.match("port: (\\d+)", line)
189 if m is not None:
190 port = int(m.group(1))
191 portFile.close()
192
193 # Wait for the simulation to start accepting RPC connections.
194 checkCount = 0
195 while not is_port_open(port):
196 checkCount += 1
197 if checkCount > 200:
198 raise Exception(f"Cosim RPC port ({port}) never opened")
199 if simProc.poll() is not None:
200 raise Exception("Simulation exited early")
201 time.sleep(0.05)
202
203 if server_only:
204 # wait for user input to kill the server
205 input(
206 f"Running in server-only mode on port {port} - Press anything to kill the server..."
207 )
208 return 0
209 else:
210 # Run the inner command, passing the connection info via environment vars.
211 testEnv = os.environ.copy()
212 testEnv["ESI_COSIM_PORT"] = str(port)
213 testEnv["ESI_COSIM_HOST"] = "localhost"
214 return subprocess.run(inner_command, cwd=os.getcwd(),
215 env=testEnv).returncode
216 finally:
217 # Make sure to stop the simulation no matter what.
218 if simProc:
219 os.killpg(os.getpgid(simProc.pid), signal.SIGINT)
220 # Allow the simulation time to flush its outputs.
221 try:
222 simProc.wait(timeout=1.0)
223 except subprocess.TimeoutExpired:
224 # If the simulation doesn't exit of its own free will, kill it.
225 simProc.kill()
226
227
229 """Run and compile funcs for Verilator."""
230
231 DefaultDriver = CosimCollateralDir / "driver.cpp"
232
233 def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
234 super().__init__(sources, run_dir, debug)
235
236 self.verilator = "verilator"
237 if "VERILATOR_PATH" in os.environ:
238 self.verilator = os.environ["VERILATOR_PATH"]
239
240 def compile_commands(self) -> List[List[str]]:
241 cmd: List[str] = [
242 self.verilator,
243 "--cc",
244 "--top-module",
245 self.sources.top,
246 "-DSIMULATION",
247 "-Wno-TIMESCALEMOD",
248 "-Wno-fatal",
249 "-sv",
250 "--build",
251 "--exe",
252 "--assert",
253 str(Verilator.DefaultDriver),
254 ]
255 cflags = [
256 "-DTOP_MODULE=" + self.sources.top,
257 ]
258 if self.debug:
259 cmd += [
260 "--trace", "--trace-params", "--trace-structs", "--trace-underscore"
261 ]
262 cflags.append("-DTRACE")
263 if len(cflags) > 0:
264 cmd += ["-CFLAGS", " ".join(cflags)]
265 if len(self.sources.dpi_so) > 0:
266 cmd += ["-LDFLAGS", " ".join(["-l" + so for so in self.sources.dpi_so])]
267 cmd += [str(p) for p in self.sources.rtl_sources]
268 return [cmd]
269
270 def run_command(self, gui: bool):
271 if gui:
272 raise RuntimeError("Verilator does not support GUI mode.")
273 exe = Path.cwd() / "obj_dir" / ("V" + self.sources.top)
274 return [str(exe)]
275
276
278 """Run and compile funcs for Questasim."""
279
280 DefaultDriver = CosimCollateralDir / "driver.sv"
281
282 # Questa doesn't use stderr for error messages. Everything goes to stdout.
283 UsesStderr = False
284
285 def internal_compile_commands(self) -> List[str]:
286 cmds = [
287 "onerror { quit -f -code 1 }",
288 ]
289 sources = self.sources.rtl_sources
290 sources.append(Questa.DefaultDriver)
291 for src in sources:
292 cmds.append(f"vlog -incr +acc -sv +define+TOP_MODULE={self.sources.top}"
293 f" +define+SIMULATION {str(src)}")
294 cmds.append(f"vopt -incr driver -o driver_opt +acc")
295 return cmds
296
297 def compile_commands(self) -> List[List[str]]:
298 with open("compile.do", "w") as f:
299 for cmd in self.internal_compile_commands():
300 f.write(cmd)
301 f.write("\n")
302 f.write("quit\n")
303 return [
304 ["vsim", "-batch", "-do", "compile.do"],
305 ]
306
307 def run_command(self, gui: bool) -> List[str]:
308 vsim = "vsim"
309 # Note: vsim exit codes say nothing about the test run's pass/fail even
310 # if $fatal is encountered in the simulation.
311 if gui:
312 cmd = [
313 vsim,
314 "driver_opt",
315 ]
316 else:
317 cmd = [
318 vsim,
319 "driver_opt",
320 "-batch",
321 "-do",
322 "run -all",
323 ]
324 for lib in self.sources.dpi_so_paths():
325 svLib = os.path.splitext(lib)[0]
326 cmd.append("-sv_lib")
327 cmd.append(svLib)
328 return cmd
329
330 def run(self,
331 inner_command: str,
332 gui: bool = False,
333 server_only: bool = False) -> int:
334 """Override the Simulator.run() to add a soft link in the run directory (to
335 the work directory) before running vsim the usual way."""
336
337 # Create a soft link to the work directory.
338 workDir = self.run_dir / "work"
339 if not workDir.exists():
340 os.symlink(Path(os.getcwd()) / "work", workDir)
341
342 # Run the simulation.
343 return super().run(inner_command, gui, server_only=server_only)
344
345
346def __main__(args):
347 argparser = argparse.ArgumentParser(
348 description="Wrap a 'inner_cmd' in an ESI cosimulation environment.",
349 formatter_class=argparse.RawDescriptionHelpFormatter,
350 epilog=textwrap.dedent("""
351 Notes:
352 - For Verilator, libEsiCosimDpiServer.so must be in the dynamic
353 library runtime search path (LD_LIBRARY_PATH) and link time path
354 (LIBRARY_PATH). If it is installed to a standard location (e.g.
355 /usr/lib), this should be handled automatically.
356 - This script needs to sit in the same directory as the ESI support
357 SystemVerilog (e.g. Cosim_DpiPkg.sv, Cosim_MMIO.sv, etc.). It can,
358 however, be soft linked to a different location.
359 - The simulator executable(s) must be in your PATH.
360 """))
361
362 argparser.add_argument(
363 "--sim",
364 type=str,
365 default="verilator",
366 help="Name of the RTL simulator to use or path to an executable.")
367 argparser.add_argument("--rundir",
368 default="run",
369 help="Directory in which simulation should be run.")
370 argparser.add_argument(
371 "--top",
372 default="ESI_Cosim_Top",
373 help="Name of the 'top' module to use in the simulation.")
374 argparser.add_argument("--no-compile",
375 action="store_true",
376 help="Do not run the compile.")
377 argparser.add_argument("--debug",
378 action="store_true",
379 help="Enable debug output.")
380 argparser.add_argument("--gui",
381 action="store_true",
382 help="Run the simulator in GUI mode (if supported).")
383 argparser.add_argument("--source",
384 help="Directories containing the source files.",
385 default="hw")
386
387 argparser.add_argument("inner_cmd",
388 nargs=argparse.REMAINDER,
389 help="Command to run in the simulation environment.")
390
391 argparser.add_argument(
392 "--server-only",
393 action="store_true",
394 help="Only run the cosim server, and do not run any inner command.")
395
396 if len(args) <= 1:
397 argparser.print_help()
398 return
399 args = argparser.parse_args(args[1:])
400
401 sources = SourceFiles(args.top)
402 sources.add_dir(Path(args.source))
403
404 if args.sim == "verilator":
405 sim = Verilator(sources, Path(args.rundir), args.debug)
406 elif args.sim == "questa":
407 sim = Questa(sources, Path(args.rundir), args.debug)
408 else:
409 print("Unknown simulator: " + args.sim)
410 print("Supported simulators: ")
411 print(" - verilator")
412 print(" - questa")
413 return 1
414
415 if not args.no_compile:
416 rc = sim.compile()
417 if rc != 0:
418 return rc
419 return sim.run(args.inner_cmd[1:], gui=args.gui, server_only=args.server_only)
420
421
422if __name__ == '__main__':
423 sys.exit(__main__(sys.argv))
static void print(TypedAttr val, llvm::raw_ostream &os)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
List[List[str]] compile_commands(self)
Definition esi-cosim.py:297
int run(self, str inner_command, bool gui=False, bool server_only=False)
Definition esi-cosim.py:333
List[str] internal_compile_commands(self)
Definition esi-cosim.py:285
List[str] run_command(self, bool gui)
Definition esi-cosim.py:307
Dict[str, str] get_env()
Definition esi-cosim.py:98
int run(self, str inner_command, bool gui=False, bool server_only=False)
Definition esi-cosim.py:141
List[str] run_command(self, bool gui)
Definition esi-cosim.py:134
__init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition esi-cosim.py:92
List[List[str]] compile_commands(self)
Definition esi-cosim.py:108
add_dir(self, Path dir)
Definition esi-cosim.py:56
List[Path] rtl_sources(self)
Definition esi-cosim.py:80
List[Path] dpi_so_paths(self)
Definition esi-cosim.py:64
None __init__(self, str top)
Definition esi-cosim.py:42
run_command(self, bool gui)
Definition esi-cosim.py:270
List[List[str]] compile_commands(self)
Definition esi-cosim.py:240
__init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition esi-cosim.py:233
bool is_port_open(port)
Definition esi-cosim.py:32
__main__(args)
Definition esi-cosim.py:346