11from pathlib
import Path
12from typing
import Dict, List, Optional, Callable, IO
15_thisdir = Path(__file__).parent
16CosimCollateralDir = _thisdir
20 """Check if a TCP port is open locally."""
21 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
22 result = sock.connect_ex((
'127.0.0.1', port))
24 return True if result == 0
else False
31 self.user: List[Path] = []
33 self.dpi_so: List[str] = [
"EsiCosimDpiServer"]
35 self.dpi_sv: List[Path] = [
36 CosimCollateralDir /
"Cosim_DpiPkg.sv",
37 CosimCollateralDir /
"Cosim_Endpoint.sv",
38 CosimCollateralDir /
"Cosim_CycleCount.sv",
39 CosimCollateralDir /
"Cosim_Manifest.sv",
45 """Add a single RTL file to the source list."""
49 raise FileNotFoundError(f
"File {file} does not exist")
52 """Add all the RTL files in a directory to the source list."""
53 for file
in sorted(dir.iterdir()):
54 if file.is_file()
and (file.suffix ==
".sv" or file.suffix ==
".v"):
60 """Return a list of all the DPI shared object files."""
62 def find_so(name: str) -> Path:
63 for path
in Simulator.get_env().get(
"LD_LIBRARY_PATH",
"").split(
":"):
65 so = Path(path) / f
"{name}.dll"
67 so = Path(path) / f
"lib{name}.so"
70 raise FileNotFoundError(f
"Could not find {name}.so in LD_LIBRARY_PATH")
72 return [find_so(name)
for name
in self.dpi_so]
76 """Return a list of all the RTL source files."""
77 return self.dpi_sv + self.user
83 proc: subprocess.Popen,
85 threads: Optional[List[threading.Thread]] =
None,
89 self.threads: List[threading.Thread] = threads
or []
93 """Make sure to stop the simulation no matter what."""
95 os.killpg(os.getpgid(self.
proc.pid), signal.SIGINT)
98 self.
proc.wait(timeout=1.0)
99 except subprocess.TimeoutExpired:
104 for t
in self.threads:
116 sources: SourceFiles,
119 save_waveform: bool =
False,
120 run_stdout_callback: Optional[Callable[[str],
None]] =
None,
121 run_stderr_callback: Optional[Callable[[str],
None]] =
None,
122 compile_stdout_callback: Optional[Callable[[str],
None]] =
None,
123 compile_stderr_callback: Optional[Callable[[str],
None]] =
None,
124 make_default_logs: bool =
True,
125 macro_definitions: Optional[Dict[str, str]] =
None):
126 """Simulator base class.
128 Optional sinks can be provided for capturing output. If not provided,
129 the simulator will write to log files in `run_dir`.
132 sources: SourceFiles describing RTL/DPI inputs.
133 run_dir: Directory where build/run artifacts are placed.
134 debug: Enable cosim debug mode.
135 save_waveform: When True and debug=True, dump simulator waveforms to a
136 waveform file. The exact format depends on the backend (e.g. FST for
137 Verilator, VCD for Questa). Requires debug to be enabled.
138 run_stdout_callback: Line-based callback for runtime stdout.
139 run_stderr_callback: Line-based callback for runtime stderr.
140 compile_stdout_callback: Line-based callback for compile stdout.
141 compile_stderr_callback: Line-based callback for compile stderr.
142 make_default_logs: If True and corresponding callback is not supplied,
143 create log file and emit via internally-created callback.
144 macro_definitions: Optional dictionary of macro definitions to be defined
154 self._default_files: List[IO[str]] = []
156 def _ensure_default(cb: Optional[Callable[[str],
None]], filename: str):
157 """Return (callback, file_handle_or_None) with optional file creation.
160 * If a callback is provided, return it unchanged with no file.
161 * If no callback and make_default_logs is False, return (None, None).
162 * If no callback and make_default_logs is True, create a log file and
163 return a writer callback plus the opened file handle.
167 if not make_default_logs:
170 p.parent.mkdir(parents=
True, exist_ok=
True)
172 self._default_files.
append(logf)
174 def _writer(line: str, _lf=logf):
175 _lf.write(line +
"\n")
182 compile_stdout_callback,
'compile_stdout.log')
184 compile_stderr_callback,
'compile_stderr.log')
186 run_stdout_callback,
'sim_stdout.log')
188 run_stderr_callback,
'sim_stderr.log')
192 """Get the environment variables to locate shared objects."""
194 env = os.environ.copy()
195 env[
"LIBRARY_PATH"] = env.get(
"LIBRARY_PATH",
"") +
":" + str(
196 _thisdir.parent /
"lib")
197 env[
"LD_LIBRARY_PATH"] = env.get(
"LD_LIBRARY_PATH",
"") +
":" + str(
198 _thisdir.parent /
"lib")
202 """Compile the sources. Returns the exit code of the simulation compiler."""
203 assert False,
"Must be implemented by subclass"
207 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
211 env=Simulator.get_env(),
216 if isinstance(ret, int)
and ret != 0:
217 print(
"====== Compilation failure")
225 print(stdout_content)
230 print(stderr_content)
236 """Return the command to run the simulation."""
237 assert False,
"Must be implemented by subclass"
241 """File extension for waveform dumps.
243 Subclasses should override if their format differs. The Verilator C++
244 driver writes FST (via ``VerilatedFstC``); the generic SV driver uses
245 ``$dumpfile/$dumpvars`` which produces VCD.
249 def run_proc(self, gui: bool =
False) -> SimProcess:
250 """Run the simulation process. Returns the Popen object and the port which
251 the simulation is listening on.
253 If user-provided stdout/stderr sinks were supplied in the constructor,
254 they are used. Otherwise, log files are created in `run_dir`.
256 self.
run_dir.mkdir(parents=
True, exist_ok=
True)
258 env_gui = os.environ.get(
"COSIM_GUI",
"0")
264 portFileName = self.
run_dir /
"cosim.cfg"
265 if os.path.exists(portFileName):
266 os.remove(portFileName)
269 simEnv = Simulator.get_env()
272 simEnv[
"COSIM_DEBUG_FILE"] = str(debug_file)
273 if "DEBUG_PERIOD" not in simEnv:
275 simEnv[
"DEBUG_PERIOD"] =
"1"
277 waveform_file = (self.
run_dir /
278 f
"cosim_waveform{self.waveform_extension}").
resolve()
279 simEnv[
"SAVE_WAVE"] = str(waveform_file)
292 while (
not os.path.exists(portFileName))
and \
296 if checkCount > 500
and not gui:
297 raise Exception(f
"Cosim never wrote cfg file: {portFileName}")
300 portFile = open(portFileName,
"r")
301 for line
in portFile.readlines():
302 m = re.match(
"port: (\\d+)", line)
304 port = int(m.group(1))
312 raise Exception(f
"Cosim RPC port ({port}) never opened")
313 if proc.poll()
is not None:
314 raise Exception(
"Simulation exited early")
316 return SimProcess(proc=proc, port=port, threads=threads, gui=gui)
319 self, cmd: List[str], env: Optional[Dict[str, str]], cwd: Optional[Path],
320 stdout_cb: Optional[Callable[[str],
321 None]], stderr_cb: Optional[Callable[[str],
323 wait: bool) -> int | tuple[subprocess.Popen, List[threading.Thread]]:
324 """Start a subprocess and stream its stdout/stderr to callbacks.
326 If wait is True, blocks until process completes and returns its exit code.
327 If wait is False, returns the Popen object (threads keep streaming).
329 if os.name ==
"posix":
330 proc = subprocess.Popen(cmd,
331 stdout=subprocess.PIPE,
332 stderr=subprocess.PIPE,
336 preexec_fn=os.setsid)
338 proc = subprocess.Popen(cmd,
339 stdout=subprocess.PIPE,
340 stderr=subprocess.PIPE,
344 creationflags=subprocess.CREATE_NEW_PROCESS_GROUP)
346 def _reader(pipe, cb):
350 if raw.endswith(
'\n'):
355 except Exception
as e:
356 print(f
"Exception in simulator output callback: {e}")
358 threads: List[threading.Thread] = [
359 threading.Thread(target=_reader,
360 args=(proc.stdout, stdout_cb),
362 threading.Thread(target=_reader,
363 args=(proc.stderr, stderr_cb),
377 server_only: bool =
False) -> int:
378 """Start the simulation then run the command specified. Kill the simulation
379 when the command exits."""
389 f
"Running in server-only mode on port {simProc.port} - Press anything to kill the server..."
394 testEnv = os.environ.copy()
395 testEnv[
"ESI_COSIM_PORT"] = str(simProc.port)
396 testEnv[
"ESI_COSIM_HOST"] =
"localhost"
397 ret = subprocess.run(inner_command, cwd=os.getcwd(),
398 env=testEnv).returncode
400 print(
"GUI mode - waiting for simulator to exit...")
404 if simProc
and simProc.proc.poll()
is None:
408def get_simulator(name: str,
409 sources: SourceFiles,
412 save_waveform: bool =
False) -> Simulator:
414 if name ==
"verilator":
415 from .verilator
import Verilator
416 return Verilator(sources, rundir, debug, save_waveform=save_waveform)
417 elif name ==
"questa":
418 from .questa
import Questa
419 return Questa(sources, rundir, debug, save_waveform=save_waveform)
421 raise ValueError(f
"Unknown simulator: {name}")
static void print(TypedAttr val, llvm::raw_ostream &os)
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
static StringAttr append(StringAttr base, const Twine &suffix)
Return a attribute with the specified suffix appended.
__init__(self, subprocess.Popen proc, int port, Optional[List[threading.Thread]] threads=None, bool gui=False)
int|tuple[subprocess.Popen, List[threading.Thread]] _start_process_with_callbacks(self, List[str] cmd, Optional[Dict[str, str]] env, Optional[Path] cwd, Optional[Callable[[str], None]] stdout_cb, Optional[Callable[[str], None]] stderr_cb, bool wait)
__init__(self, SourceFiles sources, Path run_dir, bool debug, bool save_waveform=False, Optional[Callable[[str], None]] run_stdout_callback=None, Optional[Callable[[str], None]] run_stderr_callback=None, Optional[Callable[[str], None]] compile_stdout_callback=None, Optional[Callable[[str], None]] compile_stderr_callback=None, bool make_default_logs=True, Optional[Dict[str, str]] macro_definitions=None)
int run(self, str inner_command, bool gui=False, bool server_only=False)
str waveform_extension(self)
List[List[str]] compile_commands(self)
List[str] run_command(self, bool gui)
SimProcess run_proc(self, bool gui=False)
List[Path] rtl_sources(self)
add_file(self, Path file)
None __init__(self, str top)
List[Path] dpi_so_paths(self)