Coverage for tsfpga/vivado/project.py: 84%
175 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-28 04:01 +0000
« prev ^ index » next coverage.py v6.4, created at 2022-05-28 04:01 +0000
1# --------------------------------------------------------------------------------------------------
2# Copyright (c) Lukas Vik. All rights reserved.
3#
4# This file is part of the tsfpga project.
5# https://tsfpga.com
6# https://gitlab.com/tsfpga/tsfpga
7# --------------------------------------------------------------------------------------------------
9from copy import deepcopy
10import shutil
12from tsfpga import TSFPGA_TCL
13from tsfpga.system_utils import create_file, read_file
14from tsfpga.build_step_tcl_hook import BuildStepTclHook
15from .build_result import BuildResult
16from .common import run_vivado_tcl, run_vivado_gui
17from .hierarchical_utilization_parser import HierarchicalUtilizationParser
18from .logic_level_distribution_parser import LogicLevelDistributionParser
19from .tcl import VivadoTcl
22class VivadoProject:
23 """
24 Used for handling a Xilinx Vivado HDL project
25 """
27 # pylint: disable=too-many-arguments,too-many-instance-attributes
28 def __init__(
29 self,
30 name,
31 modules,
32 part,
33 top=None,
34 generics=None,
35 constraints=None,
36 tcl_sources=None,
37 build_step_hooks=None,
38 vivado_path=None,
39 default_run_index=1,
40 defined_at=None,
41 **other_arguments,
42 ):
43 """
44 Class constructor. Performs a shallow copy of the mutable arguments, so that the user
45 can e.g. append items to their list after creating an object.
47 Arguments:
48 name (str): Project name.
49 modules (list(BaseModule)): Modules that shall be included in the project.
50 part (str): Part identification.
51 top (str): Name of top level entity. If left out, the top level name will be
52 inferred from the ``name``.
53 generics: A dict with generics values (`dict(name: value)`). Use this parameter
54 for "static" generics that do not change between multiple builds of this
55 project. These will be set in the project when it is created.
57 Compare to the build-time generic argument in :meth:`build`.
59 The generic value shall be of type
61 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``),
62 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.),
63 * :class:`float` (suitable for VHDL type ``real``),
64 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``,
65 ``unsigned``, etc.), or
66 * :class:`.StringGenericValue` (suitable for VHDL type ``string``).
67 constraints (list(Constraint)): Constraints that will be applied to the project.
68 tcl_sources (list(pathlib.Path)): A list of TCL files. Use for e.g. block design,
69 pinning, settings, etc.
70 build_step_hooks (list(BuildStepTclHook)): Build step hooks that will be applied to the
71 project.
72 vivado_path (pathlib.Path): A path to the Vivado executable. If omitted,
73 the default location from the system PATH will be used.
74 default_run_index (int): Default run index (synth_X and impl_X) that is set in the
75 project. Can also use the argument to :meth:`build() <VivadoProject.build>` to
76 specify at build-time.
77 defined_at (pathlib.Path): Optional path to the file where you defined this
78 project. To get a useful ``build.py --list`` message. Is useful when you have many
79 projects set up.
80 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
81 instead be passed on to
83 * :func:`BaseModule.get_synthesis_files()
84 <tsfpga.module.BaseModule.get_synthesis_files>`
85 * :func:`BaseModule.get_ip_core_files()
86 <tsfpga.module.BaseModule.get_ip_core_files>`
87 * :func:`BaseModule.get_scoped_constraints()
88 <tsfpga.module.BaseModule.get_scoped_constraints>`
89 * :func:`VivadoProject.pre_create`
90 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
91 * :func:`VivadoProject.pre_build`
92 * :func:`VivadoProject.post_build`
94 along with further arguments supplied at build-time to :meth:`.create` and
95 :meth:`.build`.
97 .. note::
98 This is a "kwargs" style argument. You can pass any number of named arguments.
99 """
100 self.name = name
101 self.modules = modules.copy()
102 self.part = part
103 self.static_generics = {} if generics is None else generics.copy()
104 self.constraints = [] if constraints is None else constraints.copy()
105 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy()
106 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy()
107 self._vivado_path = vivado_path
108 self.default_run_index = default_run_index
109 self.defined_at = defined_at
110 self.other_arguments = None if other_arguments is None else other_arguments.copy()
112 # Will be set by child class when applicable
113 self.is_netlist_build = False
114 self.analyze_synthesis_timing = True
115 self.report_logic_level_distribution = False
116 self.ip_cores_only = False
118 self.top = name + "_top" if top is None else top
120 self.tcl = VivadoTcl(name=self.name)
122 def project_file(self, project_path):
123 """
124 Arguments:
125 project_path (pathlib.Path): A path containing a Vivado project.
126 Return:
127 pathlib.Path: The project file of this project, in the given folder
128 """
129 return project_path / (self.name + ".xpr")
131 def _setup_tcl_sources(self):
132 tsfpga_tcl_sources = [
133 TSFPGA_TCL / "vivado_default_run.tcl",
134 TSFPGA_TCL / "vivado_fast_run.tcl",
135 TSFPGA_TCL / "vivado_messages.tcl",
136 ]
138 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga
139 # settings. Conversely, tsfpga should not modify something that the user has set up.
140 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources
142 def _setup_build_step_hooks(self):
143 # Check the implemented timing and resource utilization via TCL build hooks.
144 # This is different than for synthesis, where it is embedded in the build script.
145 # This is due to Vivado limitations related to post-synthesis hooks.
146 # Specifically, the report_utilization figures do not include IP cores when it is run in
147 # a post-synthesis hook.
148 self.build_step_hooks.append(
149 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
150 )
151 self.build_step_hooks.append(
152 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE")
153 )
155 if not self.analyze_synthesis_timing:
156 # In this special case however, the synthesized design is never opened, and
157 # report_utilization is not run by the build_vivado_project.tcl.
158 # So in order to get a utilization report anyway we add it as a hook.
159 # This mode is exclusively used by netlist builds, which very rarely include IP cores,
160 # so it is acceptable that the utilization report might be erroneous with regards to
161 # IP cores.
162 self.build_step_hooks.append(
163 BuildStepTclHook(
164 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST"
165 )
166 )
168 if self.report_logic_level_distribution:
169 # Used by netlist builds
170 self.build_step_hooks.append(
171 BuildStepTclHook(
172 TSFPGA_TCL / "report_logic_level_distribution.tcl",
173 "STEPS.SYNTH_DESIGN.TCL.POST",
174 )
175 )
177 def _create_tcl(self, project_path, ip_cache_path, all_arguments):
178 """
179 Make a TCL file that creates a Vivado project
180 """
181 if project_path.exists():
182 raise ValueError(f"Folder already exists: {project_path}")
183 project_path.mkdir(parents=True)
185 create_vivado_project_tcl = project_path / "create_vivado_project.tcl"
186 tcl = self.tcl.create(
187 project_folder=project_path,
188 modules=self.modules,
189 part=self.part,
190 top=self.top,
191 run_index=self.default_run_index,
192 generics=self.static_generics,
193 constraints=self.constraints,
194 tcl_sources=self.tcl_sources,
195 build_step_hooks=self.build_step_hooks,
196 ip_cache_path=ip_cache_path,
197 disable_io_buffers=self.is_netlist_build,
198 ip_cores_only=self.ip_cores_only,
199 other_arguments=all_arguments,
200 )
201 create_file(create_vivado_project_tcl, tcl)
203 return create_vivado_project_tcl
205 def create(self, project_path, ip_cache_path=None, **other_arguments):
206 """
207 Create a Vivado project
209 Arguments:
210 project_path (pathlib.Path): Path where the project shall be placed.
211 ip_cache_path (pathlib.Path): Path to a folder where the Vivado IP cache can be
212 placed. If omitted, the Vivado IP cache mechanism will not be enabled.
213 other_arguments: Optional further arguments. Will not be used by tsfpga, but will
214 instead be sent to
216 * :func:`BaseModule.get_synthesis_files()
217 <tsfpga.module.BaseModule.get_synthesis_files>`
218 * :func:`BaseModule.get_ip_core_files()
219 <tsfpga.module.BaseModule.get_ip_core_files>`
220 * :func:`BaseModule.get_scoped_constraints()
221 <tsfpga.module.BaseModule.get_scoped_constraints>`
222 * :func:`VivadoProject.pre_create`
224 along with further ``other_arguments`` supplied to :meth:`.__init__`.
226 .. note::
227 This is a "kwargs" style argument. You can pass any number of named arguments.
228 Returns:
229 bool: True if everything went well.
230 """
231 print(f"Creating Vivado project in {project_path}")
232 self._setup_tcl_sources()
233 self._setup_build_step_hooks()
235 # The pre-create hook might have side effects. E.g. change some register constants.
236 # So we make a deep copy of the module list before the hook is called.
237 # Note that the modules are copied before the pre-build hooks as well,
238 # since we do not know if we might be performing a create-only or
239 # build-only operation. The copy does not take any significant time, so this is not
240 # an issue.
241 self.modules = deepcopy(self.modules)
243 # Send all available arguments that are reasonable to use in pre-create and module getter
244 # functions. Prefer run-time values over the static.
245 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments)
246 all_arguments.update(
247 generics=self.static_generics,
248 part=self.part,
249 )
251 if not self.pre_create(
252 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments
253 ):
254 print("ERROR: Project pre-create hook returned False. Failing the build.")
255 return False
257 create_vivado_project_tcl = self._create_tcl(
258 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments
259 )
260 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl)
262 def pre_create(self, **kwargs): # pylint: disable=no-self-use, unused-argument
263 """
264 Override this function in a child class if you wish to do something useful with it.
265 Will be called from :meth:`.create` right before the call to Vivado.
267 An example use case for this function is when TCL source scripts for the Vivado project
268 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the
269 Vivado system PATH.
271 .. Note::
272 This default method does nothing. Shall be overridden by project that utilize
273 this mechanism.
275 Arguments:
276 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in
277 the ``other_arguments`` argument to :func:`VivadoProject.__init__`.
279 Return:
280 bool: True if everything went well.
281 """
282 return True
284 def _build_tcl(
285 self,
286 project_path,
287 output_path,
288 num_threads,
289 run_index,
290 all_generics,
291 synth_only,
292 from_impl,
293 ):
294 """
295 Make a TCL file that builds a Vivado project
296 """
297 project_file = self.project_file(project_path)
298 if not project_file.exists():
299 raise ValueError(
300 f"Project file does not exist in the specified location: {project_file}"
301 )
303 build_vivado_project_tcl = project_path / "build_vivado_project.tcl"
304 tcl = self.tcl.build(
305 project_file=project_file,
306 output_path=output_path,
307 num_threads=num_threads,
308 run_index=run_index,
309 generics=all_generics,
310 synth_only=synth_only,
311 from_impl=from_impl,
312 analyze_synthesis_timing=self.analyze_synthesis_timing,
313 )
314 create_file(build_vivado_project_tcl, tcl)
316 return build_vivado_project_tcl
318 def pre_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument
319 """
320 Override this function in a child class if you wish to do something useful with it.
321 Will be called from :meth:`.build` right before the call to Vivado.
323 Arguments:
324 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
325 parameters from the user.
327 Return:
328 bool: True if everything went well.
329 """
330 return True
332 def post_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument
333 """
334 Override this function in a child class if you wish to do something useful with it.
335 Will be called from :meth:`.build` right after the call to Vivado.
337 An example use case for this function is to encrypt the bit file, or generate any other
338 material that shall be included in FPGA release artifacts.
340 .. Note::
341 This default method does nothing. Shall be overridden by project that utilize
342 this mechanism.
344 Arguments:
345 kwargs: Will have all the :meth:`.build` parameters in it. Including additional
346 parameters from the user. Will also include ``build_result`` with
347 implemented/synthesized size, which can be used for asserting the expected resource
348 utilization.
350 Return:
351 bool: True if everything went well.
352 """
353 return True
355 def build(
356 self,
357 project_path,
358 output_path=None,
359 run_index=None,
360 generics=None,
361 synth_only=False,
362 from_impl=False,
363 num_threads=12,
364 **pre_and_post_build_parameters,
365 ):
366 """
367 Build a Vivado project
369 Arguments:
370 project_path (pathlib.Path): A path containing a Vivado project.
371 output_path (pathlib.Path): Results (bit file, ...) will be placed here.
372 run_index (int): Select Vivado run (synth_X and impl_X) to build with.
373 generics: A dict with generics values (`dict(name: value)`). Use for run-time
374 generics, i.e. values that can change between each build of this project.
376 Compare to the create-time generics argument in :meth:`.__init__`.
378 The generic value types follow the same rules as for :meth:`.__init__`.
379 synth_only (bool): Run synthesis and then stop.
380 from_impl (bool): Run the ``impl`` steps and onward on an existing synthesized design.
381 num_threads (int): Number of parallel threads to use during run.
382 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga,
383 but will instead be sent to
385 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>`
386 * :func:`VivadoProject.pre_build`
387 * :func:`VivadoProject.post_build`
389 along with further ``other_arguments`` supplied to :meth:`.__init__`.
391 .. note::
392 This is a "kwargs" style argument. You can pass any number of named arguments.
394 Return:
395 :class:`.build_result.BuildResult`: Result object with build information.
396 """
397 synth_only = synth_only or self.is_netlist_build
399 if output_path is None and not synth_only:
400 raise ValueError("Must specify output_path when doing an implementation run")
402 if synth_only:
403 print(f"Synthesizing Vivado project in {project_path}")
404 else:
405 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}")
407 # Combine to all available generics. Prefer run-time values over static.
408 all_generics = copy_and_combine_dicts(self.static_generics, generics)
410 # Run index is optional to specify at build-time
411 run_index = self.default_run_index if run_index is None else run_index
413 # Send all available information to pre- and post build functions. Prefer build-time values
414 # over the static arguments.
415 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters)
416 all_parameters.update(
417 project_path=project_path,
418 output_path=output_path,
419 run_index=run_index,
420 generics=all_generics,
421 synth_only=synth_only,
422 from_impl=from_impl,
423 num_threads=num_threads,
424 )
426 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks)
427 # might have side effects. E.g. change some register constants. So we make a deep copy of
428 # the module list before any of these hooks are called. Note that the modules are copied
429 # before the pre-create hook as well, since we do not know if we might be performing a
430 # create-only or build-only operation. The copy does not take any significant time, so this
431 # is not an issue.
432 self.modules = deepcopy(self.modules)
434 result = BuildResult(self.name)
436 for module in self.modules:
437 if not module.pre_build(project=self, **all_parameters):
438 print(
439 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build."
440 )
441 result.success = False
442 return result
444 # Make sure register packages are up to date
445 module.create_regs_vhdl_package()
447 if not self.pre_build(**all_parameters):
448 print("ERROR: Project pre-build hook returned False. Failing the build.")
449 result.success = False
450 return result
452 build_vivado_project_tcl = self._build_tcl(
453 project_path=project_path,
454 output_path=output_path,
455 num_threads=num_threads,
456 run_index=run_index,
457 all_generics=all_generics,
458 synth_only=synth_only,
459 from_impl=from_impl,
460 )
462 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl):
463 result.success = False
464 return result
466 result.synthesis_size = self._get_size(project_path, f"synth_{run_index}")
467 if self.report_logic_level_distribution:
468 result.logic_level_distribution = self._get_logic_level_distribution(
469 project_path, f"synth_{run_index}"
470 )
472 if not synth_only:
473 impl_folder = project_path / f"{self.name}.runs" / f"impl_{run_index}"
474 shutil.copy2(impl_folder / f"{self.top}.bit", output_path / f"{self.name}.bit")
475 shutil.copy2(impl_folder / f"{self.top}.bin", output_path / f"{self.name}.bin")
476 result.implementation_size = self._get_size(project_path, f"impl_{run_index}")
478 # Send the result object, along with everything else, to the post-build function
479 all_parameters.update(build_result=result)
481 if not self.post_build(**all_parameters):
482 print("ERROR: Project post-build hook returned False. Failing the build.")
483 result.success = False
485 return result
487 def open(self, project_path):
488 """
489 Open the project in Vivado GUI.
491 Arguments:
492 project_path (pathlib.Path): A path containing a Vivado project.
494 Returns:
495 bool: True if everything went well.
496 """
497 return run_vivado_gui(self._vivado_path, self.project_file(project_path))
499 def _get_size(self, project_path, run):
500 """
501 Reads the hierarchical utilization report and returns the top level size
502 for the specified run.
503 """
504 report_as_string = read_file(
505 project_path / f"{self.name}.runs" / run / "hierarchical_utilization.rpt"
506 )
507 return HierarchicalUtilizationParser.get_size(report_as_string)
509 def _get_logic_level_distribution(self, project_path, run):
510 """
511 Reads the hierarchical utilization report and returns the top level size
512 for the specified run.
513 """
514 report_as_string = read_file(
515 project_path / f"{self.name}.runs" / run / "logical_level_distribution.rpt"
516 )
517 return LogicLevelDistributionParser.get_table(report_as_string)
519 def __str__(self):
520 result = f"{self.name}\n"
522 if self.defined_at is not None:
523 result += f"Defined at: {self.defined_at.resolve()}\n"
525 result += f"Type: {self.__class__.__name__}\n"
526 result += f"Top level: {self.top}\n"
528 if self.static_generics:
529 generics = self._dict_to_string(self.static_generics)
530 else:
531 generics = "-"
532 result += f"Generics: {generics}\n"
534 if self.other_arguments:
535 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n"
537 return result
539 @staticmethod
540 def _dict_to_string(data):
541 return ", ".join([f"{name}={value}" for name, value in data.items()])
544class VivadoNetlistProject(VivadoProject):
545 """
546 Used for handling Vivado build of a module without top level pinning.
547 """
549 def __init__(self, analyze_synthesis_timing=False, build_result_checkers=None, **kwargs):
550 """
551 Arguments:
552 analyze_synthesis_timing (bool): Enable analysis of the synthesized design's timing.
553 This will make the build flow open the design, and check for unhandled clock
554 crossings and pulse width violations.
555 Enabling it will add significant build time (can be as much as +40%).
556 Also, in order for clock crossing check to work, the clocks have to be created
557 using a constraint file.
558 build_result_checkers (list(SizeChecker, MaximumLogicLevel)):
559 Checkers that will be executed after a successful build. Is used to automatically
560 check that e.g. resource utilization is not greater than expected.
561 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`.
562 """
563 super().__init__(**kwargs)
565 self.is_netlist_build = True
566 self.analyze_synthesis_timing = analyze_synthesis_timing
567 self.report_logic_level_distribution = True
568 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers
570 def build(self, **kwargs): # pylint: disable=arguments-differ
571 """
572 Build the project.
574 Arguments:
575 kwargs: All arguments as accepted by :meth:`.VivadoProject.build`.
576 """
577 result = super().build(**kwargs)
578 result.success = result.success and self._check_size(result)
580 return result
582 def _check_size(self, build_result):
583 if not build_result.success:
584 print(f"Can not do post_build check for {self.name} since it did not succeed.")
585 return False
587 success = True
588 for build_result_checker in self.build_result_checkers:
589 checker_result = build_result_checker.check(build_result)
590 success = success and checker_result
592 return success
595class VivadoIpCoreProject(VivadoProject):
596 """
597 A Vivado project that is only used to generate simulation models of IP cores.
598 """
600 def __init__(self, **kwargs):
601 """
602 Arguments:
603 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`.
604 """
605 super().__init__(**kwargs)
607 self.ip_cores_only = True
609 def build(self, **kwargs): # pylint: disable=arguments-differ
610 """
611 Not implemented.
612 """
613 raise NotImplementedError("IP core project can not be built")
616def copy_and_combine_dicts(dict_first, dict_second):
617 """
618 Will prefer values in the second dict, in case the same key occurs in both.
619 Will return ``None`` if both are ``None``.
620 """
621 if dict_first is None and dict_second is None:
622 return None
624 if dict_first is None:
625 return dict_second.copy()
627 if dict_second is None:
628 return dict_first.copy()
630 result = dict_first.copy()
631 result.update(dict_second)
632 return result