Coverage for tsfpga/module.py: 99%
184 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 11:31 +0000
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-07 11:31 +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# --------------------------------------------------------------------------------------------------
9# Standard libraries
10import random
11from pathlib import Path
12from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Union
14# Third party libraries
15from hdl_registers.generator.vhdl.axi_lite.wrapper import VhdlAxiLiteWrapperGenerator
16from hdl_registers.generator.vhdl.record_package import VhdlRecordPackageGenerator
17from hdl_registers.generator.vhdl.register_package import VhdlRegisterPackageGenerator
18from hdl_registers.generator.vhdl.simulation.check_package import (
19 VhdlSimulationCheckPackageGenerator,
20)
21from hdl_registers.generator.vhdl.simulation.read_write_package import (
22 VhdlSimulationReadWritePackageGenerator,
23)
24from hdl_registers.generator.vhdl.simulation.wait_until_package import (
25 VhdlSimulationWaitUntilPackageGenerator,
26)
27from hdl_registers.parser.toml import from_toml
28from hdl_registers.register import Register
29from hdl_registers.register_list import RegisterList
31# First party libraries
32from tsfpga.constraint import Constraint
33from tsfpga.hdl_file import HdlFile
34from tsfpga.ip_core_file import IpCoreFile
35from tsfpga.module_list import ModuleList
36from tsfpga.system_utils import load_python_module
38if TYPE_CHECKING:
39 # Local folder libraries
40 from .vivado.project import VivadoProject
43class BaseModule:
44 """
45 Base class for handling a HDL module with RTL code, constraints, etc.
47 Files are gathered from a lot of different sub-folders, to accommodate for projects having
48 different catalog structure.
49 """
51 # Set to False if you do not want to create these register artifacts for this module.
52 # Can be done in a child class or on an object instance.
53 # Note that artifacts will only be created if the module actually has any registers.
54 create_register_package = True
55 create_record_package = True
56 create_axi_lite_wrapper = True
57 create_simulation_read_write_package = True
58 create_simulation_check_package = True
59 create_simulation_wait_until_package = True
61 def __init__(
62 self, path: Path, library_name: str, default_registers: Optional[list[Register]] = None
63 ):
64 """
65 Arguments:
66 path: Path to the module folder.
67 library_name: VHDL library name.
68 default_registers: Default registers.
69 """
70 self.path = path.resolve()
71 self.name = path.name
72 self.library_name = library_name
74 self._default_registers = default_registers
75 self._registers: Optional[RegisterList] = None
77 @staticmethod
78 def _get_file_list(
79 folders: list[Path],
80 file_endings: Union[str, tuple[str, ...]],
81 files_include: Optional[set[Path]] = None,
82 files_avoid: Optional[set[Path]] = None,
83 ) -> list[Path]:
84 """
85 Returns a list of files given a list of folders.
87 Arguments:
88 folders: The folders to search.
89 file_endings: File endings to include.
90 files_include: Optionally filter to only include these files.
91 files_avoid: Optionally filter to discard these files.
92 """
93 files = []
94 for folder in folders:
95 for file in folder.glob("*"):
96 if not file.is_file():
97 continue
99 if not file.name.lower().endswith(file_endings):
100 continue
102 if files_include is not None and file not in files_include:
103 continue
105 if files_avoid is not None and file in files_avoid:
106 continue
108 files.append(file)
110 return files
112 def _get_hdl_file_list( # pylint: disable=too-many-arguments
113 self,
114 folders: list[Path],
115 files_include: Optional[set[Path]] = None,
116 files_avoid: Optional[set[Path]] = None,
117 include_vhdl_files: bool = True,
118 include_verilog_files: bool = True,
119 include_systemverilog_files: bool = True,
120 ) -> list[HdlFile]:
121 """
122 Return a list of HDL file objects.
123 """
124 file_endings: tuple[str, ...] = tuple()
125 if include_vhdl_files:
126 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VHDL]
127 if include_verilog_files:
128 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VERILOG_SOURCE]
129 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.VERILOG_HEADER]
130 if include_systemverilog_files:
131 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.SYSTEMVERILOG_SOURCE]
132 file_endings += HdlFile.file_endings_mapping[HdlFile.Type.SYSTEMVERILOG_HEADER]
134 return [
135 HdlFile(path=file_path)
136 for file_path in self._get_file_list(
137 folders=folders,
138 file_endings=file_endings,
139 files_include=files_include,
140 files_avoid=files_avoid,
141 )
142 ]
144 @property
145 def registers(self) -> Optional[RegisterList]:
146 """
147 Get the registers for this module.
148 Will be ``None`` if the module doesn't have any registers.
149 I.e. if no TOML file exists and no hook creates registers.
150 """
151 if self._registers:
152 # Only create object from TOML once.
153 return self._registers
155 toml_file = self.path / f"regs_{self.name}.toml"
156 if toml_file.exists():
157 self._registers = from_toml(
158 name=self.name, toml_file=toml_file, default_registers=self._default_registers
159 )
161 self.registers_hook()
162 return self._registers
164 def registers_hook(self) -> None:
165 """
166 This function will be called directly after creating this module's registers from
167 the TOML definition file.
168 If the TOML file does not exist this hook will still be called, but the module's registers
169 will be ``None``.
171 This is a good place if you want to add or modify some registers from Python.
172 Override this method and implement the desired behavior in a subclass.
174 .. Note::
175 This default method does nothing.
176 Shall be overridden by modules that utilize this mechanism.
177 """
179 def create_register_synthesis_files(self) -> None:
180 """
181 Create the register artifacts that are needed for synthesis.
182 If this module does not have registers, this method does nothing.
183 """
184 if self.registers is not None:
185 # Delete any old file that might exist so we don't have multiple and
186 # outdated definitions.
187 # This package location was used before the separate register folders were introduced,
188 # back when we only created one register artifact.
189 old_regs_pkg = self.path / f"{self.name}_regs_pkg.vhd"
190 if old_regs_pkg.exists():
191 old_regs_pkg.unlink()
193 if self.create_register_package:
194 VhdlRegisterPackageGenerator(
195 register_list=self.registers, output_folder=self.register_synthesis_folder
196 ).create_if_needed()
198 if self.create_record_package:
199 VhdlRecordPackageGenerator(
200 register_list=self.registers, output_folder=self.register_synthesis_folder
201 ).create_if_needed()
203 if self.create_axi_lite_wrapper:
204 VhdlAxiLiteWrapperGenerator(
205 register_list=self.registers, output_folder=self.register_synthesis_folder
206 ).create_if_needed()
208 def create_register_simulation_files(self) -> None:
209 """
210 Create the register artifacts that are needed for simulation.
211 Does not create the implementation files, which are also technically needed for simulation.
212 So a call to :meth:`.create_register_synthesis_files` must also be done.
214 If this module does not have registers, this method does nothing.
215 """
216 if self.registers is not None:
217 if self.create_simulation_read_write_package:
218 VhdlSimulationReadWritePackageGenerator(
219 register_list=self.registers, output_folder=self.register_simulation_folder
220 ).create_if_needed()
222 if self.create_simulation_check_package:
223 VhdlSimulationCheckPackageGenerator(
224 register_list=self.registers, output_folder=self.register_simulation_folder
225 ).create_if_needed()
227 if self.create_simulation_wait_until_package:
228 VhdlSimulationWaitUntilPackageGenerator(
229 register_list=self.registers, output_folder=self.register_simulation_folder
230 ).create_if_needed()
232 @property
233 def synthesis_folders(self) -> list[Path]:
234 """
235 Synthesis/implementation source code files will be gathered from these folders.
236 """
237 return [
238 self.path,
239 self.path / "src",
240 self.path / "rtl",
241 self.path / "hdl" / "rtl",
242 self.path / "hdl" / "package",
243 self.register_synthesis_folder,
244 ]
246 @property
247 def register_synthesis_folder(self) -> Path:
248 """
249 Generated register artifacts that are needed for synthesis/implementation will be
250 placed in this folder.
251 """
252 return self.path / "regs_src"
254 @property
255 def sim_folders(self) -> list[Path]:
256 """
257 Files with simulation models (the ``sim`` folder) will be gathered from these folders.
258 """
259 return [
260 self.path / "sim",
261 self.register_simulation_folder,
262 ]
264 @property
265 def register_simulation_folder(self) -> Path:
266 """
267 Generated register artifacts that are needed for simulation will be placed in this folder.
268 """
269 return self.path / "regs_sim"
271 @property
272 def test_folders(self) -> list[Path]:
273 """
274 Testbench files will be gathered from these folders.
275 """
276 return [
277 self.path / "test",
278 self.path / "rtl" / "tb",
279 ]
281 def get_synthesis_files( # pylint: disable=unused-argument
282 self,
283 files_include: Optional[set[Path]] = None,
284 files_avoid: Optional[set[Path]] = None,
285 include_vhdl_files: bool = True,
286 include_verilog_files: bool = True,
287 include_systemverilog_files: bool = True,
288 **kwargs: Any,
289 ) -> list[HdlFile]:
290 """
291 Get a list of files that shall be included in a synthesis project.
293 The ``files_include`` and ``files_avoid`` arguments can be used to filter what files are
294 included.
295 This can be useful in many situations, e.g. when encrypted files of files that include an
296 IP core shall be avoided.
297 It is recommended to overload this function in a subclass in your ``module_*.py``,
298 and call this super method with the arguments supplied.
300 Arguments:
301 files_include: Optionally filter to only include these files.
302 files_avoid: Optionally filter to discard these files.
303 include_vhdl_files: Optionally disable inclusion of files with VHDL
304 file endings.
305 include_verilog_files: Optionally disable inclusion of files with Verilog
306 file endings.
307 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog
308 file endings.
309 kwargs: Further parameters that can be sent by build flow to control what
310 files are included.
312 Return:
313 Files that should be included in a synthesis project.
314 """
315 self.create_register_synthesis_files()
317 return self._get_hdl_file_list(
318 folders=self.synthesis_folders,
319 files_include=files_include,
320 files_avoid=files_avoid,
321 include_vhdl_files=include_vhdl_files,
322 include_verilog_files=include_verilog_files,
323 include_systemverilog_files=include_systemverilog_files,
324 )
326 def get_simulation_files( # pylint: disable=too-many-arguments
327 self,
328 include_tests: bool = True,
329 files_include: Optional[set[Path]] = None,
330 files_avoid: Optional[set[Path]] = None,
331 include_vhdl_files: bool = True,
332 include_verilog_files: bool = True,
333 include_systemverilog_files: bool = True,
334 **kwargs: Any,
335 ) -> list[HdlFile]:
336 """
337 See :meth:`.get_synthesis_files` for instructions on how to use ``files_include``
338 and ``files_avoid``.
340 Arguments:
341 include_tests: When ``False``, the ``test`` files are not included
342 (the ``sim`` files are always included).
343 files_include: Optionally filter to only include these files.
344 files_avoid: Optionally filter to discard these files.
345 include_vhdl_files: Optionally disable inclusion of files with VHDL
346 file endings.
347 include_verilog_files: Optionally disable inclusion of files with Verilog
348 file endings.
349 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog
350 file endings.
351 kwargs: Further parameters that can be sent by simulation flow to control what
352 files are included.
354 Return:
355 Files that should be included in a simulation project.
356 """
357 # Shallow copy the list since we might append to it.
358 sim_and_test_folders = self.sim_folders.copy()
360 if include_tests:
361 sim_and_test_folders += self.test_folders
363 self.create_register_simulation_files()
365 test_files = self._get_hdl_file_list(
366 folders=sim_and_test_folders,
367 files_include=files_include,
368 files_avoid=files_avoid,
369 include_vhdl_files=include_vhdl_files,
370 include_verilog_files=include_verilog_files,
371 include_systemverilog_files=include_systemverilog_files,
372 )
374 synthesis_files = self.get_synthesis_files(
375 files_include=files_include,
376 files_avoid=files_avoid,
377 include_vhdl_files=include_vhdl_files,
378 include_verilog_files=include_verilog_files,
379 include_systemverilog_files=include_systemverilog_files,
380 **kwargs,
381 )
383 return synthesis_files + test_files
385 def get_documentation_files( # pylint: disable=unused-argument
386 self,
387 files_include: Optional[set[Path]] = None,
388 files_avoid: Optional[set[Path]] = None,
389 include_vhdl_files: bool = True,
390 include_verilog_files: bool = True,
391 include_systemverilog_files: bool = True,
392 **kwargs: Any,
393 ) -> list[HdlFile]:
394 """
395 Get a list of files that shall be included in a documentation build.
397 It will return all files from the module except testbenches and any generated
398 register package.
399 Overwrite in a subclass if you want to change this behavior.
401 Arguments:
402 files_include: Optionally filter to only include these files.
403 files_avoid: Optionally filter to discard these files.
404 include_vhdl_files: Optionally disable inclusion of files with VHDL
405 file endings.
406 include_verilog_files: Optionally disable inclusion of files with Verilog
407 file endings.
408 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog
409 file endings.
411 Return:
412 Files that should be included in documentation.
413 """
414 # Do not include generated register code in the documentation.
415 files_to_avoid = set(
416 self._get_file_list(
417 folders=[self.register_synthesis_folder, self.register_simulation_folder],
418 file_endings="vhd",
419 )
420 )
421 if files_avoid:
422 files_to_avoid |= files_avoid
424 return self._get_hdl_file_list(
425 folders=self.synthesis_folders + self.sim_folders,
426 files_include=files_include,
427 files_avoid=files_to_avoid,
428 include_vhdl_files=include_vhdl_files,
429 include_verilog_files=include_verilog_files,
430 include_systemverilog_files=include_systemverilog_files,
431 )
433 # pylint: disable=unused-argument
434 def get_ip_core_files(
435 self,
436 files_include: Optional[set[Path]] = None,
437 files_avoid: Optional[set[Path]] = None,
438 **kwargs: Any,
439 ) -> list[IpCoreFile]:
440 """
441 Get IP cores for this module.
443 Note that the :class:`.ip_core_file.IpCoreFile` class accepts a ``variables`` argument that
444 can be used to parameterize IP core creation. By overloading this method in a subclass
445 you can pass on ``kwargs`` arguments from the build/simulation flow to
446 :class:`.ip_core_file.IpCoreFile` creation to achieve this parameterization.
448 Arguments:
449 files_include: Optionally filter to only include these files.
450 files_avoid: Optionally filter to discard these files.
451 kwargs: Further parameters that can be sent by build/simulation flow to control what
452 IP cores are included and what their variables are.
454 Return:
455 The IP cores for this module.
456 """
457 folders = [
458 self.path / "ip_cores",
459 ]
460 file_endings = "tcl"
461 return [
462 IpCoreFile(ip_core_file)
463 for ip_core_file in self._get_file_list(
464 folders=folders,
465 file_endings=file_endings,
466 files_include=files_include,
467 files_avoid=files_avoid,
468 )
469 ]
471 # pylint: disable=unused-argument
472 def get_scoped_constraints(
473 self,
474 files_include: Optional[set[Path]] = None,
475 files_avoid: Optional[set[Path]] = None,
476 **kwargs: Any,
477 ) -> list[Constraint]:
478 """
479 Constraints that shall be applied to a certain entity within this module.
481 Arguments:
482 files_include: Optionally filter to only include these files.
483 files_avoid: Optionally filter to discard these files.
484 kwargs: Further parameters that can be sent by build/simulation flow to control what
485 constraints are included.
487 Return:
488 The constraints.
489 """
490 folders = [
491 self.path / "scoped_constraints",
492 self.path / "entity_constraints",
493 self.path / "hdl" / "constraints",
494 ]
495 file_endings = ("tcl", "xdc")
496 constraint_files = self._get_file_list(
497 folders=folders,
498 file_endings=file_endings,
499 files_include=files_include,
500 files_avoid=files_avoid,
501 )
503 constraints = []
504 if constraint_files:
505 synthesis_files = self.get_synthesis_files()
506 for constraint_file in constraint_files:
507 # Scoped constraints often depend on clocks having been created by another
508 # constraint file before they can work. Set processing order to "late" to make
509 # this more probable.
510 constraint = Constraint(
511 constraint_file, scoped_constraint=True, processing_order="late"
512 )
513 constraint.validate_scoped_entity(synthesis_files)
514 constraints.append(constraint)
516 return constraints
518 def setup_vunit(self, vunit_proj: Any, **kwargs: Any) -> None:
519 """
520 Setup local configuration of this module's test benches.
522 .. Note::
523 This default method does nothing. Should be overridden by modules that have
524 any test benches that operate via generics.
526 Arguments:
527 vunit_proj: The VUnit project that is used to run simulation.
528 kwargs: Use this to pass an arbitrary list of arguments from your ``simulate.py``
529 to the module where you set up your tests. This could be, e.g., data dimensions,
530 location of test files, etc.
531 """
533 def pre_build(
534 self, project: "VivadoProject", **kwargs: Any
535 ) -> bool: # pylint: disable=unused-argument
536 """
537 This method hook will be called before an FPGA build is run. A typical use case for this
538 mechanism is to set a register constant or default value based on the generics that
539 are passed to the project. Could also be used to, e.g., generate BRAM init files
540 based on project information, etc.
542 .. Note::
543 This default method does nothing. Should be overridden by modules that
544 utilize this mechanism.
546 Arguments:
547 project: The project that is being built.
548 kwargs: All other parameters to the build flow. Includes arguments to
549 :meth:`.VivadoProject.build` method as well as other arguments set in
550 :meth:`.VivadoProject.__init__`.
552 Return:
553 True if everything went well.
554 """
555 return True
557 def get_build_projects(self) -> list["VivadoProject"]:
558 """
559 Get FPGA build projects defined by this module.
561 .. Note::
562 This default method does nothing. Should be overridden by modules that set up
563 build projects.
565 Return:
566 FPGA build projects.
567 """
568 return []
570 @staticmethod
571 def test_case_name(
572 name: Optional[str] = None, generics: Optional[dict[str, Any]] = None
573 ) -> str:
574 """
575 Construct a string suitable for naming test cases.
577 Arguments:
578 name: Optional base name.
579 generics: Dictionary of values that will be included in the name.
581 Return:
582 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``.
583 """
584 if name:
585 test_case_name = name
586 else:
587 test_case_name = ""
589 if generics:
590 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()])
592 if test_case_name:
593 test_case_name = f"{name}.{generics_string}"
594 else:
595 test_case_name = generics_string
597 return test_case_name
599 def add_vunit_config( # pylint: disable=too-many-arguments
600 self,
601 test: Any,
602 name: Optional[str] = None,
603 generics: Optional[dict[str, Any]] = None,
604 set_random_seed: Optional[Union[bool, int]] = False,
605 pre_config: Optional[Callable[..., bool]] = None,
606 post_check: Optional[Callable[..., bool]] = None,
607 ) -> None:
608 """
609 Add config for VUnit test case.
610 Wrapper that sets a suitable name and can set a random seed generic.
612 Arguments:
613 test: VUnit test object. Can be testbench or test case.
614 name: Optional designated name for this config. Will be used to form the name of
615 the config together with the ``generics`` value.
616 generics: Generic values that will be applied to the testbench entity. The values
617 will also be used to form the name of the config.
618 set_random_seed: Controls setting of the ``seed`` generic:
620 * When this argument is not assigned, or assigned ``False``, the generic will not
621 be set.
622 * When set to boolean ``True``, a random natural (non-negative integer)
623 generic value will be set.
624 * When set to an integer value, that value will be set for the generic.
625 This is useful to get a static test case name for waveform inspection.
627 If the generic is to be set it must exist in the testbench entity, and should have
628 VHDL type ``natural``.
629 pre_config: Function to be run before the test.
630 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details.
631 post_check: Function to be run after the test.
632 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details.
633 """
634 generics = {} if generics is None else generics
636 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int)
637 # returns True if it is an integer or a bool.
638 if isinstance(set_random_seed, bool):
639 if set_random_seed:
640 # Use the maximum range for a natural in VHDL-2008
641 generics["seed"] = random.randint(0, 2**31 - 1)
643 elif isinstance(set_random_seed, int):
644 generics["seed"] = set_random_seed
646 name = self.test_case_name(name=name, generics=generics)
647 # VUnit does not allow an empty name, which can happen if both 'name' and 'generics' to
648 # this method are None, but the user sets for example a 'pre_config'.
649 # Avoid this error mode by setting a default name when it happens.
650 name = "test" if name == "" else name
652 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check)
654 def __str__(self) -> str:
655 return f"{self.name}:{self.path}"
658def get_modules(
659 modules_folders: list[Path],
660 names_include: Optional[set[str]] = None,
661 names_avoid: Optional[set[str]] = None,
662 library_name_has_lib_suffix: bool = False,
663 default_registers: Optional[list[Register]] = None,
664) -> ModuleList:
665 """
666 Get a list of Module objects based on the source code folders.
668 Arguments:
669 modules_folders: A list of paths where your modules are located.
670 names_include: If specified, only modules with these names will be included.
671 names_avoid: If specified, modules with these names will be discarded.
672 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``,
673 otherwise it is just ``<module name>``.
674 default_registers: Default registers.
676 Return:
677 List of module objects (:class:`BaseModule` or subclasses thereof)
678 created from the specified folders.
679 """
680 modules = ModuleList()
682 for module_folder in _iterate_module_folders(modules_folders):
683 module_name = module_folder.name
685 if names_include is not None and module_name not in names_include:
686 continue
688 if names_avoid is not None and module_name in names_avoid:
689 continue
691 modules.append(
692 _get_module_object(
693 path=module_folder,
694 name=module_name,
695 library_name_has_lib_suffix=library_name_has_lib_suffix,
696 default_registers=default_registers,
697 )
698 )
700 return modules
703def _iterate_module_folders(modules_folders: list[Path]) -> Iterable[Path]:
704 for modules_folder in modules_folders:
705 for module_folder in modules_folder.glob("*"):
706 if module_folder.is_dir():
707 yield module_folder
710def _get_module_object(
711 path: Path,
712 name: str,
713 library_name_has_lib_suffix: bool,
714 default_registers: Optional[list["Register"]],
715) -> BaseModule:
716 module_file = path / f"module_{name}.py"
717 library_name = f"{name}_lib" if library_name_has_lib_suffix else name
719 if module_file.exists():
720 # We assume that the user lets their 'Module' class inherit from 'BaseModule'.
721 module: BaseModule = load_python_module(module_file).Module(
722 path=path,
723 library_name=library_name,
724 default_registers=default_registers,
725 )
726 return module
728 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)