CIRCT  19.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  CosimCollateralDir / "Cosim_MMIO.sv",
53  ]
54  # Name of the top module.
55  self.toptop = top
56 
57  def add_dir(self, dir: Path):
58  """Add all the RTL files in a directory to the source list."""
59  for file in sorted(dir.iterdir()):
60  if file.is_file() and (file.suffix == ".sv" or file.suffix == ".v"):
61  self.user.append(file)
62 
63  def dpi_so_paths(self) -> List[Path]:
64  """Return a list of all the DPI shared object files."""
65 
66  def find_so(name: str) -> Path:
67  for path in Simulator.get_env().get("LD_LIBRARY_PATH", "").split(":"):
68  if os.name == "nt":
69  so = Path(path) / f"{name}.dll"
70  else:
71  so = Path(path) / f"lib{name}.so"
72  if so.exists():
73  return so
74  raise FileNotFoundError(f"Could not find {name}.so in LD_LIBRARY_PATH")
75 
76  return [find_so(name) for name in self.dpi_so]
77 
78  @property
79  def rtl_sources(self) -> List[Path]:
80  """Return a list of all the RTL source files."""
81  return self.dpi_sv + self.user
82 
83 
84 class Simulator:
85 
86  # Some RTL simulators don't use stderr for error messages. Everything goes to
87  # stdout. Boo! They should feel bad about this. Also, they can specify that
88  # broken behavior by overriding this.
89  UsesStderr = True
90 
91  def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
92  self.sourcessources = sources
93  self.run_dirrun_dir = run_dir
94  self.debugdebug = debug
95 
96  @staticmethod
97  def get_env() -> Dict[str, str]:
98  """Get the environment variables to locate shared objects."""
99 
100  env = os.environ.copy()
101  env["LIBRARY_PATH"] = env.get("LIBRARY_PATH", "") + ":" + str(
102  _thisdir.parent / "lib")
103  env["LD_LIBRARY_PATH"] = env.get("LD_LIBRARY_PATH", "") + ":" + str(
104  _thisdir.parent / "lib")
105  return env
106 
107  def compile_command(self) -> List[str]:
108  """Compile the sources. Returns the exit code of the simulation compiler."""
109  assert False, "Must be implemented by subclass"
110 
111  def compile(self) -> int:
112  cp = subprocess.run(self.compile_commandcompile_command(),
113  env=Simulator.get_env(),
114  capture_output=True,
115  text=True)
116  self.run_dirrun_dir.mkdir(parents=True, exist_ok=True)
117  open(self.run_dirrun_dir / "compile_stdout.log", "w").write(cp.stdout)
118  open(self.run_dirrun_dir / "compile_stderr.log", "w").write(cp.stderr)
119  if cp.returncode != 0:
120  print("====== Compilation failure:")
121  if self.UsesStderrUsesStderr:
122  print(cp.stderr)
123  else:
124  print(cp.stdout)
125  return cp.returncode
126 
127  def run_command(self) -> List[str]:
128  """Return the command to run the simulation."""
129  assert False, "Must be implemented by subclass"
130 
131  def run(self, inner_command: str) -> int:
132  """Start the simulation then run the command specified. Kill the simulation
133  when the command exits."""
134 
135  # 'simProc' is accessed in the finally block. Declare it here to avoid
136  # syntax errors in that block.
137  simProc = None
138  try:
139  # Open log files
140  self.run_dirrun_dir.mkdir(parents=True, exist_ok=True)
141  simStdout = open(self.run_dirrun_dir / "sim_stdout.log", "w")
142  simStderr = open(self.run_dirrun_dir / "sim_stderr.log", "w")
143 
144  # Erase the config file if it exists. We don't want to read
145  # an old config.
146  portFileName = self.run_dirrun_dir / "cosim.cfg"
147  if os.path.exists(portFileName):
148  os.remove(portFileName)
149 
150  # Run the simulation.
151  simEnv = Simulator.get_env()
152  if self.debugdebug:
153  simEnv["COSIM_DEBUG_FILE"] = "cosim_debug.log"
154  simProc = subprocess.Popen(self.run_commandrun_command(),
155  stdout=simStdout,
156  stderr=simStderr,
157  env=simEnv,
158  cwd=self.run_dirrun_dir,
159  preexec_fn=os.setsid)
160  simStderr.close()
161  simStdout.close()
162 
163  # Get the port which the simulation RPC selected.
164  checkCount = 0
165  while (not os.path.exists(portFileName)) and \
166  simProc.poll() is None:
167  time.sleep(0.1)
168  checkCount += 1
169  if checkCount > 200:
170  raise Exception(f"Cosim never wrote cfg file: {portFileName}")
171  port = -1
172  while port < 0:
173  portFile = open(portFileName, "r")
174  for line in portFile.readlines():
175  m = re.match("port: (\\d+)", line)
176  if m is not None:
177  port = int(m.group(1))
178  portFile.close()
179 
180  # Wait for the simulation to start accepting RPC connections.
181  checkCount = 0
182  while not is_port_open(port):
183  checkCount += 1
184  if checkCount > 200:
185  raise Exception(f"Cosim RPC port ({port}) never opened")
186  if simProc.poll() is not None:
187  raise Exception("Simulation exited early")
188  time.sleep(0.05)
189 
190  # Run the inner command, passing the connection info via environment vars.
191  testEnv = Simulator.get_env()
192  testEnv["ESI_COSIM_PORT"] = str(port)
193  testEnv["ESI_COSIM_HOST"] = "localhost"
194  return subprocess.run(inner_command, cwd=os.getcwd(),
195  env=testEnv).returncode
196  finally:
197  # Make sure to stop the simulation no matter what.
198  if simProc:
199  os.killpg(os.getpgid(simProc.pid), signal.SIGINT)
200  # Allow the simulation time to flush its outputs.
201  try:
202  simProc.wait(timeout=1.0)
203  except subprocess.TimeoutExpired:
204  # If the simulation doesn't exit of its own free will, kill it.
205  simProc.kill()
206 
207 
209  """Run and compile funcs for Verilator."""
210 
211  DefaultDriver = CosimCollateralDir / "driver.cpp"
212 
213  def __init__(self, sources: SourceFiles, run_dir: Path, debug: bool):
214  super().__init__(sources, run_dir, debug)
215 
216  self.verilatorverilator = "verilator"
217  if "VERILATOR_PATH" in os.environ:
218  self.verilatorverilator = os.environ["VERILATOR_PATH"]
219 
220  def compile_command(self) -> List[str]:
221  cmd: List[str] = [
222  self.verilatorverilator,
223  "--cc",
224  "--top-module",
225  self.sourcessources.top,
226  "-DSIMULATION",
227  "-sv",
228  "--build",
229  "--exe",
230  "--assert",
231  str(Verilator.DefaultDriver),
232  ]
233  cflags = [
234  "-DTOP_MODULE=" + self.sourcessources.top,
235  ]
236  if self.debugdebug:
237  cmd += ["--trace", "--trace-params", "--trace-structs"]
238  cflags.append("-DTRACE")
239  if len(cflags) > 0:
240  cmd += ["-CFLAGS", " ".join(cflags)]
241  if len(self.sourcessources.dpi_so) > 0:
242  cmd += ["-LDFLAGS", " ".join(["-l" + so for so in self.sourcessources.dpi_so])]
243  cmd += [str(p) for p in self.sourcessources.rtl_sources]
244  return cmd
245 
246  def run_command(self):
247  exe = Path.cwd() / "obj_dir" / ("V" + self.sourcessources.top)
248  return [str(exe)]
249 
250 
252  """Run and compile funcs for Questasim."""
253 
254  DefaultDriver = CosimCollateralDir / "driver.sv"
255 
256  # Questa doesn't use stderr for error messages. Everything goes to stdout.
257  UsesStderr = False
258 
259  def compile_command(self) -> List[str]:
260  cmd = [
261  "vlog",
262  "-sv",
263  "+define+TOP_MODULE=" + self.sourcessources.top,
264  "+define+SIMULATION",
265  str(Questa.DefaultDriver),
266  ]
267  cmd += [str(p) for p in self.sourcessources.rtl_sources]
268  return cmd
269 
270  def run_command(self) -> List[str]:
271  vsim = "vsim"
272  # Note: vsim exit codes say nothing about the test run's pass/fail even
273  # if $fatal is encountered in the simulation.
274  cmd = [
275  vsim,
276  "driver",
277  "-batch",
278  "-do",
279  "run -all",
280  ]
281  for lib in self.sourcessources.dpi_so_paths():
282  svLib = os.path.splitext(lib)[0]
283  cmd.append("-sv_lib")
284  cmd.append(svLib)
285  if len(self.sourcessources.dpi_so) > 0:
286  cmd.append("-cpppath")
287  cmd.append("/usr/bin/clang++")
288  return cmd
289 
290  def run(self, inner_command: str) -> int:
291  """Override the Simulator.run() to add a soft link in the run directory (to
292  the work directory) before running vsim the usual way."""
293 
294  # Create a soft link to the work directory.
295  workDir = self.run_dirrun_dir / "work"
296  if not workDir.exists():
297  os.symlink(Path(os.getcwd()) / "work", workDir)
298 
299  # Run the simulation.
300  return super().run(inner_command)
301 
302 
303 def __main__(args):
304  argparser = argparse.ArgumentParser(
305  description="Wrap a 'inner_cmd' in an ESI cosimulation environment.",
306  formatter_class=argparse.RawDescriptionHelpFormatter,
307  epilog=textwrap.dedent("""
308  Notes:
309  - For Verilator, libEsiCosimDpiServer.so must be in the dynamic
310  library runtime search path (LD_LIBRARY_PATH) and link time path
311  (LIBRARY_PATH). If it is installed to a standard location (e.g.
312  /usr/lib), this should be handled automatically.
313  - This script needs to sit in the same directory as the ESI support
314  SystemVerilog (e.g. Cosim_DpiPkg.sv, Cosim_MMIO.sv, etc.). It can,
315  however, be soft linked to a different location.
316  - The simulator executable(s) must be in your PATH.
317  """))
318 
319  argparser.add_argument(
320  "--sim",
321  type=str,
322  default="verilator",
323  help="Name of the RTL simulator to use or path to an executable.")
324  argparser.add_argument("--rundir",
325  default="run",
326  help="Directory in which simulation should be run.")
327  argparser.add_argument(
328  "--top",
329  default="ESI_Cosim_Top",
330  help="Name of the 'top' module to use in the simulation.")
331  argparser.add_argument("--debug",
332  action="store_true",
333  help="Enable debug output.")
334  argparser.add_argument("--source",
335  help="Directories containing the source files.",
336  nargs="+",
337  default=["hw"])
338 
339  argparser.add_argument("inner_cmd",
340  nargs=argparse.REMAINDER,
341  help="Command to run in the simulation environment.")
342 
343  if len(args) <= 1:
344  argparser.print_help()
345  return
346  args = argparser.parse_args(args[1:])
347 
348  sources = SourceFiles(args.top)
349  for src in args.source:
350  sources.add_dir(Path(src))
351 
352  if args.sim == "verilator":
353  sim = Verilator(sources, Path(args.rundir), args.debug)
354  elif args.sim == "questa":
355  sim = Questa(sources, Path(args.rundir), args.debug)
356  else:
357  print("Unknown simulator: " + args.sim)
358  print("Supported simulators: ")
359  print(" - verilator")
360  print(" - questa")
361  return 1
362 
363  rc = sim.compile()
364  if rc != 0:
365  return rc
366  return sim.run(args.inner_cmd[1:])
367 
368 
369 if __name__ == '__main__':
370  sys.exit(__main__(sys.argv))
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
int run(self, str inner_command)
Definition: esi-cosim.py:290
List[str] compile_command(self)
Definition: esi-cosim.py:259
List[str] run_command(self)
Definition: esi-cosim.py:270
Dict[str, str] get_env()
Definition: esi-cosim.py:97
List[str] compile_command(self)
Definition: esi-cosim.py:107
int run(self, str inner_command)
Definition: esi-cosim.py:131
int compile(self)
Definition: esi-cosim.py:111
List[str] run_command(self)
Definition: esi-cosim.py:127
def __init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition: esi-cosim.py:91
List[Path] rtl_sources(self)
Definition: esi-cosim.py:79
List[Path] dpi_so_paths(self)
Definition: esi-cosim.py:63
None __init__(self, str top)
Definition: esi-cosim.py:42
def add_dir(self, Path dir)
Definition: esi-cosim.py:57
def run_command(self)
Definition: esi-cosim.py:246
List[str] compile_command(self)
Definition: esi-cosim.py:220
def __init__(self, SourceFiles sources, Path run_dir, bool debug)
Definition: esi-cosim.py:213
Direction get(bool isOutput)
Returns an output direction if isOutput is true, otherwise returns an input direction.
Definition: CalyxOps.cpp:54
bool is_port_open(port)
Definition: esi-cosim.py:32
def __main__(args)
Definition: esi-cosim.py:303