CIRCT  20.0.0git
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 
16 import argparse
17 import os
18 from pathlib import Path
19 import re
20 import signal
21 import socket
22 import subprocess
23 import sys
24 import textwrap
25 import time
26 from typing import Dict, List
27 
28 _thisdir = Path(__file__).parent
29 CosimCollateralDir = _thisdir.parent / "cosim"
30 
31 
32 def 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.toptop = 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_diradd_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 
85 class Simulator:
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.sourcessources = sources
94  self.run_dirrun_dir = run_dir
95  self.debugdebug = 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_commandscompile_commands()
114  self.run_dirrun_dir.mkdir(parents=True, exist_ok=True)
115  with (self.run_dirrun_dir / "compile_stdout.log").open("w") as stdout, (
116  self.run_dirrun_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.UsesStderrUsesStderr:
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_dirrun_dir.mkdir(parents=True, exist_ok=True)
147  simStdout = open(self.run_dirrun_dir / "sim_stdout.log", "w")
148  simStderr = open(self.run_dirrun_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_dirrun_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.debugdebug:
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_commandrun_command(gui),
163  stdout=simStdout,
164  stderr=simStderr,
165  env=simEnv,
166  cwd=self.run_dirrun_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.verilatorverilator = "verilator"
225  if "VERILATOR_PATH" in os.environ:
226  self.verilatorverilator = os.environ["VERILATOR_PATH"]
227 
228  def compile_commands(self) -> List[List[str]]:
229  cmd: List[str] = [
230  self.verilatorverilator,
231  "--cc",
232  "--top-module",
233  self.sourcessources.top,
234  "-DSIMULATION",
235  "-sv",
236  "--build",
237  "--exe",
238  "--assert",
239  str(Verilator.DefaultDriver),
240  ]
241  cflags = [
242  "-DTOP_MODULE=" + self.sourcessources.top,
243  ]
244  if self.debugdebug:
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.sourcessources.dpi_so) > 0:
250  cmd += ["-LDFLAGS", " ".join(["-l" + so for so in self.sourcessources.dpi_so])]
251  cmd += [str(p) for p in self.sourcessources.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.sourcessources.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.sourcessources.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_commandsinternal_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.sourcessources.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_dirrun_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 
327 def __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 
398 if __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
int compile(self)
Definition: esi-cosim.py:112
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
def __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
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
def add_dir(self, Path dir)
Definition: esi-cosim.py:56
def __init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition: esi-cosim.py:221
def run_command(self, bool gui)
Definition: esi-cosim.py:254
List[List[str]] compile_commands(self)
Definition: esi-cosim.py:228
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition: CalyxOps.cpp:55
bool is_port_open(port)
Definition: esi-cosim.py:32
def __main__(args)
Definition: esi-cosim.py:327