Coverage for tsfpga/module.py: 99%
146 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-10 20:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-10 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# --------------------------------------------------------------------------------------------------
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.read_write_package import (
19 VhdlSimulationReadWritePackageGenerator,
20)
21from hdl_registers.generator.vhdl.simulation.wait_until_package import (
22 VhdlSimulationWaitUntilPackageGenerator,
23)
24from hdl_registers.parser.toml import from_toml
25from hdl_registers.register import Register
26from hdl_registers.register_list import RegisterList
28# First party libraries
29from tsfpga.constraint import Constraint
30from tsfpga.hdl_file import HdlFile
31from tsfpga.ip_core_file import IpCoreFile
32from tsfpga.module_list import ModuleList
33from tsfpga.system_utils import load_python_module
35if TYPE_CHECKING:
36 # Local folder libraries
37 from .vivado.project import VivadoProject
40class BaseModule:
41 """
42 Base class for handling a HDL module with RTL code, constraints, etc.
44 Files are gathered from a lot of different sub-folders, to accommodate for projects having
45 different catalog structure.
46 """
48 def __init__(
49 self, path: Path, library_name: str, default_registers: Optional[list[Register]] = None
50 ):
51 """
52 Arguments:
53 path: Path to the module folder.
54 library_name: VHDL library name.
55 default_registers: Default registers.
56 """
57 self.path = path.resolve()
58 self.name = path.name
59 self.library_name = library_name
61 self._default_registers = default_registers
62 self._registers: Optional[RegisterList] = None
64 @staticmethod
65 def _get_file_list(
66 folders: list[Path],
67 file_endings: Union[str, tuple[str, ...]],
68 files_include: Optional[set[Path]] = None,
69 files_avoid: Optional[set[Path]] = None,
70 ) -> list[Path]:
71 """
72 Returns a list of files given a list of folders.
74 Arguments:
75 folders: The folders to search.
76 file_endings: File endings to include.
77 files_include: Optionally filter to only include these files.
78 files_avoid: Optionally filter to discard these files.
79 """
80 files = []
81 for folder in folders:
82 for file in folder.glob("*"):
83 if not file.is_file():
84 continue
86 if not file.name.lower().endswith(file_endings):
87 continue
89 if files_include is not None and file not in files_include:
90 continue
92 if files_avoid is not None and file in files_avoid:
93 continue
95 files.append(file)
97 return files
99 def _get_hdl_file_list(
100 self,
101 folders: list[Path],
102 files_include: Optional[set[Path]] = None,
103 files_avoid: Optional[set[Path]] = None,
104 ) -> list[HdlFile]:
105 """
106 Return a list of HDL file objects.
107 """
108 return [
109 HdlFile(file_path)
110 for file_path in self._get_file_list(
111 folders=folders,
112 file_endings=HdlFile.file_endings,
113 files_include=files_include,
114 files_avoid=files_avoid,
115 )
116 ]
118 @property
119 def registers(self) -> Union[RegisterList, None]:
120 """
121 Get the registers for this module.
122 Can be ``None`` if no TOML file exists and no hook creates registers.
123 """
124 if self._registers:
125 # Only create object once
126 return self._registers
128 toml_file = self.path / f"regs_{self.name}.toml"
129 if toml_file.exists():
130 self._registers = from_toml(
131 name=self.name, toml_file=toml_file, default_registers=self._default_registers
132 )
134 self.registers_hook()
135 return self._registers
137 def registers_hook(self) -> None:
138 """
139 This function will be called directly after creating this module's registers from
140 the TOML definition file. If the TOML file does not exist this hook will still be called,
141 but the module's registers will be ``None``.
143 This is a good place if you want to add or modify some registers from Python.
144 Override this method and implement the desired behavior in a subclass.
146 .. Note::
147 This default method does nothing. Shall be overridden by modules that utilize
148 this mechanism.
149 """
151 def create_regs_vhdl_package(self) -> None:
152 """
153 Create a VHDL package in this module with register definitions.
154 """
155 if self.registers is not None:
156 VhdlRegisterPackageGenerator(
157 register_list=self.registers, output_folder=self.path
158 ).create_if_needed()
160 VhdlRecordPackageGenerator(
161 register_list=self.registers, output_folder=self.path
162 ).create_if_needed()
164 VhdlSimulationReadWritePackageGenerator(
165 register_list=self.registers, output_folder=self.path
166 ).create_if_needed()
168 VhdlSimulationWaitUntilPackageGenerator(
169 register_list=self.registers, output_folder=self.path
170 ).create_if_needed()
172 VhdlAxiLiteWrapperGenerator(
173 register_list=self.registers, output_folder=self.path
174 ).create_if_needed()
176 @property
177 def synthesis_folders(self) -> list[Path]:
178 """
179 Synthesis/implementation source code files will be gathered from these folders.
180 """
181 return [
182 self.path,
183 self.path / "src",
184 self.path / "rtl",
185 self.path / "hdl" / "rtl",
186 self.path / "hdl" / "package",
187 ]
189 @property
190 def sim_folders(self) -> list[Path]:
191 """
192 Files with simulation models (the ``sim`` folder) will be gathered from these folders.
193 """
194 return [
195 self.path / "sim",
196 ]
198 @property
199 def test_folders(self) -> list[Path]:
200 """
201 Testbench files will be gathered from these folders.
202 """
203 return [
204 self.path / "test",
205 self.path / "rtl" / "tb",
206 ]
208 def get_synthesis_files( # pylint: disable=unused-argument
209 self,
210 files_include: Optional[set[Path]] = None,
211 files_avoid: Optional[set[Path]] = None,
212 **kwargs: Any,
213 ) -> list[HdlFile]:
214 """
215 Get a list of files that shall be included in a synthesis project.
217 The ``files_include`` and ``files_avoid`` arguments can be used to filter what files are
218 included.
219 This can be useful in many situations, e.g. when encrypted files of files that include an
220 IP core shall be avoided.
221 It is recommended to overload this function in a subclass in your ``module_*.py``,
222 and call this super method with the arguments supplied.
224 Arguments:
225 files_include: Optionally filter to only include these files.
226 files_avoid: Optionally filter to discard these files.
227 kwargs: Further parameters that can be sent by build flow to control what
228 files are included.
230 Return:
231 Files that should be included in a synthesis project.
232 """
233 self.create_regs_vhdl_package()
235 return self._get_hdl_file_list(
236 folders=self.synthesis_folders, files_include=files_include, files_avoid=files_avoid
237 )
239 def get_simulation_files(
240 self,
241 include_tests: bool = True,
242 files_include: Optional[set[Path]] = None,
243 files_avoid: Optional[set[Path]] = None,
244 **kwargs: Any,
245 ) -> list[HdlFile]:
246 """
247 See :meth:`.get_synthesis_files` for instructions on how to use ``files_include``
248 and ``files_avoid``.
250 Arguments:
251 include_tests: When ``False``, the ``test`` files are not included
252 (the ``sim`` files are always included).
253 files_include: Optionally filter to only include these files.
254 files_avoid: Optionally filter to discard these files.
255 kwargs: Further parameters that can be sent by simulation flow to control what
256 files are included.
258 Return:
259 Files that should be included in a simulation project.
260 """
261 test_folders = self.sim_folders.copy()
263 if include_tests:
264 test_folders += self.test_folders
266 test_files = self._get_hdl_file_list(
267 folders=test_folders, files_include=files_include, files_avoid=files_avoid
268 )
270 synthesis_files = self.get_synthesis_files(
271 files_include=files_include, files_avoid=files_avoid, **kwargs
272 )
274 return synthesis_files + test_files
276 def get_documentation_files( # pylint: disable=unused-argument
277 self,
278 files_include: Optional[set[Path]] = None,
279 files_avoid: Optional[set[Path]] = None,
280 **kwargs: Any,
281 ) -> list[HdlFile]:
282 """
283 Get a list of files that shall be included in a documentation build.
285 It will return all files from the module except testbenches and any generated
286 register package.
287 Overwrite in a subclass if you want to change this behavior.
289 Return:
290 Files that should be included in documentation.
291 """
292 return self._get_hdl_file_list(
293 folders=self.synthesis_folders + self.sim_folders,
294 files_include=files_include,
295 files_avoid=files_avoid,
296 )
298 # pylint: disable=unused-argument
299 def get_ip_core_files(
300 self,
301 files_include: Optional[set[Path]] = None,
302 files_avoid: Optional[set[Path]] = None,
303 **kwargs: Any,
304 ) -> list[IpCoreFile]:
305 """
306 Get IP cores for this module.
308 Note that the :class:`.ip_core_file.IpCoreFile` class accepts a ``variables`` argument that
309 can be used to parameterize IP core creation. By overloading this method in a subclass
310 you can pass on ``kwargs`` arguments from the build/simulation flow to
311 :class:`.ip_core_file.IpCoreFile` creation to achieve this parameterization.
313 Arguments:
314 files_include: Optionally filter to only include these files.
315 files_avoid: Optionally filter to discard these files.
316 kwargs: Further parameters that can be sent by build/simulation flow to control what
317 IP cores are included and what their variables are.
319 Return:
320 The IP cores for this module.
321 """
322 folders = [
323 self.path / "ip_cores",
324 ]
325 file_endings = "tcl"
326 return [
327 IpCoreFile(ip_core_file)
328 for ip_core_file in self._get_file_list(
329 folders=folders,
330 file_endings=file_endings,
331 files_include=files_include,
332 files_avoid=files_avoid,
333 )
334 ]
336 # pylint: disable=unused-argument
337 def get_scoped_constraints(
338 self,
339 files_include: Optional[set[Path]] = None,
340 files_avoid: Optional[set[Path]] = None,
341 **kwargs: Any,
342 ) -> list[Constraint]:
343 """
344 Constraints that shall be applied to a certain entity within this module.
346 Arguments:
347 files_include: Optionally filter to only include these files.
348 files_avoid: Optionally filter to discard these files.
349 kwargs: Further parameters that can be sent by build/simulation flow to control what
350 constraints are included.
352 Return:
353 The constraints.
354 """
355 folders = [
356 self.path / "scoped_constraints",
357 self.path / "entity_constraints",
358 self.path / "hdl" / "constraints",
359 ]
360 file_endings = ("tcl", "xdc")
361 constraint_files = self._get_file_list(
362 folders=folders,
363 file_endings=file_endings,
364 files_include=files_include,
365 files_avoid=files_avoid,
366 )
368 constraints = []
369 if constraint_files:
370 synthesis_files = self.get_synthesis_files()
371 for constraint_file in constraint_files:
372 # Scoped constraints often depend on clocks having been created by another
373 # constraint file before they can work. Set processing order to "late" to make
374 # this more probable.
375 constraint = Constraint(
376 constraint_file, scoped_constraint=True, processing_order="late"
377 )
378 constraint.validate_scoped_entity(synthesis_files)
379 constraints.append(constraint)
380 return constraints
382 def setup_vunit(self, vunit_proj: Any, **kwargs: Any) -> None:
383 """
384 Setup local configuration of this module's test benches.
386 .. Note::
387 This default method does nothing. Should be overridden by modules that have
388 any test benches that operate via generics.
390 Arguments:
391 vunit_proj: The VUnit project that is used to run simulation.
392 kwargs: Use this to pass an arbitrary list of arguments from your ``simulate.py``
393 to the module where you set up your tests. This could be, e.g., data dimensions,
394 location of test files, etc.
395 """
397 def pre_build(
398 self, project: "VivadoProject", **kwargs: Any
399 ) -> bool: # pylint: disable=unused-argument
400 """
401 This method hook will be called before an FPGA build is run. A typical use case for this
402 mechanism is to set a register constant or default value based on the generics that
403 are passed to the project. Could also be used to, e.g., generate BRAM init files
404 based on project information, etc.
406 .. Note::
407 This default method does nothing. Should be overridden by modules that
408 utilize this mechanism.
410 Arguments:
411 project: The project that is being built.
412 kwargs: All other parameters to the build flow. Includes arguments to
413 :meth:`.VivadoProject.build` method as well as other arguments set in
414 :meth:`.VivadoProject.__init__`.
416 Return:
417 True if everything went well.
418 """
419 return True
421 def get_build_projects(self) -> list["VivadoProject"]:
422 """
423 Get FPGA build projects defined by this module.
425 .. Note::
426 This default method does nothing. Should be overridden by modules that set up
427 build projects.
429 Return:
430 FPGA build projects.
431 """
432 return []
434 @staticmethod
435 def test_case_name(
436 name: Optional[str] = None, generics: Optional[dict[str, Any]] = None
437 ) -> str:
438 """
439 Construct a string suitable for naming test cases.
441 Arguments:
442 name: Optional base name.
443 generics: Dictionary of values that will be included in the name.
445 Return:
446 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``.
447 """
448 if name:
449 test_case_name = name
450 else:
451 test_case_name = ""
453 if generics:
454 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()])
456 if test_case_name:
457 test_case_name = f"{name}.{generics_string}"
458 else:
459 test_case_name = generics_string
461 return test_case_name
463 def add_vunit_config( # pylint: disable=too-many-arguments
464 self,
465 test: Any,
466 name: Optional[str] = None,
467 generics: Optional[dict[str, Any]] = None,
468 set_random_seed: Optional[Union[bool, int]] = False,
469 pre_config: Optional[Callable[..., bool]] = None,
470 post_check: Optional[Callable[..., bool]] = None,
471 ) -> None:
472 """
473 Add config for VUnit test case. Wrapper that sets a suitable name and can set a random
474 seed generic.
476 Arguments:
477 test: VUnit test object. Can be testbench or test case.
478 name: Optional designated name for this config. Will be used to form the name of
479 the config together with the ``generics`` value.
480 generics: Generic values that will be applied to the testbench entity. The values
481 will also be used to form the name of the config.
482 set_random_seed: Controls setting of the ``seed`` generic:
484 * When this argument is not assigned, or assigned ``False``, the generic will not
485 be set.
486 * When set to boolean ``True``, a random natural (non-negative integer)
487 generic value will be set.
488 * When set to an integer value, that value will be set for the generic.
489 This is useful to get a static test case name for waveform inspection.
491 If the generic is to be set it must exist in the testbench entity, and should have
492 VHDL type ``natural``.
493 pre_config: Function to be run before the test. See VUnit documentation for details.
494 post_check: Function to be run after the test. See VUnit documentation for details.
495 """
496 generics = {} if generics is None else generics
498 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int)
499 # returns True if it is an integer or a bool.
500 if isinstance(set_random_seed, bool):
501 if set_random_seed:
502 # Use the maximum range for a natural in VHDL-2008
503 generics["seed"] = random.randint(0, 2**31 - 1)
504 elif isinstance(set_random_seed, int):
505 generics["seed"] = set_random_seed
507 name = self.test_case_name(name, generics)
508 # VUnit does not allow an empty name, which can happen if both 'name' and 'generics' to
509 # this method are None, but the user sets for example a 'pre_config'.
510 # Avoid this error mode by setting a default name when it happens.
511 name = "test" if name == "" else name
513 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check)
515 def __str__(self) -> str:
516 return f"{self.name}:{self.path}"
519def get_modules(
520 modules_folders: list[Path],
521 names_include: Optional[set[str]] = None,
522 names_avoid: Optional[set[str]] = None,
523 library_name_has_lib_suffix: bool = False,
524 default_registers: Optional[list[Register]] = None,
525) -> ModuleList:
526 """
527 Get a list of Module objects based on the source code folders.
529 Arguments:
530 modules_folders: A list of paths where your modules are located.
531 names_include: If specified, only modules with these names will be included.
532 names_avoid: If specified, modules with these names will be discarded.
533 library_name_has_lib_suffix: If set, the library name will be ``<module name>_lib``,
534 otherwise it is just ``<module name>``.
535 default_registers: Default registers.
537 Return:
538 List of module objects (:class:`BaseModule` or subclasses thereof)
539 created from the specified folders.
540 """
541 modules = ModuleList()
543 for module_folder in _iterate_module_folders(modules_folders):
544 module_name = module_folder.name
546 if names_include is not None and module_name not in names_include:
547 continue
549 if names_avoid is not None and module_name in names_avoid:
550 continue
552 modules.append(
553 _get_module_object(
554 path=module_folder,
555 name=module_name,
556 library_name_has_lib_suffix=library_name_has_lib_suffix,
557 default_registers=default_registers,
558 )
559 )
561 return modules
564def _iterate_module_folders(modules_folders: list[Path]) -> Iterable[Path]:
565 for modules_folder in modules_folders:
566 for module_folder in modules_folder.glob("*"):
567 if module_folder.is_dir():
568 yield module_folder
571def _get_module_object(
572 path: Path,
573 name: str,
574 library_name_has_lib_suffix: bool,
575 default_registers: Optional[list["Register"]],
576) -> BaseModule:
577 module_file = path / f"module_{name}.py"
578 library_name = f"{name}_lib" if library_name_has_lib_suffix else name
580 if module_file.exists():
581 # We assume that the user lets their 'Module' class inherit from 'BaseModule'.
582 module: BaseModule = load_python_module(module_file).Module(
583 path=path,
584 library_name=library_name,
585 default_registers=default_registers,
586 )
587 return module
589 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)