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