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