Coverage for tsfpga/module.py: 93%
129 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# --------------------------------------------------------------------------------------------------
9import random
11from hdl_registers.parser import from_toml
13import tsfpga
14from tsfpga.constraint import Constraint
15from tsfpga.hdl_file import HdlFile
16from tsfpga.ip_core_file import IpCoreFile
17from tsfpga.module_list import ModuleList
18from tsfpga.system_utils import load_python_module
21class BaseModule:
22 """
23 Base class for handling a HDL module with RTL code, constraints, etc.
25 Files are gathered from a lot of different subfolders, to accommodate for projects having
26 different catalog structure.
27 """
29 def __init__(self, path, library_name, default_registers=None):
30 """
31 Arguments:
32 path (pathlib.Path): Path to the module folder.
33 library_name (str): VHDL library name.
34 default_registers (list(hdl_registers.register.Register)): Default registers.
35 """
36 self.path = path.resolve()
37 self.name = path.name
38 self.library_name = library_name
40 self._default_registers = default_registers
41 self._registers = None
43 @staticmethod
44 def _get_file_list(folders, file_endings, files_include=None, files_avoid=None):
45 """
46 Returns a list of files given a list of folders.
48 Arguments:
49 folders (pathlib.Path): The folders to search.
50 file_endings (tuple(str)): File endings to include.
51 files_include (set(pathlib.Path)): Optionally filter to only include these files.
52 files_avoid (set(pathlib.Path)): Optionally filter to discard these files.
53 """
54 files = []
55 for folder in folders:
56 for file in folder.glob("*"):
57 if not file.is_file():
58 continue
60 if not file.name.lower().endswith(file_endings):
61 continue
63 if files_include is not None and file not in files_include:
64 continue
66 if files_avoid is not None and file in files_avoid:
67 continue
69 files.append(file)
71 return files
73 def _get_hdl_file_list(self, folders, files_include, files_avoid):
74 """
75 Return a list of HDL file objects.
76 """
77 return [
78 HdlFile(file_path)
79 for file_path in self._get_file_list(
80 folders=folders,
81 file_endings=HdlFile.file_endings,
82 files_include=files_include,
83 files_avoid=files_avoid,
84 )
85 ]
87 @property
88 def registers(self):
89 """
90 hdl_registers.register_list.RegisterList: Get the registers for this module. Can be
91 ``None`` if no TOML file exists and no hook creates registers.
92 """
93 if self._registers is not None:
94 # Only create object once
95 return self._registers
97 toml_file = self.path / f"regs_{self.name}.toml"
98 if toml_file.exists():
99 self._registers = from_toml(self.name, toml_file, self._default_registers)
101 self.registers_hook()
102 return self._registers
104 def registers_hook(self):
105 """
106 This function will be called directly after creating this module's registers from
107 the TOML definition file. If the TOML file does not exist this hook will still be called,
108 but the module's registers will be ``None``.
110 This is a good place if you want to add or modify some registers from Python.
111 Override this method and implement the desired behavior in a child class.
113 .. Note::
114 This default method does nothing. Shall be overridden by modules that utilize
115 this mechanism.
116 """
118 def create_regs_vhdl_package(self):
119 """
120 Create a VHDL package in this module with register definitions.
121 """
122 if self.registers is not None:
123 self.registers.create_vhdl_package(self.path)
125 # pylint: disable=unused-argument
126 def get_synthesis_files(self, files_include=None, files_avoid=None, **kwargs):
127 """
128 Get a list of files that shall be included in a synthesis project.
130 The ``files_include`` and ``files_avoid`` arguments can be used to filter what files are
131 included.
132 This can be useful in many situations, e.g. when encrypted files of files that include an
133 IP core shall be avoided.
134 It is recommended to overload this function in a child class in your ``module_*.py``,
135 and call this super method with the arguments supplied.
137 Arguments:
138 files_include (set(`pathlib.Path`)): Optionally filter to only include these files.
139 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files.
140 kwargs: Further parameters that can be sent by build flow to control what
141 files are included.
143 Return:
144 list(HdlFile): Files that should be included in a synthesis project.
145 """
146 self.create_regs_vhdl_package()
148 folders = [
149 self.path,
150 self.path / "src",
151 self.path / "rtl",
152 self.path / "hdl" / "rtl",
153 self.path / "hdl" / "package",
154 ]
155 return self._get_hdl_file_list(
156 folders=folders, files_include=files_include, files_avoid=files_avoid
157 )
159 def get_simulation_files(
160 self, include_tests=True, files_include=None, files_avoid=None, **kwargs
161 ):
162 """
163 See :meth:`.get_synthesis_files` for instructions on how to use ``files_include``
164 and ``files_avoid``.
166 Arguments:
167 include_tests (bool): When ``False`` the ``test`` folder, where testbenches usually
168 reside, is not included. The ``sim`` folder is always included.
169 files_include (set(`pathlib.Path`)): Optionally filter to only include these files.
170 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files.
171 kwargs: Further parameters that can be sent by simulation flow to control what
172 files are included.
174 Return:
175 list(HdlFile): Files that should be included in a simulation project.
176 """
177 test_folders = [
178 self.path / "sim",
179 ]
181 if include_tests:
182 test_folders += [self.path / "rtl" / "tb", self.path / "test"]
184 synthesis_files = self.get_synthesis_files(
185 files_include=files_include, files_avoid=files_avoid, **kwargs
186 )
187 test_files = self._get_hdl_file_list(
188 test_folders, files_include=files_include, files_avoid=files_avoid
189 )
191 return synthesis_files + test_files
193 # pylint: disable=unused-argument
194 def get_ip_core_files(self, files_include=None, files_avoid=None, **kwargs):
195 """
196 Get IP cores for this module.
198 Note that the :class:`.ip_core_file.IpCoreFile` class accepts a ``variables`` argument that
199 can be used to parameterize IP core creation. By overloading this method in a child class
200 you can pass on ``kwargs`` arguments from the build/simulation flow to
201 :class:`.ip_core_file.IpCoreFile` creation to achieve this parameterization.
203 Arguments:
204 files_include (set(`pathlib.Path`)): Optionally filter to only include these files.
205 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files.
206 kwargs: Further parameters that can be sent by build/simulation flow to control what
207 IP cores are included and what their variables are.
209 Return:
210 list(IpCoreFile): The IP cores for this module.
211 """
212 folders = [
213 self.path / "ip_cores",
214 ]
215 file_endings = "tcl"
216 return [
217 IpCoreFile(ip_core_file)
218 for ip_core_file in self._get_file_list(
219 folders=folders,
220 file_endings=file_endings,
221 files_include=files_include,
222 files_avoid=files_avoid,
223 )
224 ]
226 # pylint: disable=unused-argument
227 def get_scoped_constraints(self, files_include=None, files_avoid=None, **kwargs):
228 """
229 Constraints that shall be applied to a certain entity within this module.
231 Arguments:
232 files_include (set(`pathlib.Path`)): Optionally filter to only include these files.
233 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files.
234 kwargs: Further parameters that can be sent by build/simulation flow to control what
235 constraints are included.
237 Return:
238 list(Constraint): The constraints.
239 """
240 folders = [
241 self.path / "scoped_constraints",
242 self.path / "entity_constraints",
243 self.path / "hdl" / "constraints",
244 ]
245 file_endings = ("tcl", "xdc")
246 constraint_files = self._get_file_list(
247 folders=folders,
248 file_endings=file_endings,
249 files_include=files_include,
250 files_avoid=files_avoid,
251 )
253 constraints = []
254 if constraint_files:
255 synthesis_files = self.get_synthesis_files()
256 for constraint_file in constraint_files:
257 # Scoped constraints often depend on clocks having been created by another
258 # constraint file before they can work. Set processing order to "late" to make
259 # this more probable.
260 constraint = Constraint(
261 constraint_file, scoped_constraint=True, processing_order="late"
262 )
263 constraint.validate_scoped_entity(synthesis_files)
264 constraints.append(constraint)
265 return constraints
267 def setup_vunit(self, vunit_proj, **kwargs):
268 """
269 Setup local configuration of this module's test benches.
271 .. Note::
272 This default method does nothing. Should be overridden by modules that have
273 any test benches that operate via generics.
275 Arguments:
276 vunit_proj: The VUnit project that is used to run simulation.
277 kwargs: Use this to pass an arbitrary list of arguments from your ``simulate.py``
278 to the module where you set up your tests. This could be, e.g., data dimensions,
279 location of test files, etc.
280 """
282 # pylint: disable=unused-argument, no-self-use
283 def pre_build(self, project, **kwargs):
284 """
285 This method hook will be called before an FPGA build is run. A typical use case for this
286 mechanism is to set a register constant or default value based on the generics that
287 are passed to the project. Could also be used to, e.g., generate BRAM init files
288 based on project information, etc.
290 .. Note::
291 This default method does nothing. Should be overridden by modules that
292 utilize this mechanism.
294 Arguments:
295 project (VivadoProject): The project that is being built.
296 kwargs: All other parameters to the build flow. Includes arguments to
297 :meth:`.VivadoProject.build` method as well as other arguments set in
298 :meth:`.VivadoProject.__init__`.
300 Return:
301 bool: True if everything went well.
302 """
303 return True
305 def get_build_projects(self): # pylint: disable=no-self-use
306 """
307 Get FPGA build projects defined by this module.
309 .. Note::
310 This default method does nothing. Should be overridden by modules that set up
311 build projects.
313 Return:
314 list(VivadoProject): FPGA build projects.
315 """
316 return []
318 @staticmethod
319 def test_case_name(name=None, generics=None):
320 """
321 Construct a string suitable for naming test cases.
323 Arguments:
324 name (str): Optional base name.
325 generics (dict): Dictionary of values that will be included in the name.
327 Returns:
328 str: For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``.
329 """
330 if name:
331 test_case_name = name
332 else:
333 test_case_name = ""
335 if generics:
336 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()])
338 if test_case_name:
339 test_case_name = f"{name}.{generics_string}"
340 else:
341 test_case_name = generics_string
343 return test_case_name
345 def add_vunit_config(
346 self,
347 test,
348 name=None,
349 generics=None,
350 set_random_seed=False,
351 pre_config=None,
352 post_check=None,
353 ): # pylint: disable=too-many-arguments
354 """
355 Add config for VUnit test case. Wrapper that sets a suitable name and can set a random
356 seed generic.
358 Arguments:
359 test: VUnit test object. Can be testbench or test case.
360 name (str): Optional designated name for this config. Will be used to form the name of
361 the config together with the ``generics`` value.
362 generics (dict): Generic values that will be applied to the testbench entity. The values
363 will also be used to form the name of the config.
364 set_random_seed (bool, int): Controls setting of the ``seed`` generic:
366 * When this argument is not assigned, or assigned ``False``, the generic will not
367 be set.
368 * When set to boolean ``True``, a random natural (non-negative integer)
369 generic value will be set.
370 * When set to an integer value, that value will be set for the generic.
371 This is useful to get a static test case name for waveform inspection.
373 If the generic is to be set it must exist in the testbench entity, and should have
374 VHDL type ``natural``.
375 pre_config: Function to be run before the test. See VUnit documentation for details.
376 post_check: Function to be run after the test. See VUnit documentation for details.
377 """
378 generics = {} if generics is None else generics
380 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int)
381 # returns True if it is an integer or a bool.
382 if isinstance(set_random_seed, bool):
383 if set_random_seed:
384 # Use the maximum range for a natural in VHDL-2008
385 generics["seed"] = random.randint(0, 2**31 - 1)
386 elif isinstance(set_random_seed, int):
387 generics["seed"] = set_random_seed
389 name = self.test_case_name(name, generics)
390 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check)
392 def __str__(self):
393 return f"{self.name}:{self.path}"
396def get_modules(
397 modules_folders,
398 names_include=None,
399 names_avoid=None,
400 library_name_has_lib_suffix=False,
401 default_registers=None,
402):
403 """
404 Get a list of Module objects based on the source code folders.
406 Arguments:
407 modules_folders (list(pathlib.Path)): A list of paths where your modules are located.
408 names_include (list(str)): If specified, only modules with these names will be included.
409 names_avoid (list(str)): If specified, modules with these names will be discarded.
410 library_name_has_lib_suffix (bool): If set, the library name will be
411 ``<module name>_lib``, otherwise it is just ``<module name>``.
412 default_registers (list(hdl_registers.register.Register)): Default registers.
414 Return:
415 ModuleList: List of module objects (:class:`BaseModule` or child classes thereof)
416 created from the specified folders.
417 """
418 modules = ModuleList()
420 for module_folder in _iterate_module_folders(modules_folders):
421 module_name = module_folder.name
423 if names_include is not None and module_name not in names_include:
424 continue
426 if names_avoid is not None and module_name in names_avoid:
427 continue
429 modules.append(
430 _get_module_object(
431 module_folder, module_name, library_name_has_lib_suffix, default_registers
432 )
433 )
435 return modules
438def _iterate_module_folders(modules_folders):
439 for modules_folder in modules_folders:
440 for module_folder in modules_folder.glob("*"):
441 if module_folder.is_dir():
442 yield module_folder
445def _get_module_object(path, name, library_name_has_lib_suffix, default_registers):
446 module_file = path / f"module_{name}.py"
447 library_name = f"{name}_lib" if library_name_has_lib_suffix else name
449 if module_file.exists():
450 return load_python_module(module_file).Module(path, library_name, default_registers)
451 return BaseModule(path, library_name, default_registers)
454def get_hdl_modules(names_include=None, names_avoid=None):
455 """
456 Wrapper of :func:`.get_modules` which returns the ``hdl_modules`` module objects.
457 If this is a PyPI release of tsfpga, a release of ``hdl_modules`` will be bundled and modules
458 from there will be returned.
460 If this is a repo checkout of tsfpga, the function will try to find the ``hdl_modules`` repo
461 if it is next to the tsfpga repo.
463 If neither of those alternatives work, the function will assert False.
465 Arguments will be passed on to :func:`.get_modules`.
467 Return:
468 :class:`.ModuleList`: The module objects.
469 """
470 if tsfpga.HDL_MODULES_LOCATION is not None:
471 if not tsfpga.HDL_MODULES_LOCATION.exists():
472 raise FileNotFoundError(
473 f"The marked location {tsfpga.HDL_MODULES_LOCATION} does not exist. "
474 "There is something wrong with the release script."
475 )
477 return get_modules(
478 modules_folders=[tsfpga.HDL_MODULES_LOCATION],
479 names_include=names_include,
480 names_avoid=names_avoid,
481 )
483 # Presumed location of the hdl_modules repo
484 hdl_modules_repo_root = (tsfpga.REPO_ROOT.parent / "hdl_modules").resolve()
485 if (hdl_modules_repo_root / "modules").exists():
486 return get_modules(
487 modules_folders=[hdl_modules_repo_root / "modules"],
488 names_include=names_include,
489 names_avoid=names_avoid,
490 )
492 raise FileNotFoundError(
493 f"The hdl_modules modules could not be found. Searched in {hdl_modules_repo_root}"
494 )