Coverage for tsfpga/vivado/project.py: 89%
208 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-21 20:51 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-21 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 shutil
12from copy import deepcopy
13from pathlib import Path
14from typing import TYPE_CHECKING, Any, NoReturn
16from tsfpga import TSFPGA_TCL
17from tsfpga.build_step_tcl_hook import BuildStepTclHook
18from tsfpga.constraint import Constraint
19from tsfpga.system_utils import create_file, read_file
21from .build_result import BuildResult
22from .common import run_vivado_gui, run_vivado_tcl
23from .hierarchical_utilization_parser import HierarchicalUtilizationParser
24from .logic_level_distribution_parser import LogicLevelDistributionParser
25from .tcl import VivadoTcl
27if TYPE_CHECKING:
28 from tsfpga.module_list import ModuleList
30 from .build_result_checker import MaximumLogicLevel, SizeChecker
33class VivadoProject:
34 """
35 Used for handling a Xilinx Vivado HDL project
36 """
38 def __init__( # noqa: PLR0913
39 self,
40 name: str,
41 modules: ModuleList,
42 part: str,
43 top: str | None = None,
44 generics: dict[str, Any] | None = None,
45 constraints: list[Constraint] | None = None,
46 tcl_sources: list[Path] | None = None,
47 build_step_hooks: list[BuildStepTclHook] | None = None,
48 vivado_path: Path | None = None,
49 default_run_index: int = 1,
50 impl_explore: bool = False,
51 defined_at: Path | None = None,
52 **other_arguments: Any, # noqa: ANN401
53 ) -> None:
54 """
55 Class constructor. Performs a shallow copy of the mutable arguments, so that the user
56 can e.g. append items to their list after creating an object.
58 Arguments:
59 name: Project name.
60 modules: Modules that shall be included in the project.
61 part: Part identification.
62 top: Name of top level entity.
63 If left out, the top level name will be inferred from the ``name``.
64 generics: A dict with generics values (name: value). Use this parameter
65 for "static" generics that do not change between multiple builds of this
66 project. These will be set in the project when it is created.
68 Compare to the build-time generic argument in :meth:`build`.
70 The generic value shall be of type
72 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``),
73 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.),
74 * :class:`float` (suitable for VHDL type ``real``),
75 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``,
76 ``unsigned``, etc.), or
77 * :class:`.StringGenericValue` (suitable for VHDL type ``string``).
78 constraints: Constraints that will be applied to the project.
79 tcl_sources: A list of TCL files. Use for e.g. block design, pinning, settings, etc.
80 build_step_hooks: Build step hooks that will be applied to the project.
81 vivado_path: A path to the Vivado executable.
82 If omitted, the default location from the system PATH will be used.
83 default_run_index: Default run index (synth_X and impl_X) that is set in the
84 project.
85 Can also use the argument to :meth:`build() <VivadoProject.build>` to
86 specify at build-time.
87 impl_explore: Run multiple implementation strategies in parallel.
88 defined_at: Optional path to the file where you defined this project.
89 To get a useful ``build_fpga.py --list`` message. Is useful when you have many
90 projects set up.
91 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
92 instead be passed on to
94 * :func:`BaseModule.get_synthesis_files()
95 <tsfpga.module.BaseModule.get_synthesis_files>`
96 * :func:`BaseModule.get_ip_core_files()
97 <tsfpga.module.BaseModule.get_ip_core_files>`
98 * :func:`BaseModule.get_scoped_constraints()
99 <tsfpga.module.BaseModule.get_scoped_constraints>`
100 * :func:`VivadoProject.pre_create`
101 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
102 * :func:`VivadoProject.pre_build`
103 * :func:`VivadoProject.post_build`
105 along with further arguments supplied at build-time to :meth:`.create` and
106 :meth:`.build`.
108 .. note::
109 This is a "kwargs" style argument. You can pass any number of named arguments.
110 """
111 self.name = name
112 self.modules = modules.copy()
113 self.part = part
114 self.static_generics = {} if generics is None else generics.copy()
115 self.constraints = [] if constraints is None else constraints.copy()
116 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy()
117 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy()
118 self._vivado_path = vivado_path
119 self.default_run_index = default_run_index
120 self.impl_explore = impl_explore
121 self.defined_at = defined_at
122 self.other_arguments = None if other_arguments is None else other_arguments.copy()
124 # Will be set by subclass when applicable
125 self.is_netlist_build = False
126 self.analyze_synthesis_timing = True
127 self.report_logic_level_distribution = False
128 self.ip_cores_only = False
130 self.top = name + "_top" if top is None else top
132 self.tcl = VivadoTcl(name=self.name)
134 for constraint in self.constraints:
135 if not isinstance(constraint, Constraint):
136 raise TypeError(f'Got bad type for "constraints" element: {constraint}')
138 for tcl_source in self.tcl_sources:
139 if not isinstance(tcl_source, Path):
140 raise TypeError(f'Got bad type for "tcl_sources" element: {tcl_source}')
142 for build_step_hook in self.build_step_hooks:
143 if not isinstance(build_step_hook, BuildStepTclHook):
144 raise TypeError(f'Got bad type for "build_step_hooks" element: {build_step_hook}')
146 def project_file(self, project_path: Path) -> Path:
147 """
148 Arguments:
149 project_path: A path containing a Vivado project.
151 Return:
152 The project file of this project, in the given folder
153 """
154 return project_path / (self.name + ".xpr")
156 def _setup_tcl_sources(self) -> None:
157 tsfpga_tcl_sources = [
158 TSFPGA_TCL / "vivado_default_run.tcl",
159 TSFPGA_TCL / "vivado_fast_run.tcl",
160 TSFPGA_TCL / "vivado_messages.tcl",
161 ]
163 if self.impl_explore:
164 tsfpga_tcl_sources.append(TSFPGA_TCL / "vivado_strategies.tcl")
166 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga
167 # settings. Conversely, tsfpga should not modify something that the user has set up.
168 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources
170 def _setup_build_step_hooks(self) -> None:
171 # Check that no ERROR messages have been sent by Vivado. After synthesis as well as
172 # after implementation.
173 self.build_step_hooks.append(
174 BuildStepTclHook(
175 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
176 )
177 )
178 self.build_step_hooks.append(
179 BuildStepTclHook(
180 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE"
181 )
182 )
184 # Check the implemented timing and resource utilization via TCL build hooks.
185 # This is different than for synthesis, where it is embedded in the build script.
186 # This is due to Vivado limitations related to post-synthesis hooks.
187 # Specifically, the report_utilization figures do not include IP cores when it is run in
188 # a post-synthesis hook.
189 self.build_step_hooks.append(
190 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
191 )
192 self.build_step_hooks.append(
193 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
194 )
195 self.build_step_hooks.append(
196 BuildStepTclHook(TSFPGA_TCL / "check_cdc.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
197 )
199 if not self.analyze_synthesis_timing:
200 # In this special case however, the synthesized design is never opened (to save
201 # execution time), meaning 'report_utilization' is not run by the
202 # 'build_vivado_project.tcl' script.
203 # So in order to get a utilization report anyway we add it as a hook.
204 # This mode is exclusively used by netlist builds, which very rarely include IP cores,
205 # so it is acceptable that the utilization report might be erroneous with regards to
206 # IP cores.
207 self.build_step_hooks.append(
208 BuildStepTclHook(
209 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
210 )
211 )
213 if self.report_logic_level_distribution:
214 # Used by netlist builds
215 self.build_step_hooks.append(
216 BuildStepTclHook(
217 TSFPGA_TCL / "report_logic_level_distribution.tcl",
218 "STEPS.SYNTH_DESIGN.TCL.POST",
219 )
220 )
222 def _create_tcl(
223 self, project_path: Path, ip_cache_path: Path | None, all_arguments: dict[str, Any]
224 ) -> Path:
225 """
226 Make a TCL file that creates a Vivado project
227 """
228 if project_path.exists():
229 raise ValueError(f"Folder already exists: {project_path}")
230 project_path.mkdir(parents=True)
232 create_vivado_project_tcl = project_path / "create_vivado_project.tcl"
233 tcl = self.tcl.create(
234 project_folder=project_path,
235 modules=self.modules,
236 part=self.part,
237 top=self.top,
238 run_index=self.default_run_index,
239 generics=self.static_generics,
240 constraints=self.constraints,
241 tcl_sources=self.tcl_sources,
242 build_step_hooks=self.build_step_hooks,
243 ip_cache_path=ip_cache_path,
244 disable_io_buffers=self.is_netlist_build,
245 ip_cores_only=self.ip_cores_only,
246 other_arguments=all_arguments,
247 )
248 create_file(create_vivado_project_tcl, tcl)
250 return create_vivado_project_tcl
252 def create(
253 self,
254 project_path: Path,
255 ip_cache_path: Path | None = None,
256 **other_arguments: Any, # noqa: ANN401
257 ) -> bool:
258 """
259 Create a Vivado project
261 Arguments:
262 project_path: Path where the project shall be placed.
263 ip_cache_path: Path to a folder where the Vivado IP cache can be
264 placed. If omitted, the Vivado IP cache mechanism will not be enabled.
265 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
266 instead be sent to
268 * :func:`BaseModule.get_synthesis_files()
269 <tsfpga.module.BaseModule.get_synthesis_files>`
270 * :func:`BaseModule.get_ip_core_files()
271 <tsfpga.module.BaseModule.get_ip_core_files>`
272 * :func:`BaseModule.get_scoped_constraints()
273 <tsfpga.module.BaseModule.get_scoped_constraints>`
274 * :func:`VivadoProject.pre_create`
276 along with further ``other_arguments`` supplied to :meth:`.__init__`.
278 .. note::
279 This is a "kwargs" style argument. You can pass any number of named arguments.
281 Return:
282 True if everything went well.
283 """
284 print(f"Creating Vivado project in {project_path}")
285 self._setup_tcl_sources()
286 self._setup_build_step_hooks()
288 # The pre-create hook might have side effects. E.g. change some register constants.
289 # So we make a deep copy of the module list before the hook is called.
290 # Note that the modules are copied before the pre-build hooks as well,
291 # since we do not know if we might be performing a create-only or
292 # build-only operation. The copy does not take any significant time, so this is not
293 # an issue.
294 self.modules = deepcopy(self.modules)
296 # Send all available arguments that are reasonable to use in pre-create and module getter
297 # functions. Prefer run-time values over the static.
298 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments)
299 all_arguments.update(
300 generics=self.static_generics,
301 part=self.part,
302 )
304 if not self.pre_create(
305 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments
306 ):
307 print("ERROR: Project pre-create hook returned False. Failing the build.")
308 return False
310 create_vivado_project_tcl = self._create_tcl(
311 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments
312 )
313 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl)
315 def pre_create(
316 self,
317 **kwargs: Any, # noqa: ANN401, ARG002
318 ) -> bool:
319 """
320 Override this function in a subclass if you wish to do something useful with it.
321 Will be called from :meth:`.create` right before the call to Vivado.
323 An example use case for this function is when TCL source scripts for the Vivado project
324 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the
325 Vivado system PATH.
327 .. Note::
328 This default method does nothing. Shall be overridden by project that utilize
329 this mechanism.
331 Arguments:
332 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in
333 the ``other_arguments`` argument to :func:`VivadoProject.__init__`.
335 Return:
336 True if everything went well.
337 """
338 return True
340 def _build_tcl( # noqa: PLR0913
341 self,
342 project_path: Path,
343 output_path: Path,
344 num_threads: int,
345 run_index: int,
346 all_generics: dict[str, Any],
347 synth_only: bool,
348 from_impl: bool,
349 impl_explore: bool,
350 ) -> Path:
351 """
352 Make a TCL file that builds a Vivado project
353 """
354 project_file = self.project_file(project_path)
355 if not project_file.exists():
356 raise ValueError(
357 f"Project file does not exist in the specified location: {project_file}"
358 )
360 build_vivado_project_tcl = project_path / "build_vivado_project.tcl"
361 tcl = self.tcl.build(
362 project_file=project_file,
363 output_path=output_path,
364 num_threads=num_threads,
365 run_index=run_index,
366 generics=all_generics,
367 synth_only=synth_only,
368 from_impl=from_impl,
369 analyze_synthesis_timing=self.analyze_synthesis_timing,
370 impl_explore=impl_explore,
371 )
372 create_file(build_vivado_project_tcl, tcl)
374 return build_vivado_project_tcl
376 def pre_build(
377 self,
378 **kwargs: Any, # noqa: ANN401, ARG002
379 ) -> bool:
380 """
381 Override this function in a subclass if you wish to do something useful with it.
382 Will be called from :meth:`.build` right before the call to Vivado.
384 Arguments:
385 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
386 parameters from the user.
388 Return:
389 True if everything went well.
390 """
391 return True
393 def post_build(
394 self,
395 **kwargs: Any, # noqa: ANN401, ARG002
396 ) -> bool:
397 """
398 Override this function in a subclass if you wish to do something useful with it.
399 Will be called from :meth:`.build` right after the call to Vivado.
401 An example use case for this function is to encrypt the bit file, or generate any other
402 material that shall be included in FPGA release artifacts.
404 .. Note::
405 This default method does nothing. Shall be overridden by project that utilize
406 this mechanism.
408 Arguments:
409 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
410 parameters from the user. Will also include ``build_result`` with
411 implemented/synthesized size, which can be used for asserting the expected resource
412 utilization.
414 Return:
415 True if everything went well.
416 """
417 return True
419 def build( # noqa: C901, PLR0912, PLR0913
420 self,
421 project_path: Path,
422 output_path: Path | None = None,
423 run_index: int | None = None,
424 generics: dict[str, Any] | None = None,
425 synth_only: bool = False,
426 from_impl: bool = False,
427 num_threads: int = 12,
428 **pre_and_post_build_parameters: Any, # noqa: ANN401
429 ) -> BuildResult:
430 """
431 Build a Vivado project
433 Arguments:
434 project_path: A path containing a Vivado project.
435 output_path: Results (bit file, ...) will be placed here.
436 run_index: Select Vivado run (synth_X and impl_X) to build with.
437 generics: A dict with generics values (`dict(name: value)`). Use for run-time
438 generics, i.e. values that can change between each build of this project.
440 Compare to the create-time generics argument in :meth:`.__init__`.
442 The generic value types follow the same rules as for :meth:`.__init__`.
443 synth_only: Run synthesis and then stop.
444 from_impl: Run the ``impl`` steps and onward on an existing synthesized design.
445 num_threads: Number of parallel threads to use during run.
446 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga,
447 but will instead be sent to
449 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
450 * :func:`VivadoProject.pre_build`
451 * :func:`VivadoProject.post_build`
453 along with further ``other_arguments`` supplied to :meth:`.__init__`.
455 .. note::
456 This is a "kwargs" style argument. You can pass any number of named arguments.
458 Return:
459 Result object with build information.
460 """
461 synth_only = synth_only or self.is_netlist_build
463 if output_path is None and not synth_only:
464 raise ValueError("Must specify output_path when doing an implementation run")
466 if synth_only:
467 print(f"Synthesizing Vivado project in {project_path}")
468 else:
469 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}")
471 # Combine to all available generics. Prefer run-time values over static.
472 all_generics = copy_and_combine_dicts(self.static_generics, generics)
474 # Run index is optional to specify at build-time
475 run_index = self.default_run_index if run_index is None else run_index
477 # Send all available information to pre- and post build functions. Prefer build-time values
478 # over the static arguments.
479 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters)
480 all_parameters.update(
481 project_path=project_path,
482 output_path=output_path,
483 run_index=run_index,
484 generics=all_generics,
485 synth_only=synth_only,
486 from_impl=from_impl,
487 num_threads=num_threads,
488 )
490 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks)
491 # might have side effects. E.g. change some register constants. So we make a deep copy of
492 # the module list before any of these hooks are called. Note that the modules are copied
493 # before the pre-create hook as well, since we do not know if we might be performing a
494 # create-only or build-only operation. The copy does not take any significant time, so this
495 # is not an issue.
496 self.modules = deepcopy(self.modules)
498 result = BuildResult(self.name)
500 for module in self.modules:
501 if not module.pre_build(project=self, **all_parameters):
502 print(
503 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build."
504 )
505 result.success = False
506 return result
508 # Make sure register packages are up to date
509 module.create_register_synthesis_files()
511 if not self.pre_build(**all_parameters):
512 print("ERROR: Project pre-build hook returned False. Failing the build.")
513 result.success = False
514 return result
516 # We ignore the type of 'output_path' going from 'Path | None' to 'Path'.
517 # It is only used if 'synth_only' is False, and we have an assertion that 'output_path' is
518 # not None in that case above.
520 build_vivado_project_tcl = self._build_tcl(
521 project_path=project_path,
522 output_path=output_path,
523 num_threads=num_threads,
524 run_index=run_index,
525 all_generics=all_generics,
526 synth_only=synth_only,
527 from_impl=from_impl,
528 impl_explore=self.impl_explore,
529 )
531 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl):
532 result.success = False
533 return result
535 result.synthesis_size = self._get_size(project_path, f"synth_{run_index}")
536 if self.report_logic_level_distribution:
537 result.logic_level_distribution = self._get_logic_level_distribution(
538 project_path, f"synth_{run_index}"
539 )
541 if not synth_only:
542 if self.impl_explore:
543 runs_path = project_path / f"{self.name}.runs"
544 for run in runs_path.iterdir():
545 if "impl_explore_" in run.resolve().name:
546 # Check files for existence, since not all runs may have completed
547 bit_file = run / f"{self.top}.bit"
548 bin_file = run / f"{self.top}.bin"
549 if bit_file.exists() or bin_file.exists():
550 impl_folder = run
551 run_name = run.resolve().name
552 break
553 else:
554 run_name = f"impl_{run_index}"
555 impl_folder = project_path / f"{self.name}.runs" / run_name
556 bit_file = impl_folder / f"{self.top}.bit"
557 bin_file = impl_folder / f"{self.top}.bin"
559 shutil.copy2(bit_file, output_path / f"{self.name}.bit")
560 shutil.copy2(bin_file, output_path / f"{self.name}.bin")
561 result.implementation_size = self._get_size(project_path, run_name)
563 # Send the result object, along with everything else, to the post-build function
564 all_parameters.update(build_result=result)
566 if not self.post_build(**all_parameters):
567 print("ERROR: Project post-build hook returned False. Failing the build.")
568 result.success = False
570 return result
572 def open(self, project_path: Path) -> bool:
573 """
574 Open the project in Vivado GUI.
576 Arguments:
577 project_path: A path containing a Vivado project.
579 Return:
580 True if everything went well.
581 """
582 return run_vivado_gui(self._vivado_path, self.project_file(project_path))
584 def _get_size(self, project_path: Path, run: str) -> dict[str, int]:
585 """
586 Reads the hierarchical utilization report and returns the top level size
587 for the specified run.
588 """
589 report_as_string = read_file(
590 project_path / f"{self.name}.runs" / run / "hierarchical_utilization.rpt"
591 )
592 return HierarchicalUtilizationParser.get_size(report_as_string)
594 def _get_logic_level_distribution(self, project_path: Path, run: str) -> str:
595 """
596 Reads the hierarchical utilization report and returns the top level size
597 for the specified run.
598 """
599 report_as_string = read_file(
600 project_path / f"{self.name}.runs" / run / "logical_level_distribution.rpt"
601 )
602 return LogicLevelDistributionParser.get_table(report_as_string)
604 def __str__(self) -> str:
605 result = f"{self.name}\n"
607 if self.defined_at is not None:
608 result += f"Defined at: {self.defined_at.resolve()}\n"
610 result += f"Type: {self.__class__.__name__}\n"
611 result += f"Top level: {self.top}\n"
613 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-"
614 result += f"Generics: {generics}\n"
616 if self.other_arguments:
617 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n"
619 return result
621 @staticmethod
622 def _dict_to_string(data: dict[str, Any]) -> str:
623 return ", ".join([f"{name}={value}" for name, value in data.items()])
626class VivadoNetlistProject(VivadoProject):
627 """
628 Used for handling Vivado build of a module without top level pinning.
629 """
631 def __init__(
632 self,
633 analyze_synthesis_timing: bool = False,
634 build_result_checkers: list[SizeChecker | MaximumLogicLevel] | None = None,
635 **kwargs: Any, # noqa: ANN401
636 ) -> None:
637 """
638 Arguments:
639 analyze_synthesis_timing: Enable analysis of the synthesized design's timing.
640 This will make the build flow open the design, and check for unhandled clock
641 crossings and pulse width violations.
642 Enabling it will add significant build time (can be as much as +40%).
643 Also, in order for clock crossing check to work, the clocks have to be created
644 using a constraint file.
645 build_result_checkers:
646 Checkers that will be executed after a successful build. Is used to automatically
647 check that e.g. resource utilization is not greater than expected.
648 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`.
649 """
650 super().__init__(**kwargs)
652 self.is_netlist_build = True
653 self.analyze_synthesis_timing = analyze_synthesis_timing
654 self.report_logic_level_distribution = True
655 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers
657 def build(
658 self,
659 **kwargs: Any, # noqa: ANN401
660 ) -> BuildResult:
661 """
662 Build the project.
664 Arguments:
665 kwargs: All arguments as accepted by :meth:`.VivadoProject.build`.
666 """
667 result = super().build(**kwargs)
668 result.success = result.success and self._check_size(result)
670 return result
672 def _check_size(self, build_result: BuildResult) -> bool:
673 if not build_result.success:
674 print(f"Can not do post_build check for {self.name} since it did not succeed.")
675 return False
677 success = True
678 for build_result_checker in self.build_result_checkers:
679 checker_result = build_result_checker.check(build_result)
680 success = success and checker_result
682 return success
685class VivadoIpCoreProject(VivadoProject):
686 """
687 A Vivado project that is only used to generate simulation models of IP cores.
688 """
690 ip_cores_only = True
692 def __init__(
693 self,
694 **kwargs: Any, # noqa: ANN401
695 ) -> None:
696 """
697 Arguments:
698 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`.
699 """
700 super().__init__(**kwargs)
702 def build(
703 self,
704 **kwargs: Any, # noqa: ANN401
705 ) -> NoReturn:
706 """
707 Not implemented.
708 """
709 raise NotImplementedError("IP core project can not be built")
712def copy_and_combine_dicts(
713 dict_first: dict[str, Any] | None, dict_second: dict[str, Any] | None
714) -> dict[str, Any]:
715 """
716 Will prefer values in the second dict, in case the same key occurs in both.
717 Will return an empty dictionary if both are ``None``.
718 """
719 if dict_first is None:
720 if dict_second is None:
721 return {}
723 return dict_second.copy()
725 if dict_second is None:
726 return dict_first.copy()
728 result = dict_first.copy()
729 result.update(dict_second)
731 return result