Coverage for tsfpga/vivado/project.py: 92%
269 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 20:51 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-17 20:51 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project, a project platform for modern FPGA development.
5# https://tsfpga.com
6# https://github.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9from __future__ import annotations
11import contextlib
12import re
13import shutil
14from copy import deepcopy
15from pathlib import Path
16from typing import TYPE_CHECKING, Any, NoReturn
18from tsfpga import TSFPGA_TCL
19from tsfpga.build_step_tcl_hook import BuildStepTclHook
20from tsfpga.constraint import Constraint
21from tsfpga.hdl_file import HdlFile
22from tsfpga.system_utils import create_file, read_file
24from .build_result import BuildResult
25from .common import run_vivado_gui, run_vivado_tcl, to_tcl_path
26from .hierarchical_utilization_parser import HierarchicalUtilizationParser
27from .logic_level_distribution_parser import LogicLevelDistributionParser
28from .tcl import VivadoTcl
29from .timing_parser import FoundNoSlackError, TimingParser
31if TYPE_CHECKING:
32 from tsfpga.module_list import ModuleList
33 from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue
35 from .build_result_checker import MaximumLogicLevel, SizeChecker
38class VivadoProject:
39 """
40 Used for handling a Xilinx Vivado HDL project
41 """
43 def __init__( # noqa: PLR0913
44 self,
45 name: str,
46 modules: ModuleList,
47 part: str,
48 top: str | None = None,
49 generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue]
50 | None = None,
51 constraints: list[Constraint] | None = None,
52 tcl_sources: list[Path] | None = None,
53 build_step_hooks: list[BuildStepTclHook] | None = None,
54 vivado_path: Path | None = None,
55 default_run_index: int = 1,
56 impl_explore: bool = False,
57 defined_at: Path | None = None,
58 **other_arguments: Any, # noqa: ANN401
59 ) -> None:
60 """
61 Class constructor. Performs a shallow copy of the mutable arguments, so that the user
62 can e.g. append items to their list after creating an object.
64 Arguments:
65 name: Project name.
66 modules: Modules that shall be included in the project.
67 part: Part identification.
68 top: Name of top level entity.
69 If left out, the top level name will be inferred from the ``name``.
70 generics: A dict with generics values (name: value). Use this parameter
71 for "static" generics that do not change between multiple builds of this
72 project. These will be set in the project when it is created.
74 Compare to the build-time generic argument in :meth:`build`.
76 The generic value shall be of type
78 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``),
79 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.),
80 * :class:`float` (suitable for VHDL type ``real``),
81 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``,
82 ``unsigned``, etc.), or
83 * :class:`.StringGenericValue` (suitable for VHDL type ``string``).
84 constraints: Constraints that will be applied to the project.
85 tcl_sources: A list of TCL files. Use for e.g. block design, pinning, settings, etc.
86 build_step_hooks: Build step hooks that will be applied to the project.
87 vivado_path: A path to the Vivado executable.
88 If omitted, the default location from the system PATH will be used.
89 default_run_index: Default run index (synth_X and impl_X) that is set in the
90 project.
91 Can also use the argument to :meth:`build() <VivadoProject.build>` to
92 specify at build-time.
93 impl_explore: Run multiple implementation strategies in parallel.
94 defined_at: Optional path to the file where you defined this project.
95 To get a useful ``build_fpga.py --list`` message. Is useful when you have many
96 projects set up.
97 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
98 instead be passed on to
100 * :func:`BaseModule.get_synthesis_files()
101 <tsfpga.module.BaseModule.get_synthesis_files>`
102 * :func:`BaseModule.get_ip_core_files()
103 <tsfpga.module.BaseModule.get_ip_core_files>`
104 * :func:`BaseModule.get_scoped_constraints()
105 <tsfpga.module.BaseModule.get_scoped_constraints>`
106 * :func:`VivadoProject.pre_create`
107 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
108 * :func:`VivadoProject.pre_build`
109 * :func:`VivadoProject.post_build`
111 along with further arguments supplied at build-time to :meth:`.create` and
112 :meth:`.build`.
114 .. note::
115 This is a "kwargs" style argument. You can pass any number of named arguments.
116 """
117 self.name = name
118 self.modules = modules.copy()
119 self.part = part
120 self.static_generics = {} if generics is None else generics.copy()
121 self.constraints = [] if constraints is None else constraints.copy()
122 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy()
123 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy()
124 self._vivado_path = vivado_path
125 self.default_run_index = default_run_index
126 self.impl_explore = impl_explore
127 self.defined_at = defined_at
128 self.other_arguments = None if other_arguments is None else other_arguments.copy()
130 # Will be set by subclass when applicable
131 self.is_netlist_build = False
132 self.open_and_analyze_synthesized_design = True
133 self.ip_cores_only = False
135 self.top = name + "_top" if top is None else top
137 self.tcl = VivadoTcl(name=self.name)
139 for constraint in self.constraints:
140 if not isinstance(constraint, Constraint):
141 raise TypeError(f'Got bad type for "constraints" element: {constraint}')
143 for tcl_source in self.tcl_sources:
144 if not isinstance(tcl_source, Path):
145 raise TypeError(f'Got bad type for "tcl_sources" element: {tcl_source}')
147 for build_step_hook in self.build_step_hooks:
148 if not isinstance(build_step_hook, BuildStepTclHook):
149 raise TypeError(f'Got bad type for "build_step_hooks" element: {build_step_hook}')
151 def project_file(self, project_path: Path) -> Path:
152 """
153 Arguments:
154 project_path: A path containing a Vivado project.
156 Return:
157 The project file of this project, in the given folder
158 """
159 return project_path / f"{self.name}.xpr"
161 def _setup_tcl_sources(self) -> None:
162 tsfpga_tcl_sources = [
163 TSFPGA_TCL / "vivado_default_run.tcl",
164 TSFPGA_TCL / "vivado_fast_run.tcl",
165 TSFPGA_TCL / "vivado_messages.tcl",
166 ]
168 if self.impl_explore:
169 tsfpga_tcl_sources.append(TSFPGA_TCL / "vivado_strategies.tcl")
171 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga
172 # settings. Conversely, tsfpga should not modify something that the user has set up.
173 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources
175 def _setup_and_create_build_step_hooks(
176 self, project_path: Path
177 ) -> dict[str, tuple[Path, list[BuildStepTclHook]]]:
178 """
179 Add all necessary tsfpga build step hooks to the list of hooks supplied by the user.
180 Create the TCL files for these hooks in the project folder.
181 """
182 # Shallow copy so that we do not append the state of this object.
183 # If this method is called twice, once at create-time and once at build-time, we do not
184 # want duplicates.
185 build_step_hooks = self.build_step_hooks.copy()
187 # Check that no ERROR messages have been sent by Vivado. After synthesis as well as
188 # after implementation.
189 build_step_hooks.append(
190 BuildStepTclHook(
191 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
192 )
193 )
194 build_step_hooks.append(
195 BuildStepTclHook(
196 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE"
197 )
198 )
200 # Check the implemented timing and resource utilization via TCL build hooks.
201 # This is different than for synthesis, where it is embedded in the build script.
202 # This is due to Vivado limitations related to post-synthesis hooks.
203 # Specifically, the report_utilization figures do not include IP cores when it is run in
204 # a post-synthesis hook.
205 build_step_hooks.append(
206 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
207 )
208 build_step_hooks.append(
209 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
210 )
211 build_step_hooks.append(
212 BuildStepTclHook(TSFPGA_TCL / "check_cdc.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
213 )
215 if not self.open_and_analyze_synthesized_design:
216 # In this special case, used only by the fastest netlist builds, the synthesized design
217 # is never opened (to save execution time).
218 # So in order to get access to some design metrics, we need to add hooks instead.
220 # Note that this report is does not report numbers from IP cores within the design,
221 # when the report is generated via a hook.
222 # But since this mode is used exclusively by netlist builds, which very rarely include
223 # IP cores, this is deemed acceptable on order to save time.
224 build_step_hooks.append(
225 BuildStepTclHook(
226 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
227 )
228 )
230 # Note that this report is better if generated on an open design, since it will then
231 # list each clock domain separately.
232 # If done like this with a hook, all paths will be on one line, no matter which clock
233 # domain they belong to.
234 build_step_hooks.append(
235 BuildStepTclHook(
236 TSFPGA_TCL / "report_logic_level_distribution.tcl",
237 "STEPS.SYNTH_DESIGN.TCL.POST",
238 )
239 )
241 organized_build_step_hooks = self._organize_build_step_hooks(
242 build_step_hooks=build_step_hooks, project_folder=project_path
243 )
244 self._create_build_step_hook_files(build_step_hooks=organized_build_step_hooks)
246 return organized_build_step_hooks
248 @staticmethod
249 def _organize_build_step_hooks(
250 build_step_hooks: list[BuildStepTclHook], project_folder: Path
251 ) -> dict[str, tuple[Path, list[BuildStepTclHook]]]:
252 """
253 Since there can be many hooks for the same step, reorganize them into a dict:
254 {step name: (script file in project, [list of hooks for that step])}
256 Vivado will only accept one TCL script as hook for each step.
257 So if we want to add more we have to create a new TCL file, that sources the other files,
258 and add that as the hook to Vivado.
259 """
260 result = {}
261 for build_step_hook in build_step_hooks:
262 if build_step_hook.hook_step in result:
263 result[build_step_hook.hook_step][1].append(build_step_hook)
264 else:
265 tcl_file = project_folder / (
266 "hook_" + build_step_hook.hook_step.replace(".", "_") + ".tcl"
267 )
268 result[build_step_hook.hook_step] = (tcl_file, [build_step_hook])
270 return result
272 def _create_build_step_hook_files(
273 self, build_step_hooks: dict[str, tuple[Path, list[BuildStepTclHook]]]
274 ) -> None:
275 for step_name, (tcl_file, hooks) in build_step_hooks.items():
276 source_hooks_tcl = "\n".join(
277 [f"source {{{to_tcl_path(hook.tcl_file)}}}" for hook in hooks]
278 )
279 create_file(
280 tcl_file,
281 f"""\
282# ------------------------------------------------------------------------------
283# Hook script for the "{step_name}" build step.
284# This file is auto-generated by tsfpga. Do not edit manually.
285{source_hooks_tcl}
286""",
287 )
289 def _create_tcl(
290 self,
291 project_path: Path,
292 ip_cache_path: Path | None,
293 build_step_hooks: dict[str, tuple[Path, list[BuildStepTclHook]]],
294 all_arguments: dict[str, Any],
295 ) -> Path:
296 """
297 Make a TCL file that creates a Vivado project
298 """
299 project_file = self.project_file(project_path=project_path)
300 if project_file.exists():
301 raise ValueError(f'Project "{self.name}" already exists: {project_file}')
302 project_path.mkdir(parents=True, exist_ok=True)
304 create_vivado_project_tcl = project_path / "create_vivado_project.tcl"
305 tcl = self.tcl.create(
306 project_folder=project_path,
307 modules=self.modules,
308 part=self.part,
309 top=self.top,
310 run_index=self.default_run_index,
311 generics=self.static_generics,
312 constraints=self.constraints,
313 tcl_sources=self.tcl_sources,
314 build_step_hooks=build_step_hooks,
315 ip_cache_path=ip_cache_path,
316 disable_io_buffers=self.is_netlist_build,
317 ip_cores_only=self.ip_cores_only,
318 other_arguments=all_arguments,
319 )
320 create_file(create_vivado_project_tcl, tcl)
322 return create_vivado_project_tcl
324 def create(
325 self,
326 project_path: Path,
327 ip_cache_path: Path | None = None,
328 **other_arguments: Any, # noqa: ANN401
329 ) -> bool:
330 """
331 Create a Vivado project
333 Arguments:
334 project_path: Path where the project shall be placed.
335 ip_cache_path: Path to a folder where the Vivado IP cache can be
336 placed. If omitted, the Vivado IP cache mechanism will not be enabled.
337 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
338 instead be sent to
340 * :func:`BaseModule.get_synthesis_files()
341 <tsfpga.module.BaseModule.get_synthesis_files>`
342 * :func:`BaseModule.get_ip_core_files()
343 <tsfpga.module.BaseModule.get_ip_core_files>`
344 * :func:`BaseModule.get_scoped_constraints()
345 <tsfpga.module.BaseModule.get_scoped_constraints>`
346 * :func:`VivadoProject.pre_create`
348 along with further ``other_arguments`` supplied to :meth:`.__init__`.
350 .. note::
351 This is a "kwargs" style argument. You can pass any number of named arguments.
353 Return:
354 True if everything went well.
355 """
356 print(f"Creating Vivado project in {project_path}")
357 self._setup_tcl_sources()
358 build_step_hooks = self._setup_and_create_build_step_hooks(project_path=project_path)
360 # The pre-create hook might have side effects. E.g. change some register constants.
361 # So we make a deep copy of the module list before the hook is called.
362 # Note that the modules are copied before the pre-build hooks as well,
363 # since we do not know if we might be performing a create-only or
364 # build-only operation. The copy does not take any significant time, so this is not
365 # an issue.
366 self.modules = deepcopy(self.modules)
368 # Send all available arguments that are reasonable to use in pre-create and module getter
369 # functions. Prefer run-time values over the static.
370 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments)
371 all_arguments.update(generics=self.static_generics, part=self.part)
373 if not self.pre_create(
374 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments
375 ):
376 print("ERROR: Project pre-create hook returned False. Failing the build.")
377 return False
379 create_vivado_project_tcl = self._create_tcl(
380 project_path=project_path,
381 ip_cache_path=ip_cache_path,
382 build_step_hooks=build_step_hooks,
383 all_arguments=all_arguments,
384 )
385 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl)
387 def pre_create(
388 self,
389 **kwargs: Any, # noqa: ANN401, ARG002
390 ) -> bool:
391 """
392 Override this function in a subclass if you wish to do something useful with it.
393 Will be called from :meth:`.create` right before the call to Vivado.
395 An example use case for this function is when TCL source scripts for the Vivado project
396 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the
397 Vivado system PATH.
399 .. Note::
400 This default method does nothing. Shall be overridden by project that utilize
401 this mechanism.
403 Arguments:
404 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in
405 the ``other_arguments`` argument to :func:`VivadoProject.__init__`.
407 Return:
408 True if everything went well.
409 """
410 return True
412 def _build_tcl( # noqa: PLR0913
413 self,
414 project_path: Path,
415 output_path: Path | None,
416 num_threads: int,
417 run_index: int,
418 all_generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue],
419 synth_only: bool,
420 from_impl: bool,
421 impl_explore: bool,
422 ) -> Path:
423 """
424 Make a TCL file that builds a Vivado project
425 """
426 project_file = self.project_file(project_path=project_path)
427 if not project_file.exists():
428 raise ValueError(
429 f'Project "{self.name}" does not exist in the specified location: {project_file}'
430 )
432 build_vivado_project_tcl = project_path / "build_vivado_project.tcl"
433 tcl = self.tcl.build(
434 project_file=project_file,
435 output_path=output_path,
436 num_threads=num_threads,
437 run_index=run_index,
438 generics=all_generics,
439 synth_only=synth_only,
440 from_impl=from_impl,
441 open_and_analyze_synthesized_design=self.open_and_analyze_synthesized_design,
442 impl_explore=impl_explore,
443 )
444 create_file(build_vivado_project_tcl, tcl)
446 return build_vivado_project_tcl
448 def pre_build(
449 self,
450 **kwargs: Any, # noqa: ANN401, ARG002
451 ) -> bool:
452 """
453 Override this function in a subclass if you wish to do something useful with it.
454 Will be called from :meth:`.build` right before the call to Vivado.
456 Arguments:
457 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
458 parameters from the user.
460 Return:
461 True if everything went well.
462 """
463 return True
465 def post_build(
466 self,
467 **kwargs: Any, # noqa: ANN401, ARG002
468 ) -> bool:
469 """
470 Override this function in a subclass if you wish to do something useful with it.
471 Will be called from :meth:`.build` right after the call to Vivado.
473 An example use case for this function is to encrypt the bit file, or generate any other
474 material that shall be included in FPGA release artifacts.
476 .. Note::
477 This default method does nothing. Shall be overridden by project that utilize
478 this mechanism.
480 Arguments:
481 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
482 parameters from the user. Will also include ``build_result`` with
483 implemented/synthesized size, which can be used for asserting the expected resource
484 utilization.
486 Return:
487 True if everything went well.
488 """
489 return True
491 def build( # noqa: C901, PLR0912, PLR0913
492 self,
493 project_path: Path,
494 output_path: Path | None = None,
495 run_index: int | None = None,
496 generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue]
497 | None = None,
498 synth_only: bool = False,
499 from_impl: bool = False,
500 num_threads: int = 12,
501 **pre_and_post_build_parameters: Any, # noqa: ANN401
502 ) -> BuildResult:
503 """
504 Build a Vivado project
506 Arguments:
507 project_path: A path containing a Vivado project.
508 output_path: Results (bit file, ...) will be placed here.
509 run_index: Select Vivado run (synth_X and impl_X) to build with.
510 generics: A dict with generics values (`dict(name: value)`). Use for run-time
511 generics, i.e. values that can change between each build of this project.
513 Compare to the create-time generics argument in :meth:`.__init__`.
515 The generic value types follow the same rules as for :meth:`.__init__`.
516 synth_only: Run synthesis and then stop.
517 from_impl: Run the ``impl`` steps and onward on an existing synthesized design.
518 num_threads: Number of parallel threads to use during run.
519 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga,
520 but will instead be sent to
522 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
523 * :func:`VivadoProject.pre_build`
524 * :func:`VivadoProject.post_build`
526 along with further ``other_arguments`` supplied to :meth:`.__init__`.
528 .. note::
529 This is a "kwargs" style argument. You can pass any number of named arguments.
531 Return:
532 Result object with build information.
533 """
534 synth_only = synth_only or self.is_netlist_build
536 if synth_only:
537 print(f"Synthesizing Vivado project in {project_path}")
538 else:
539 if output_path is None:
540 raise ValueError("Must specify 'output_path' when doing an implementation build.")
542 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}")
544 # Combine to all available generics. Prefer run-time values over static.
545 all_generics = copy_and_combine_dicts(self.static_generics, generics)
547 # Run index is optional to specify at build-time
548 run_index = self.default_run_index if run_index is None else run_index
550 # Send all available information to pre- and post build functions. Prefer build-time values
551 # over the static arguments.
552 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters)
553 all_parameters.update(
554 project_path=project_path,
555 output_path=output_path,
556 run_index=run_index,
557 generics=all_generics,
558 synth_only=synth_only,
559 from_impl=from_impl,
560 num_threads=num_threads,
561 )
563 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks)
564 # might have side effects. E.g. change some register constants. So we make a deep copy of
565 # the module list before any of these hooks are called. Note that the modules are copied
566 # before the pre-create hook as well, since we do not know if we might be performing a
567 # create-only or build-only operation. The copy does not take any significant time, so this
568 # is not an issue.
569 self.modules = deepcopy(self.modules)
571 result = BuildResult(name=self.name, synthesis_run_name=f"synth_{run_index}")
573 for module in self.modules:
574 if not module.pre_build(project=self, **all_parameters):
575 print(
576 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build."
577 )
578 result.success = False
579 return result
581 # Make sure register packages are up to date
582 module.create_register_synthesis_files()
584 if not self.pre_build(**all_parameters):
585 print("ERROR: Project pre-build hook returned False. Failing the build.")
586 result.success = False
587 return result
589 # We ignore the type of 'output_path' going from 'Path | None' to 'Path'.
590 # It is only used if 'synth_only' is False, and we have an assertion that 'output_path' is
591 # not None in that case above.
593 build_vivado_project_tcl = self._build_tcl(
594 project_path=project_path,
595 output_path=output_path,
596 num_threads=num_threads,
597 run_index=run_index,
598 all_generics=all_generics,
599 synth_only=synth_only,
600 from_impl=from_impl,
601 impl_explore=self.impl_explore,
602 )
604 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl):
605 result.success = False
606 return result
608 result.synthesis_size = self._get_size(
609 project_path=project_path, run_name=f"synth_{run_index}"
610 )
612 if not synth_only:
613 if self.impl_explore:
614 runs_path = project_path / f"{self.name}.runs"
615 for run_path in runs_path.iterdir():
616 if "impl_explore_" in run_path.name:
617 # Check files for existence, since not all runs may have completed
618 bit_file = run_path / f"{self.top}.bit"
619 bin_file = run_path / f"{self.top}.bin"
620 if bit_file.exists() or bin_file.exists():
621 result.implementation_run_name = run_path.name
622 break
623 else:
624 result.implementation_run_name = f"impl_{run_index}"
625 impl_folder = project_path / f"{self.name}.runs" / result.implementation_run_name
626 bit_file = impl_folder / f"{self.top}.bit"
627 bin_file = impl_folder / f"{self.top}.bin"
629 shutil.copy2(bit_file, output_path / f"{self.name}.bit")
630 shutil.copy2(bin_file, output_path / f"{self.name}.bin")
631 result.implementation_size = self._get_size(
632 project_path=project_path, run_name=result.implementation_run_name
633 )
635 # Send the result object, along with everything else, to the post-build function
636 all_parameters.update(build_result=result)
638 if not self.post_build(**all_parameters):
639 print("ERROR: Project post-build hook returned False. Failing the build.")
640 result.success = False
642 return result
644 def open(self, project_path: Path) -> bool:
645 """
646 Open the project in Vivado GUI.
648 Arguments:
649 project_path: A path containing a Vivado project.
651 Return:
652 True if everything went well.
653 """
654 return run_vivado_gui(self._vivado_path, self.project_file(project_path))
656 def _get_size(self, project_path: Path, run_name: str) -> dict[str, int]:
657 """
658 Read the hierarchical utilization report and return the top level size
659 for the specified run.
660 """
661 report_as_string = read_file(
662 project_path / f"{self.name}.runs" / run_name / "hierarchical_utilization.rpt"
663 )
664 return HierarchicalUtilizationParser.get_size(report_as_string)
666 def __str__(self) -> str:
667 result = f"{self.name}\n"
669 if self.defined_at is not None:
670 result += f"Defined at: {self.defined_at.resolve()}\n"
672 result += f"Type: {self.__class__.__name__}\n"
673 result += f"Top level: {self.top}\n"
675 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-"
676 result += f"Generics: {generics}\n"
678 if self.other_arguments:
679 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n"
681 return result
683 @staticmethod
684 def _dict_to_string(data: dict[str, Any]) -> str:
685 return ", ".join([f"{name}={value}" for name, value in data.items()])
688class VivadoNetlistProject(VivadoProject):
689 """
690 Used for handling Vivado build of a module without top level pinning.
691 """
693 _clock_period_ns = 2.0
695 def __init__(
696 self,
697 analyze_synthesis_timing: bool = False,
698 build_result_checkers: list[SizeChecker | MaximumLogicLevel] | None = None,
699 **kwargs: Any, # noqa: ANN401
700 ) -> None:
701 """
702 Arguments:
703 analyze_synthesis_timing: Enable analysis of the synthesized design's timing.
704 This will make the build flow open the design, check for unhandled clock
705 crossings, pulse width violations, etc, and calculate a maximum frequency estimate.
707 .. note::
708 Enabling this will add significant build time (can be as much as +40%).
709 build_result_checkers:
710 Checkers that will be executed after a successful build. Is used to automatically
711 check that e.g. resource utilization is not greater than expected.
712 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`.
713 """
714 super().__init__(**kwargs)
716 self.is_netlist_build = True
717 self.open_and_analyze_synthesized_design = analyze_synthesis_timing
718 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers
720 def create(
721 self,
722 project_path: Path,
723 **kwargs: Any, # noqa: ANN401
724 ) -> bool:
725 """
726 Create the project.
728 Arguments:
729 project_path: Path where the project shall be placed.
730 kwargs: All arguments as accepted by :meth:`.VivadoProject.create`.
731 """
732 # Create and add a TCL for auto-creating clocks.
733 # Whether it is used or not depends on settings, but note that these settings can
734 # change between subsequent builds, so we need to always have the file in place and be part
735 # of the project.
736 tcl_path = create_file(
737 self._get_auto_clock_constraint_path(project_path=project_path), contents="# Unused.\n"
738 )
739 # Add it "early" so that any other user constraints that might be in place
740 # can override the clocks.
741 self.constraints.append(Constraint(file=tcl_path, processing_order="early"))
743 if self.open_and_analyze_synthesized_design:
744 self._set_auto_clock_constraint(tcl_path=tcl_path)
746 return super().create(project_path=project_path, **kwargs)
748 def build(
749 self,
750 project_path: Path,
751 **kwargs: Any, # noqa: ANN401
752 ) -> BuildResult:
753 """
754 Build the project.
756 Arguments:
757 project_path: A path containing a Vivado project.
758 kwargs: All other arguments as accepted by :meth:`.VivadoProject.build`.
759 """
760 # Update hook script files, since the user might turn on and off the
761 # 'analyze_synthesis_timing' flag, which affects what scripts are added to the
762 # post-synthesis hook step.
763 self._setup_and_create_build_step_hooks(project_path=project_path)
765 if self.open_and_analyze_synthesized_design:
766 # Update, since the HDL (and details about clocks) might have changed since last time.
767 self._set_auto_clock_constraint(
768 tcl_path=self._get_auto_clock_constraint_path(project_path=project_path)
769 )
771 result = super().build(project_path=project_path, **kwargs)
773 if not result.success:
774 print(f'Can not do post-build check for "{self.name}" since it did not succeed.')
775 return result
777 result.success = result.success and self._check_size(build_result=result)
779 run_path = project_path / f"{self.name}.runs" / result.synthesis_run_name
781 if self.open_and_analyze_synthesized_design:
782 # Report might not exist or might not contain any slack information,
783 # if we could not auto detect any clocks.
784 # Could happen if the top-level file is Verilog, or if there are no clocks at all,
785 # or if our auto-detect failed.
786 with contextlib.suppress(FileNotFoundError, FoundNoSlackError):
787 slack_ns = TimingParser.get_slack_ns(read_file(run_path / "timing.rpt"))
788 # Positive slack = margin, meaning we can use a lower period,
789 # meaning higher frequency.
790 # Hence the subtraction.
791 result.maximum_synthesis_frequency_hz = 1e9 / (self._clock_period_ns - slack_ns)
793 result.logic_level_distribution = self._get_logic_level_distribution(run_path=run_path)
795 return result
797 def _get_auto_clock_constraint_path(self, project_path: Path) -> Path:
798 return project_path / f"auto_create_{self.top}_clocks.tcl"
800 def _set_auto_clock_constraint(self, tcl_path: Path) -> None:
801 # Try to auto-detect clocks in the top-level file, and create them automatically.
802 top_file = self._find_top_level_file()
803 clock_names = (
804 self._find_vhdl_clock_names(vhdl_file=top_file)
805 if top_file.path.suffix.lower() in HdlFile.file_endings_mapping[HdlFile.Type.VHDL]
806 else []
807 )
809 if clock_names:
810 create_clock_tcl = "\n".join(
811 [
812 f'create_clock -name "{clock_name}" -period {self._clock_period_ns} '
813 f'[get_ports "{clock_name}"];'
814 for clock_name in clock_names
815 ]
816 )
817 tcl = f"""\
818# Auto-create the clocks found in the top-level HDL file:
819# {top_file.path}
820{create_clock_tcl}
821"""
822 create_file(tcl_path, tcl)
824 def _find_top_level_file(self) -> HdlFile:
825 top_files = [
826 hdl_file
827 for module in self.modules
828 for hdl_file in module.get_synthesis_files()
829 if hdl_file.path.stem == self.top
830 ]
831 if len(top_files) == 0:
832 raise ValueError(
833 f'Could not find HDL source file corresponding to top-level "{self.top}".'
834 )
835 if len(top_files) > 1:
836 raise ValueError(
837 f"Found multiple HDL source files corresponding to "
838 f'top-level "{self.top}": {top_files}.'
839 )
841 return top_files[0]
843 def _find_vhdl_clock_names(self, vhdl_file: HdlFile) -> list[str]:
844 """
845 Find a list of all clock port names in the VHDL file.
846 It handles all ports that contain "clk" or "clock" in their name as clocks.
847 This magic word can be either in the beginning, middle or end of the port name
848 (separated by underscore).
849 """
850 top_vhd = read_file(vhdl_file.path)
852 entity_matches = re.findall(
853 rf"^\s*entity\s+{self.top}\s+is(.+)^\s*end\s+entity",
854 top_vhd,
855 re.DOTALL | re.MULTILINE | re.IGNORECASE,
856 )
857 if len(entity_matches) == 0:
858 raise ValueError(
859 f'Could not find "{self.top}" entity declaration in "{vhdl_file.path}".'
860 )
861 if len(entity_matches) > 1:
862 raise ValueError(
863 f'Found multiple "{self.top}" entity declarations in "{vhdl_file.path}".'
864 )
866 entity_vhd = entity_matches[0]
867 port_matches = re.findall(
868 r"^\s*port(\s|\()(.+)$",
869 entity_vhd,
870 re.DOTALL | re.MULTILINE | re.IGNORECASE,
871 )
873 if len(port_matches) == 0:
874 raise ValueError(f'Could not find "port" block in "{vhdl_file.path}".')
875 if len(port_matches) > 1:
876 raise ValueError(f'Found multiple "port" blocks in "{vhdl_file.path}".')
878 port_vhd = port_matches[0][1]
879 clock_matches = re.findall(
880 r"^\s*(\w+_)?(clk|clock)(_\w+)?\s*:",
881 port_vhd,
882 re.DOTALL | re.MULTILINE | re.IGNORECASE,
883 )
885 return [f"{prefix}{clock}{suffix}" for prefix, clock, suffix in clock_matches]
887 def _check_size(self, build_result: BuildResult) -> bool:
888 success = True
889 for build_result_checker in self.build_result_checkers:
890 checker_result = build_result_checker.check(build_result)
891 success = success and checker_result
893 return success
895 @staticmethod
896 def _get_logic_level_distribution(run_path: Path) -> str:
897 return LogicLevelDistributionParser.get_table(
898 read_file(run_path / "logic_level_distribution.rpt")
899 )
902class VivadoIpCoreProject(VivadoProject):
903 """
904 A Vivado project that is only used to generate simulation models of IP cores.
905 """
907 ip_cores_only = True
909 def __init__(
910 self,
911 **kwargs: Any, # noqa: ANN401
912 ) -> None:
913 """
914 Arguments:
915 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`.
916 """
917 super().__init__(**kwargs)
919 def build(
920 self,
921 **kwargs: Any, # noqa: ANN401
922 ) -> NoReturn:
923 """
924 Not implemented.
925 """
926 raise NotImplementedError("IP core project can not be built")
929def copy_and_combine_dicts(
930 dict_first: dict[str, Any] | None, dict_second: dict[str, Any] | None
931) -> dict[str, Any]:
932 """
933 Will prefer values in the second dict, in case the same key occurs in both.
934 Will return an empty dictionary if both are ``None``.
935 """
936 if dict_first is None:
937 if dict_second is None:
938 return {}
940 return dict_second.copy()
942 if dict_second is None:
943 return dict_first.copy()
945 result = dict_first.copy()
946 result.update(dict_second)
948 return result