Coverage for tsfpga/module.py: 99%

146 statements  

« 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# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import random 

11from pathlib import Path 

12from typing import TYPE_CHECKING, Any, Callable, Iterable, Optional, Union 

13 

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 

27 

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 

34 

35if TYPE_CHECKING: 

36 # Local folder libraries 

37 from .vivado.project import VivadoProject 

38 

39 

40class BaseModule: 

41 """ 

42 Base class for handling a HDL module with RTL code, constraints, etc. 

43 

44 Files are gathered from a lot of different sub-folders, to accommodate for projects having 

45 different catalog structure. 

46 """ 

47 

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 

60 

61 self._default_registers = default_registers 

62 self._registers: Optional[RegisterList] = None 

63 

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. 

73 

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 

85 

86 if not file.name.lower().endswith(file_endings): 

87 continue 

88 

89 if files_include is not None and file not in files_include: 

90 continue 

91 

92 if files_avoid is not None and file in files_avoid: 

93 continue 

94 

95 files.append(file) 

96 

97 return files 

98 

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 ] 

117 

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 

127 

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 ) 

133 

134 self.registers_hook() 

135 return self._registers 

136 

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``. 

142 

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. 

145 

146 .. Note:: 

147 This default method does nothing. Shall be overridden by modules that utilize 

148 this mechanism. 

149 """ 

150 

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() 

159 

160 VhdlRecordPackageGenerator( 

161 register_list=self.registers, output_folder=self.path 

162 ).create_if_needed() 

163 

164 VhdlSimulationReadWritePackageGenerator( 

165 register_list=self.registers, output_folder=self.path 

166 ).create_if_needed() 

167 

168 VhdlSimulationWaitUntilPackageGenerator( 

169 register_list=self.registers, output_folder=self.path 

170 ).create_if_needed() 

171 

172 VhdlAxiLiteWrapperGenerator( 

173 register_list=self.registers, output_folder=self.path 

174 ).create_if_needed() 

175 

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 ] 

188 

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 ] 

197 

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 ] 

207 

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. 

216 

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. 

223 

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. 

229 

230 Return: 

231 Files that should be included in a synthesis project. 

232 """ 

233 self.create_regs_vhdl_package() 

234 

235 return self._get_hdl_file_list( 

236 folders=self.synthesis_folders, files_include=files_include, files_avoid=files_avoid 

237 ) 

238 

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``. 

249 

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. 

257 

258 Return: 

259 Files that should be included in a simulation project. 

260 """ 

261 test_folders = self.sim_folders.copy() 

262 

263 if include_tests: 

264 test_folders += self.test_folders 

265 

266 test_files = self._get_hdl_file_list( 

267 folders=test_folders, files_include=files_include, files_avoid=files_avoid 

268 ) 

269 

270 synthesis_files = self.get_synthesis_files( 

271 files_include=files_include, files_avoid=files_avoid, **kwargs 

272 ) 

273 

274 return synthesis_files + test_files 

275 

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. 

284 

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. 

288 

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 ) 

297 

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. 

307 

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. 

312 

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. 

318 

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 ] 

335 

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. 

345 

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. 

351 

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 ) 

367 

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 

381 

382 def setup_vunit(self, vunit_proj: Any, **kwargs: Any) -> None: 

383 """ 

384 Setup local configuration of this module's test benches. 

385 

386 .. Note:: 

387 This default method does nothing. Should be overridden by modules that have 

388 any test benches that operate via generics. 

389 

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 """ 

396 

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. 

405 

406 .. Note:: 

407 This default method does nothing. Should be overridden by modules that 

408 utilize this mechanism. 

409 

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__`. 

415 

416 Return: 

417 True if everything went well. 

418 """ 

419 return True 

420 

421 def get_build_projects(self) -> list["VivadoProject"]: 

422 """ 

423 Get FPGA build projects defined by this module. 

424 

425 .. Note:: 

426 This default method does nothing. Should be overridden by modules that set up 

427 build projects. 

428 

429 Return: 

430 FPGA build projects. 

431 """ 

432 return [] 

433 

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. 

440 

441 Arguments: 

442 name: Optional base name. 

443 generics: Dictionary of values that will be included in the name. 

444 

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 = "" 

452 

453 if generics: 

454 generics_string = ".".join([f"{key}_{value}" for key, value in generics.items()]) 

455 

456 if test_case_name: 

457 test_case_name = f"{name}.{generics_string}" 

458 else: 

459 test_case_name = generics_string 

460 

461 return test_case_name 

462 

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. 

475 

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: 

483 

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. 

490 

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 

497 

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 

506 

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 

512 

513 test.add_config(name=name, generics=generics, pre_config=pre_config, post_check=post_check) 

514 

515 def __str__(self) -> str: 

516 return f"{self.name}:{self.path}" 

517 

518 

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. 

528 

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. 

536 

537 Return: 

538 List of module objects (:class:`BaseModule` or subclasses thereof) 

539 created from the specified folders. 

540 """ 

541 modules = ModuleList() 

542 

543 for module_folder in _iterate_module_folders(modules_folders): 

544 module_name = module_folder.name 

545 

546 if names_include is not None and module_name not in names_include: 

547 continue 

548 

549 if names_avoid is not None and module_name in names_avoid: 

550 continue 

551 

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 ) 

560 

561 return modules 

562 

563 

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 

569 

570 

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 

579 

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 

588 

589 return BaseModule(path=path, library_name=library_name, default_registers=default_registers)