Loading [MathJax]/extensions/tex2jax.js
CIRCT 21.0.0git
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Pages
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, inner_command: str, gui: bool = False) -> int:
139 """Start the simulation then run the command specified. Kill the simulation
140 when the command exits."""
141
142 # 'simProc' is accessed in the finally block. Declare it here to avoid
143 # syntax errors in that block.
144 simProc = None
145 try:
146 # Open log files
147 self.run_dir.mkdir(parents=True, exist_ok=True)
148 simStdout = open(self.run_dir / "sim_stdout.log", "w")
149 simStderr = open(self.run_dir / "sim_stderr.log", "w")
150
151 # Erase the config file if it exists. We don't want to read
152 # an old config.
153 portFileName = self.run_dir / "cosim.cfg"
154 if os.path.exists(portFileName):
155 os.remove(portFileName)
156
157 # Run the simulation.
158 simEnv = Simulator.get_env()
159 if self.debug:
160 simEnv["COSIM_DEBUG_FILE"] = "cosim_debug.log"
161 if "DEBUG_PERIOD" not in simEnv:
162 # Slow the simulation down to one tick per millisecond.
163 simEnv["DEBUG_PERIOD"] = "1"
164 simProc = subprocess.Popen(self.run_command(gui),
165 stdout=simStdout,
166 stderr=simStderr,
167 env=simEnv,
168 cwd=self.run_dir,
169 preexec_fn=os.setsid)
170 simStderr.close()
171 simStdout.close()
172
173 # Get the port which the simulation RPC selected.
174 checkCount = 0
175 while (not os.path.exists(portFileName)) and \
176 simProc.poll() is None:
177 time.sleep(0.1)
178 checkCount += 1
179 if checkCount > 200 and not gui:
180 raise Exception(f"Cosim never wrote cfg file: {portFileName}")
181 port = -1
182 while port < 0:
183 portFile = open(portFileName, "r")
184 for line in portFile.readlines():
185 m = re.match("port: (\\d+)", line)
186 if m is not None:
187 port = int(m.group(1))
188 portFile.close()
189
190 # Wait for the simulation to start accepting RPC connections.
191 checkCount = 0
192 while not is_port_open(port):
193 checkCount += 1
194 if checkCount > 200:
195 raise Exception(f"Cosim RPC port ({port}) never opened")
196 if simProc.poll() is not None:
197 raise Exception("Simulation exited early")
198 time.sleep(0.05)
199
200 # Run the inner command, passing the connection info via environment vars.
201 testEnv = os.environ.copy()
202 testEnv["ESI_COSIM_PORT"] = str(port)
203 testEnv["ESI_COSIM_HOST"] = "localhost"
204 return subprocess.run(inner_command, cwd=os.getcwd(),
205 env=testEnv).returncode
206 finally:
207 # Make sure to stop the simulation no matter what.
208 if simProc:
209 os.killpg(os.getpgid(simProc.pid), signal.SIGINT)
210 # Allow the simulation time to flush its outputs.
211 try:
212 simProc.wait(timeout=1.0)
213 except subprocess.TimeoutExpired:
214 # If the simulation doesn't exit of its own free will, kill it.
215 simProc.kill()
216
217
219 """Run and compile funcs for Verilator."""
220
221 DefaultDriver = CosimCollateralDir / "driver.cpp"
222
223 def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
224 super().__init__(sources, run_dir, debug)
225
226 self.verilator = "verilator"
227 if "VERILATOR_PATH" in os.environ:
228 self.verilator = os.environ["VERILATOR_PATH"]
229
230 def compile_commands(self) -> List[List[str]]:
231 cmd: List[str] = [
232 self.verilator,
233 "--cc",
234 "--top-module",
235 self.sources.top,
236 "-DSIMULATION",
237 "-Wno-TIMESCALEMOD",
238 "-Wno-fatal",
239 "-sv",
240 "--build",
241 "--exe",
242 "--assert",
243 str(Verilator.DefaultDriver),
244 ]
245 cflags = [
246 "-DTOP_MODULE=" + self.sources.top,
247 ]
248 if self.debug:
249 cmd += [
250 "--trace", "--trace-params", "--trace-structs", "--trace-underscore"
251 ]
252 cflags.append("-DTRACE")
253 if len(cflags) > 0:
254 cmd += ["-CFLAGS", " ".join(cflags)]
255 if len(self.sources.dpi_so) > 0:
256 cmd += ["-LDFLAGS", " ".join(["-l" + so for so in self.sources.dpi_so])]
257 cmd += [str(p) for p in self.sources.rtl_sources]
258 return [cmd]
259
260 def run_command(self, gui: bool):
261 if gui:
262 raise RuntimeError("Verilator does not support GUI mode.")
263 exe = Path.cwd() / "obj_dir" / ("V" + self.sources.top)
264 return [str(exe)]
265
266
268 """Run and compile funcs for Questasim."""
269
270 DefaultDriver = CosimCollateralDir / "driver.sv"
271
272 # Questa doesn't use stderr for error messages. Everything goes to stdout.
273 UsesStderr = False
274
275 def internal_compile_commands(self) -> List[str]:
276 cmds = [
277 "onerror { quit -f -code 1 }",
278 ]
279 sources = self.sources.rtl_sources
280 sources.append(Questa.DefaultDriver)
281 for src in sources:
282 cmds.append(f"vlog -incr +acc -sv +define+TOP_MODULE={self.sources.top}"
283 f" +define+SIMULATION {str(src)}")
284 cmds.append(f"vopt -incr driver -o driver_opt +acc")
285 return cmds
286
287 def compile_commands(self) -> List[List[str]]:
288 with open("compile.do", "w") as f:
289 for cmd in self.internal_compile_commands():
290 f.write(cmd)
291 f.write("\n")
292 f.write("quit\n")
293 return [
294 ["vsim", "-batch", "-do", "compile.do"],
295 ]
296
297 def run_command(self, gui: bool) -> List[str]:
298 vsim = "vsim"
299 # Note: vsim exit codes say nothing about the test run's pass/fail even
300 # if $fatal is encountered in the simulation.
301 if gui:
302 cmd = [
303 vsim,
304 "driver_opt",
305 ]
306 else:
307 cmd = [
308 vsim,
309 "driver_opt",
310 "-batch",
311 "-do",
312 "run -all",
313 ]
314 for lib in self.sources.dpi_so_paths():
315 svLib = os.path.splitext(lib)[0]
316 cmd.append("-sv_lib")
317 cmd.append(svLib)
318 return cmd
319
320 def run(self, inner_command: str, gui: bool = False) -> int:
321 """Override the Simulator.run() to add a soft link in the run directory (to
322 the work directory) before running vsim the usual way."""
323
324 # Create a soft link to the work directory.
325 workDir = self.run_dir / "work"
326 if not workDir.exists():
327 os.symlink(Path(os.getcwd()) / "work", workDir)
328
329 # Run the simulation.
330 return super().run(inner_command, gui)
331
332
333def __main__(args):
334 argparser = argparse.ArgumentParser(
335 description="Wrap a 'inner_cmd' in an ESI cosimulation environment.",
336 formatter_class=argparse.RawDescriptionHelpFormatter,
337 epilog=textwrap.dedent("""
338 Notes:
339 - For Verilator, libEsiCosimDpiServer.so must be in the dynamic
340 library runtime search path (LD_LIBRARY_PATH) and link time path
341 (LIBRARY_PATH). If it is installed to a standard location (e.g.
342 /usr/lib), this should be handled automatically.
343 - This script needs to sit in the same directory as the ESI support
344 SystemVerilog (e.g. Cosim_DpiPkg.sv, Cosim_MMIO.sv, etc.). It can,
345 however, be soft linked to a different location.
346 - The simulator executable(s) must be in your PATH.
347 """))
348
349 argparser.add_argument(
350 "--sim",
351 type=str,
352 default="verilator",
353 help="Name of the RTL simulator to use or path to an executable.")
354 argparser.add_argument("--rundir",
355 default="run",
356 help="Directory in which simulation should be run.")
357 argparser.add_argument(
358 "--top",
359 default="ESI_Cosim_Top",
360 help="Name of the 'top' module to use in the simulation.")
361 argparser.add_argument("--no-compile",
362 action="store_true",
363 help="Do not run the compile.")
364 argparser.add_argument("--debug",
365 action="store_true",
366 help="Enable debug output.")
367 argparser.add_argument("--gui",
368 action="store_true",
369 help="Run the simulator in GUI mode (if supported).")
370 argparser.add_argument("--source",
371 help="Directories containing the source files.",
372 default="hw")
373
374 argparser.add_argument("inner_cmd",
375 nargs=argparse.REMAINDER,
376 help="Command to run in the simulation environment.")
377
378 if len(args) <= 1:
379 argparser.print_help()
380 return
381 args = argparser.parse_args(args[1:])
382
383 sources = SourceFiles(args.top)
384 sources.add_dir(Path(args.source))
385
386 if args.sim == "verilator":
387 sim = Verilator(sources, Path(args.rundir), args.debug)
388 elif args.sim == "questa":
389 sim = Questa(sources, Path(args.rundir), args.debug)
390 else:
391 print("Unknown simulator: " + args.sim)
392 print("Supported simulators: ")
393 print(" - verilator")
394 print(" - questa")
395 return 1
396
397 if not args.no_compile:
398 rc = sim.compile()
399 if rc != 0:
400 return rc
401 return sim.run(args.inner_cmd[1:], gui=args.gui)
402
403
404if __name__ == '__main__':
405 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:287
int run(self, str inner_command, bool gui=False)
Definition esi-cosim.py:320
List[str] internal_compile_commands(self)
Definition esi-cosim.py:275
List[str] run_command(self, bool gui)
Definition esi-cosim.py:297
Dict[str, str] get_env()
Definition esi-cosim.py:98
List[str] run_command(self, bool gui)
Definition esi-cosim.py:134
int run(self, str inner_command, bool gui=False)
Definition esi-cosim.py:138
__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:260
List[List[str]] compile_commands(self)
Definition esi-cosim.py:230
__init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition esi-cosim.py:223
bool is_port_open(port)
Definition esi-cosim.py:32
__main__(args)
Definition esi-cosim.py:333