CIRCT 24.0.0git
Loading...
Searching...
No Matches
verilator.py
Go to the documentation of this file.
1# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
2# See https://llvm.org/LICENSE.txt for license information.
3# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
4
5import json
6import os
7import re
8import shutil
9from pathlib import Path
10from typing import List, Optional, Callable, Dict
11
12from .simulator import CosimCollateralDir, Simulator, SourceFiles
13
14
16 """Run and compile funcs for Verilator.
17
18 Calls ``verilator_bin`` directly (bypassing the Perl wrapper) to generate
19 C++ from RTL, then builds the simulation executable with CMake + Ninja.
20 Falls back to ``make`` when cmake/ninja are not available."""
21
22 DefaultDriver = CosimCollateralDir / "driver.cpp"
23 _CMakeSignatureFilename = ".esi-cosim-cmake-config.json"
24 _CMakeSignatureEnv = (
25 "CMAKE_PREFIX_PATH",
26 "CMAKE_TOOLCHAIN_FILE",
27 "CXX",
28 "CXXFLAGS",
29 "LDFLAGS",
30 "PATH",
31 )
32 VerilatorBinNotFound = (
33 "Cannot find verilator_bin. Set VERILATOR_PATH to an absolute path "
34 "or ensure verilator_bin is in PATH.")
35 VerilatorRootNotFound = (
36 "Cannot find VERILATOR_ROOT. Set the VERILATOR_ROOT environment "
37 "variable or ensure verilator_bin is in PATH.")
38 VerilatorPathInvalid = (
39 "VERILATOR_PATH does not point to a valid verilator_bin executable.")
40 VerilatorRootInvalid = (
41 "VERILATOR_ROOT does not point to a Verilator root containing "
42 "include/verilated.h.")
43
45 self,
46 sources: SourceFiles,
47 run_dir: Path,
48 debug: bool,
49 save_waveform: bool = False,
50 run_stdout_callback: Optional[Callable[[str], None]] = None,
51 run_stderr_callback: Optional[Callable[[str], None]] = None,
52 compile_stdout_callback: Optional[Callable[[str], None]] = None,
53 compile_stderr_callback: Optional[Callable[[str], None]] = None,
54 make_default_logs: bool = True,
55 macro_definitions: Optional[Dict[str, str]] = None,
56 ):
57 super().__init__(
58 sources=sources,
59 run_dir=run_dir,
60 debug=debug,
61 save_waveform=save_waveform,
62 run_stdout_callback=run_stdout_callback,
63 run_stderr_callback=run_stderr_callback,
64 compile_stdout_callback=compile_stdout_callback,
65 compile_stderr_callback=compile_stderr_callback,
66 make_default_logs=make_default_logs,
67 macro_definitions=macro_definitions,
68 )
69 # Set by _write_cmake when the generated CMakeLists.txt actually changed.
70 self._cmake_dirty = True
71
72 @property
73 def verilator_bin(self) -> Path:
74 vpath = Verilator._find_verilator_bin()
75 if vpath is None:
76 raise RuntimeError(Verilator.VerilatorBinNotFound)
77 return vpath
78
79 @staticmethod
80 def _find_verilator_bin() -> Optional[Path]:
81 """Locate the ``verilator_bin`` executable.
82
83 When ``VERILATOR_PATH`` is set it must point to a valid executable;
84 otherwise a ``RuntimeError`` is raised. Without it, ``verilator_bin`` is
85 looked up on ``PATH``. Returns ``None`` when nothing is found."""
86
87 def check_path(path: Path | str | None) -> Optional[Path]:
88 if isinstance(path, str):
89 path = Path(path)
90 if path is not None and path.exists() and path.is_file():
91 return path.resolve()
92 return None
93
94 if "VERILATOR_PATH" in os.environ:
95 vpath = Path(os.environ["VERILATOR_PATH"])
96 if vpath.stem == "verilator":
97 vpath = vpath.parent / "verilator_bin"
98 checked = check_path(vpath)
99 if checked is None:
100 raise RuntimeError(Verilator.VerilatorPathInvalid)
101 return checked
102 return check_path(shutil.which("verilator_bin"))
103
104 @staticmethod
105 def _find_verilator_root() -> Optional[Path]:
106 """Locate the Verilator root containing ``include/verilated.h``.
107
108 When ``VERILATOR_ROOT`` is set it must contain ``include/verilated.h``;
109 otherwise a ``RuntimeError`` is raised. Without it, the packaged root
110 (``$PREFIX/share/verilator``) is derived from the ``verilator_bin``
111 location. Returns ``None`` when nothing is found."""
112 if "VERILATOR_ROOT" in os.environ:
113 root = Path(os.environ["VERILATOR_ROOT"])
114 if (root / "include" / "verilated.h").exists():
115 return root
116 raise RuntimeError(Verilator.VerilatorRootInvalid)
117
118 verilator_bin = Verilator._find_verilator_bin()
119 if verilator_bin is None:
120 return None
121
122 # Packaged installations put Verilator's support files under
123 # $PREFIX/share/verilator, where $PREFIX is the bin directory's parent.
124 pkg_root = verilator_bin.parent.parent / "share" / "verilator"
125 if (pkg_root / "include" / "verilated.h").exists():
126 return pkg_root
127
128 return None
129
130 @property
131 def _use_cmake(self) -> bool:
132 """True when both cmake and ninja are available on PATH."""
133 return shutil.which("cmake") is not None and \
134 shutil.which("ninja") is not None
135
136 @staticmethod
137 def _raise_stack_limit() -> None:
138 """Lift the stack soft limit to the hard limit for the verilator process.
139
140 Verilator recurses over the design AST and segfaults on large designs with
141 the usual 8MB stack. Its ``verilator`` wrapper script normally runs
142 ``ulimit -s unlimited`` first; we invoke ``verilator_bin`` directly and so
143 have to do it ourselves. Subprocesses inherit the raised limit.
144 """
145 if os.name == "nt":
146 return
147 import resource
148 soft, hard = resource.getrlimit(resource.RLIMIT_STACK)
149 if soft == hard:
150 return
151 try:
152 resource.setrlimit(resource.RLIMIT_STACK, (hard, hard))
153 except (ValueError, OSError):
154 pass
155
156 @staticmethod
157 def _toolchain_args() -> List[str]:
158 """Prefer clang and lld when they are available.
159
160 Verilated code compiles about twice as fast with clang as with gcc, and
161 the model is a throwaway simulation binary, so the toolchain only affects
162 build time. Setting ``CXX`` or ``LDFLAGS`` opts back out.
163 """
164 if os.name == "nt":
165 return []
166 args = []
167 if not os.environ.get("CXX"):
168 clangxx = shutil.which("clang++")
169 if clangxx is not None:
170 args.append(f"-DCMAKE_CXX_COMPILER={clangxx}")
171 if not os.environ.get("LDFLAGS") and shutil.which("ld.lld") is not None:
172 args.append("-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld")
173 return args
174
175 @staticmethod
176 def _cmake_signature(cmake_cmd: List[str]) -> str:
177 """Serialize inputs that can affect CMake configuration."""
178 signature = {
179 "command": cmake_cmd,
180 "environment": {
181 name: os.environ.get(name) for name in Verilator._CMakeSignatureEnv
182 },
183 }
184 return json.dumps(signature, indent=2, sort_keys=True) + "\n"
185
186 def compile_commands(self) -> List[Simulator.CompileStep]:
187 """Return the compile steps for the full compile flow.
188
189 When cmake and ninja are available the returned list contains four
190 sequential steps:
191 1. ``verilator_bin`` – generates C++ from RTL.
192 2. Python callback – generates the CMakeLists.txt from the depfile.
193 3. Python callback – configures the C++ build when inputs changed.
194 4. ``ninja`` – builds the simulation executable.
195
196 Otherwise falls back to two commands:
197 1. ``verilator_bin --exe`` – generates C++ and a Makefile.
198 2. ``make`` – builds via the generated Makefile.
199 """
200 verilator_bin = self._find_verilator_bin()
201 if verilator_bin is None:
202 raise RuntimeError(Verilator.VerilatorBinNotFound)
203 verilator_root = self._find_verilator_root()
204 if verilator_root is None:
205 raise RuntimeError(Verilator.VerilatorRootNotFound)
206 os.environ["VERILATOR_ROOT"] = str(verilator_root)
207 self._raise_stack_limit()
208
209 verilator_cmd: List[str] = [
210 str(verilator_bin),
211 "--cc",
212 ]
213
214 if self.macro_definitions:
215 verilator_cmd += [
216 f"+define+{k}={v}" if v is not None else f"+define+{k}"
217 for k, v in self.macro_definitions.items()
218 ]
219
220 verilator_cmd += [
221 "--top-module",
222 self.sources.top,
223 "-DSIMULATION",
224 "-Wno-TIMESCALEMOD",
225 "-Wno-fatal",
226 "-sv",
227 "--verilate-jobs",
228 "0",
229 # Every generated .cpp re-parses the model headers, so file count sets
230 # the floor for the C++ build; 5000 balances that against parallelism.
231 "--output-split",
232 "5000",
233 ]
234 if self.debug:
235 verilator_cmd += [
236 "--assert",
237 "--trace-fst",
238 "--trace-structs",
239 "--trace-underscore",
240 ]
241
242 if self._use_cmake:
243 verilator_cmd += [str(p) for p in self.sources.rtl_sources]
244 build_dir = Path.cwd() / "obj_dir" / "cmake_build"
245 # ``CMAKE_BUILD_TYPE=Release`` is important on Windows: the prebuilt
246 # ``EsiCosimDpiServer.dll`` ships with the Release MSVC runtime, and
247 # mixing it with a Debug-runtime executable causes silent failures
248 # (e.g. transport/control connections come up but requests stall).
249 cmake_cmd = [
250 "cmake", "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", "-S",
251 str(build_dir), "-B",
252 str(build_dir)
253 ]
254 cmake_cmd += self._toolchain_args()
255 # If vcpkg is available, use its toolchain file so that
256 # ``find_package(ZLIB)`` (and other transitive deps) can pick up vcpkg
257 # installations. This is the standard story on Windows.
258 vcpkg_root = os.environ.get("VCPKG_ROOT") or os.environ.get(
259 "VCPKG_INSTALLATION_ROOT")
260 if vcpkg_root:
261 toolchain = Path(
262 vcpkg_root) / "scripts" / "buildsystems" / "vcpkg.cmake"
263 if toolchain.exists():
264 cmake_cmd.append(f"-DCMAKE_TOOLCHAIN_FILE={toolchain}")
265 ninja_cmd = ["ninja", "-C", str(build_dir)]
266 cmake_signature = self._cmake_signature(cmake_cmd)
267 signature_file = build_dir / self._CMakeSignatureFilename
268
269 def configure(cmake_cmd=cmake_cmd,
270 cmake_signature=cmake_signature,
271 signature_file=signature_file) -> int:
272 # Ninja regenerates an existing build graph when CMakeLists.txt changes.
273 # Run CMake explicitly only for a new tree or changed configure inputs.
274 signature_matches = signature_file.exists() and \
275 signature_file.read_text() == cmake_signature
276 if (build_dir / "build.ninja").exists() and signature_matches:
277 return 0
278 result = self._run_compile_command(cmake_cmd)
279 if result == 0:
280 signature_file.write_text(cmake_signature)
281 return result
282
283 return [
284 verilator_cmd, self._write_cmake_from_depfile_write_cmake_from_depfile, configure, ninja_cmd
285 ]
286
287 # -- make fallback --
288 # Let verilator generate a Makefile with --exe so it includes the
289 # driver, CFLAGS, and LDFLAGS directly.
290 verilator_cmd += ["--exe", str(Verilator.DefaultDriver)]
291 cflags = ["-DTOP_MODULE=" + self.sources.top]
292 if self.debug:
293 cflags.append("-DTRACE")
294 verilator_cmd += ["-CFLAGS", " ".join(cflags)]
295 if self.sources.dpi_so:
296 dpi_so_paths = self.sources.dpi_so_paths()
297 verilator_cmd += [
298 "-LDFLAGS",
299 " ".join(["-l" + so for so in self.sources.dpi_so]) + " " +
300 " ".join(["-L" + so.parent.as_posix() for so in dpi_so_paths]),
301 ]
302 verilator_cmd += [str(p) for p in self.sources.rtl_sources]
303 top = self.sources.top
304 make_cmd = ["make", "-C", "obj_dir", "-f", f"V{top}.mk", "-j"]
305 return [verilator_cmd, make_cmd]
306
307 def _depfile_path(self, obj_dir: Path) -> Path:
308 return obj_dir / f"V{self.sources.top}__ver.d"
309
310 def _generated_targets(self, depfile: Path) -> List[Path]:
311 depfile_contents = depfile.read_text().replace("\\\n", " ")
312 separator = re.search(r":\s", depfile_contents)
313 if separator is None:
314 raise RuntimeError(f"Malformed Verilator depfile: {depfile}")
315 return [(Path.cwd() / path).resolve()
316 for path in depfile_contents[:separator.start()].split()]
317
319 obj_dir = Path.cwd() / "obj_dir"
320 depfile = self._depfile_path(obj_dir)
321 generated_targets = self._generated_targets(depfile)
322 generated_sources = [
323 path for path in generated_targets if path.suffix == ".cpp"
324 ]
325 pch_header = next(
326 (path for path in generated_targets if path.name.endswith("__pch.h")),
327 None)
328 self._write_cmake(obj_dir, generated_sources, pch_header)
329 return 0
330
331 def _generated_cpp_sources(self, depfile: Path) -> List[Path]:
332 generated_sources = [
333 path for path in self._generated_targets(depfile)
334 if path.suffix == ".cpp"
335 ]
336 if not generated_sources:
337 raise RuntimeError(
338 f"No generated C++ sources found in depfile: {depfile}")
339 return generated_sources
340
341 @staticmethod
342 def _is_slow(source: Path) -> bool:
343 """Verilator suffixes cold-path TUs, including Syms/ConstPool, with
344 ``__Slow``."""
345 return source.stem.endswith("__Slow")
346
347 def _write_cmake(self,
348 obj_dir: Path,
349 generated_sources: List[Path],
350 pch_header: Optional[Path] = None) -> Path:
351 """Write a CMakeLists.txt for building the verilated simulation.
352
353 Returns the path to the CMake build directory."""
354
355 verilator_root = self._find_verilator_root()
356 if verilator_root is None:
357 raise RuntimeError(Verilator.VerilatorRootNotFound)
358 include_dir = verilator_root / "include"
359 exe_name = "V" + self.sources.top
360
361 slow_sources = [s for s in generated_sources if self._is_slow(s)]
362 fast_sources = [s for s in generated_sources if not self._is_slow(s)]
363
364 if os.name == "nt" and all(source.exists() for source in generated_sources):
365 # Verilator can emit deeply descriptive source filenames. CMake uses the
366 # source basename in MSVC's /Fo object path, which can overflow Windows'
367 # practical object path limits even after CMake hashes directories.
368 # Short local copies keep the build graph stable without changing the
369 # generated code or its includes.
370 short_source_dir = obj_dir / "cmake_src"
371 if short_source_dir.exists():
372 shutil.rmtree(short_source_dir)
373 short_source_dir.mkdir(parents=True)
374
375 def shorten(sources: List[Path], prefix: str) -> List[Path]:
376 shortened = []
377 for index, source in enumerate(sources):
378 destination = short_source_dir / f"{prefix}{index}.cpp"
379 shutil.copy2(source, destination)
380 shortened.append(destination)
381 return shortened
382
383 fast_sources = shorten(fast_sources, "vfast_")
384 slow_sources = shorten(slow_sources, "vslow_")
385
386 runtime_sources = [
387 include_dir / "verilated.cpp",
388 include_dir / "verilated_threads.cpp",
389 ]
390 # Include Verilator's DPI helpers when DPI shared objects are enabled.
391 if self.sources.dpi_so:
392 runtime_sources.append(include_dir / "verilated_dpi.cpp")
393 if self.debug:
394 runtime_sources.append(include_dir / "verilated_fst_c.cpp")
395 # Include constrained-randomization runtime when available (Verilator 5.x+).
396 random_cpp = include_dir / "verilated_random.cpp"
397 if random_cpp.exists():
398 runtime_sources.append(random_cpp)
399
400 rt_src = "\n ".join(s.as_posix() for s in runtime_sources)
401 driver = Path(Verilator.DefaultDriver).as_posix()
402 rt_and_driver = "\n ".join([s.as_posix() for s in runtime_sources] +
403 [driver])
404 inc = include_dir.as_posix()
405 vltstd = (include_dir / "vltstd").as_posix()
406
407 defs = [f"TOP_MODULE={self.sources.top}"]
408 if self.debug:
409 defs.append("TRACE")
410 defs_str = "\n ".join(defs)
411
412 # Link DPI shared objects by full path. On Windows, link against the
413 # ``.lib`` import library; the matching ``.dll`` is found at runtime via
414 # ``PATH`` (see ``Simulator.get_env``).
415 dpi_link = ""
416 if self.sources.dpi_so:
417 dpi_paths = self.sources.dpi_link_paths()
418 dpi_link = "\n ".join(p.as_posix() for p in dpi_paths)
419
420 # Separate object libraries so each optimization group gets its own
421 # precompiled header; one PCH cannot serve two different -O levels.
422 groups = []
423 obj_refs = []
424 for name, group_sources in (("vl_fast", fast_sources), ("vl_slow",
425 slow_sources)):
426 if not group_sources:
427 continue
428 listing = "\n ".join(s.as_posix() for s in group_sources)
429 opts = ("\ntarget_compile_options(vl_slow PRIVATE ${VL_OPT_SLOW})"
430 if name == "vl_slow" else "")
431 pch = ("" if pch_header is None else
432 f"\ntarget_precompile_headers({name} PRIVATE "
433 f"{pch_header.as_posix()})")
434 groups.append(f"""
435add_library({name} OBJECT
436 {listing}
437)
438target_link_libraries({name} PRIVATE vl_common){opts}{pch}""")
439 obj_refs.append(f"$<TARGET_OBJECTS:{name}>")
440 groups_str = "\n".join(groups)
441 obj_str = "\n ".join(obj_refs)
442
443 # Verilator's FST writer (debug builds) pulls in both zlib and lz4.
444 if self.debug:
445 zlib_find = ("find_package(ZLIB REQUIRED)\n"
446 "find_library(LZ4_LIBRARY NAMES lz4 REQUIRED)")
447 zlib_link = "ZLIB::ZLIB\n ${LZ4_LIBRARY}"
448 else:
449 zlib_find = ""
450 zlib_link = ""
451
452 content = f"""\
453cmake_minimum_required(VERSION 3.20)
454project({exe_name} CXX)
455
456set(CMAKE_CXX_STANDARD 17)
457set(CMAKE_CXX_STANDARD_REQUIRED ON)
458
459if(MSVC)
460 add_compile_options(/EHsc /bigobj)
461 set(VL_OPT_SLOW /Od)
462 set(VL_OPT_GLOBAL /O1)
463else()
464 set(VL_OPT_SLOW -O0)
465 set(VL_OPT_GLOBAL -Os)
466endif()
467
468find_package(Threads REQUIRED)
469{zlib_find}
470add_library(vl_common INTERFACE)
471
472target_include_directories(vl_common INTERFACE
473 {inc}
474 {vltstd}
475 ${{CMAKE_CURRENT_SOURCE_DIR}}/..
476)
477
478target_compile_definitions(vl_common INTERFACE
479 {defs_str}
480)
481{groups_str}
482
483add_executable({exe_name}
484 {obj_str}
485 {rt_src}
486 {driver}
487)
488
489set_source_files_properties(
490 {rt_and_driver}
491 PROPERTIES COMPILE_OPTIONS "${{VL_OPT_GLOBAL}}"
492)
493
494target_link_libraries({exe_name} PRIVATE
495 vl_common
496 Threads::Threads
497 {zlib_link}
498 {dpi_link}
499)
500"""
501 build_dir = obj_dir / "cmake_build"
502 build_dir.mkdir(parents=True, exist_ok=True)
503 cmake_file = build_dir / "CMakeLists.txt"
504 existing = cmake_file.read_text() if cmake_file.exists() else None
505 self._cmake_dirty = existing != content
506 if self._cmake_dirty:
507 cmake_file.write_text(content)
508 return build_dir
509
510 @property
511 def waveform_extension(self) -> str:
512 """Verilator's C++ driver uses ``VerilatedFstC`` — FST format."""
513 return ".fst"
514
515 def run_command(self, gui: bool):
516 if gui:
517 raise RuntimeError("Verilator does not support GUI mode.")
518 exe_name = "V" + self.sources.top
519 if os.name == "nt":
520 exe_name += ".exe"
521 if self._use_cmake:
522 exe = Path.cwd() / "obj_dir" / "cmake_build" / exe_name
523 else:
524 exe = Path.cwd() / "obj_dir" / exe_name
525 return [str(exe)]
static mlir::Operation * resolve(Context &context, mlir::SymbolRefAttr sym)
int _run_compile_command(self, CompileCommand cmd)
Definition simulator.py:304
str waveform_extension(self)
Definition verilator.py:511
Optional[Path] _find_verilator_root()
Definition verilator.py:105
List[Path] _generated_targets(self, Path depfile)
Definition verilator.py:310
int _write_cmake_from_depfile(self)
Definition verilator.py:318
bool _is_slow(Path source)
Definition verilator.py:342
str _cmake_signature(List[str] cmake_cmd)
Definition verilator.py:176
None _raise_stack_limit()
Definition verilator.py:137
Path _write_cmake(self, Path obj_dir, List[Path] generated_sources, Optional[Path] pch_header=None)
Definition verilator.py:350
List[str] _toolchain_args()
Definition verilator.py:157
Path _depfile_path(self, Path obj_dir)
Definition verilator.py:307
List[Path] _generated_cpp_sources(self, Path depfile)
Definition verilator.py:331
Path verilator_bin(self)
Definition verilator.py:73
bool _use_cmake(self)
Definition verilator.py:131
run_command(self, bool gui)
Definition verilator.py:515
List[Simulator.CompileStep] compile_commands(self)
Definition verilator.py:186
Optional[Path] _find_verilator_bin()
Definition verilator.py:80
__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)
Definition verilator.py:56