Coverage for tsfpga/module.py: 99%

186 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:39 +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 

9from __future__ import annotations 

10 

11import random 

12from typing import TYPE_CHECKING, Any 

13 

14from hdl_registers.generator.vhdl.axi_lite.wrapper import VhdlAxiLiteWrapperGenerator 

15from hdl_registers.generator.vhdl.record_package import VhdlRecordPackageGenerator 

16from hdl_registers.generator.vhdl.register_package import VhdlRegisterPackageGenerator 

17from hdl_registers.generator.vhdl.simulation.check_package import ( 

18 VhdlSimulationCheckPackageGenerator, 

19) 

20from hdl_registers.generator.vhdl.simulation.read_write_package import ( 

21 VhdlSimulationReadWritePackageGenerator, 

22) 

23from hdl_registers.generator.vhdl.simulation.wait_until_package import ( 

24 VhdlSimulationWaitUntilPackageGenerator, 

25) 

26from hdl_registers.parser.toml import from_toml 

27 

28from tsfpga.constraint import Constraint 

29from tsfpga.hdl_file import HdlFile, get_hdl_file_endings 

30from tsfpga.ip_core_file import IpCoreFile 

31from tsfpga.module_list import ModuleList 

32from tsfpga.system_utils import load_python_module 

33 

34if TYPE_CHECKING: 

35 from collections.abc import Callable, Iterable 

36 from pathlib import Path 

37 

38 from hdl_registers.register import Register 

39 from hdl_registers.register_list import RegisterList 

40 from vunit.ui import VUnit 

41 from vunit.ui.test import Test 

42 from vunit.ui.testbench import TestBench 

43 

44 from .build_project_list import BuildProject 

45 

46 

47class BaseModule: 

48 """ 

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

50 

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

52 different catalog structure. 

53 """ 

54 

55 # Set to False if you do not want to create these register artifacts for this module. 

56 # Can be done in a child class or on an object instance. 

57 # Note that artifacts will only be created if the module actually has any registers. 

58 create_register_package = True 

59 create_record_package = True 

60 create_axi_lite_wrapper = True 

61 create_simulation_read_write_package = True 

62 create_simulation_check_package = True 

63 create_simulation_wait_until_package = True 

64 

65 def __init__( 

66 self, path: Path, library_name: str, default_registers: list[Register] | None = None 

67 ) -> None: 

68 """ 

69 Arguments: 

70 path: Path to the module folder. 

71 library_name: VHDL library name. 

72 default_registers: Default registers. 

73 """ 

74 self.path = path.resolve() 

75 self.name = path.name 

76 self.library_name = library_name 

77 

78 self._default_registers = default_registers 

79 self._registers: RegisterList | None = None 

80 

81 @property 

82 def test_folders(self) -> list[Path]: 

83 """ 

84 Testbench files will be gathered from these folders. 

85 """ 

86 return [ 

87 self.path / "test", 

88 self.path / "rtl" / "tb", 

89 ] 

90 

91 @property 

92 def sim_folders(self) -> list[Path]: 

93 """ 

94 Source code files with simulation models will be gathered from these folders. 

95 

96 Theses are always included in the simulation project. 

97 When testbenches from this module are excluded, these simulation model files will still be 

98 included and can be used by other modules. 

99 """ 

100 return [ 

101 self.path / "sim", 

102 self.register_simulation_folder, 

103 ] 

104 

105 @property 

106 def synthesis_folders(self) -> list[Path]: 

107 """ 

108 Synthesis/implementation source code files will be gathered from these folders. 

109 """ 

110 return [ 

111 self.path, 

112 self.path / "src", 

113 self.path / "rtl", 

114 self.path / "hdl" / "rtl", 

115 self.path / "hdl" / "package", 

116 self.register_synthesis_folder, 

117 ] 

118 

119 @property 

120 def register_data_file(self) -> Path: 

121 """ 

122 The path to this module's register data file (which may or may not exist). 

123 """ 

124 return self.path / f"regs_{self.name}.toml" 

125 

126 @property 

127 def register_simulation_folder(self) -> Path: 

128 """ 

129 Generated register artifacts that are needed for simulation will be placed in this folder. 

130 """ 

131 return self.path / "regs_sim" 

132 

133 @property 

134 def register_synthesis_folder(self) -> Path: 

135 """ 

136 Generated register artifacts that are needed for synthesis/implementation will be 

137 placed in this folder. 

138 """ 

139 return self.path / "regs_src" 

140 

141 @property 

142 def registers(self) -> RegisterList | None: 

143 """ 

144 Get the registers for this module. 

145 Will be ``None`` if the module doesn't have any registers. 

146 I.e. if no TOML file exists and no hook creates registers. 

147 """ 

148 if self._registers: 

149 # Only create object from TOML once. 

150 return self._registers 

151 

152 toml_file = self.register_data_file 

153 if toml_file.exists(): 

154 self._registers = from_toml( 

155 name=self.name, toml_file=toml_file, default_registers=self._default_registers 

156 ) 

157 

158 self.registers_hook() 

159 return self._registers 

160 

161 def setup_vunit( 

162 self, 

163 vunit_proj: VUnit, 

164 **kwargs: Any, # noqa: ANN401 

165 ) -> None: 

166 """ 

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

168 

169 .. Note:: 

170 This default method does nothing. 

171 Should be overridden by modules that have any test benches that operate via generics. 

172 

173 Arguments: 

174 vunit_proj: The VUnit project that is used to run simulation. 

175 kwargs: Use this to pass an arbitrary list of arguments from your ``simulate.py`` 

176 to the module where you set up your tests. 

177 This could be, e.g., data dimensions, location of test files, etc. 

178 """ 

179 

180 def get_build_projects(self) -> list[BuildProject]: 

181 """ 

182 Get FPGA build projects defined by this module. 

183 

184 .. Note:: 

185 This default method does nothing. 

186 Should be overridden by modules that set up build projects. 

187 

188 Return: 

189 FPGA build projects. 

190 """ 

191 return [] 

192 

193 def get_simulation_files( 

194 self, 

195 include_tests: bool = True, 

196 files_include: set[Path] | None = None, 

197 files_avoid: set[Path] | None = None, 

198 include_vhdl_files: bool = True, 

199 include_verilog_files: bool = True, 

200 include_systemverilog_files: bool = True, 

201 **kwargs: Any, # noqa: ANN401 

202 ) -> list[HdlFile]: 

203 """ 

204 Get a list of files that shall be included in a simulation project. 

205 

206 The ``files_include`` and ``files_avoid`` arguments can be used to filter what files are 

207 included. 

208 This can be useful in many situations, e.g. when encrypted files of files that include an 

209 IP core shall be avoided. 

210 It is recommended to overload this function in a subclass in your ``module_*.py``, 

211 and call this super method with the arguments supplied. 

212 

213 Arguments: 

214 include_tests: When ``False``, testbenches (``test`` files) are not included. 

215 The ``sim`` files are always included. 

216 files_include: Optionally filter to only include these files. 

217 files_avoid: Optionally filter to discard these files. 

218 include_vhdl_files: Optionally disable inclusion of files with VHDL 

219 file endings. 

220 include_verilog_files: Optionally disable inclusion of files with Verilog 

221 file endings. 

222 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

223 file endings. 

224 kwargs: Further parameters that can be sent by simulation flow to control what 

225 files are included. 

226 

227 Return: 

228 Files that should be included in a simulation project. 

229 """ 

230 # Shallow copy the list since we might append to it. 

231 sim_and_test_folders = self.sim_folders.copy() 

232 

233 if include_tests: 

234 sim_and_test_folders += self.test_folders 

235 

236 self.create_register_simulation_files() 

237 

238 test_files = self._get_hdl_file_list( 

239 folders=sim_and_test_folders, 

240 files_include=files_include, 

241 files_avoid=files_avoid, 

242 include_vhdl_files=include_vhdl_files, 

243 include_verilog_files=include_verilog_files, 

244 include_systemverilog_files=include_systemverilog_files, 

245 ) 

246 

247 synthesis_files = self.get_synthesis_files( 

248 files_include=files_include, 

249 files_avoid=files_avoid, 

250 include_vhdl_files=include_vhdl_files, 

251 include_verilog_files=include_verilog_files, 

252 include_systemverilog_files=include_systemverilog_files, 

253 **kwargs, 

254 ) 

255 

256 return synthesis_files + test_files 

257 

258 def get_synthesis_files( 

259 self, 

260 files_include: set[Path] | None = None, 

261 files_avoid: set[Path] | None = None, 

262 include_vhdl_files: bool = True, 

263 include_verilog_files: bool = True, 

264 include_systemverilog_files: bool = True, 

265 **kwargs: Any, # noqa: ANN401, ARG002 

266 ) -> list[HdlFile]: 

267 """ 

268 Get a list of files that shall be included in a synthesis project. 

269 

270 See :meth:`.get_simulation_files` for instructions on how to use ``files_include`` 

271 and ``files_avoid``. 

272 

273 Arguments: 

274 files_include: Optionally filter to only include these files. 

275 files_avoid: Optionally filter to discard these files. 

276 include_vhdl_files: Optionally disable inclusion of files with VHDL 

277 file endings. 

278 include_verilog_files: Optionally disable inclusion of files with Verilog 

279 file endings. 

280 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

281 file endings. 

282 kwargs: Further parameters that can be sent by build flow to control what 

283 files are included. 

284 

285 Return: 

286 Files that should be included in a synthesis project. 

287 """ 

288 self.create_register_synthesis_files() 

289 

290 return self._get_hdl_file_list( 

291 folders=self.synthesis_folders, 

292 files_include=files_include, 

293 files_avoid=files_avoid, 

294 include_vhdl_files=include_vhdl_files, 

295 include_verilog_files=include_verilog_files, 

296 include_systemverilog_files=include_systemverilog_files, 

297 ) 

298 

299 def get_ip_core_files( 

300 self, 

301 files_include: set[Path] | None = None, 

302 files_avoid: set[Path] | None = None, 

303 **kwargs: Any, # noqa: ANN401, ARG002 

304 ) -> list[IpCoreFile]: 

305 """ 

306 Get the IP cores of 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. 

310 By overloading this method in a subclass, you can pass on ``kwargs`` arguments from the 

311 build/simulation flow to :class:`.ip_core_file.IpCoreFile` creation to achieve 

312 this parameterization. 

313 

314 Arguments: 

315 files_include: Optionally filter to only include these files. 

316 files_avoid: Optionally filter to discard these files. 

317 kwargs: Further parameters that can be sent by build/simulation flow to control what 

318 IP cores are included and what their variables are. 

319 

320 Return: 

321 The IP cores for this module. 

322 """ 

323 # Could be made into a class property, like e.g. 'synthesis_folders', if any user needs it. 

324 folders = [self.path / "ip_cores"] 

325 

326 return [ 

327 IpCoreFile(path=path) 

328 for path in self._get_file_list( 

329 folders=folders, 

330 file_endings=".tcl", 

331 files_include=files_include, 

332 files_avoid=files_avoid, 

333 ) 

334 ] 

335 

336 def get_scoped_constraints( 

337 self, 

338 files_include: set[Path] | None = None, 

339 files_avoid: set[Path] | None = None, 

340 **kwargs: Any, # noqa: ANN401, ARG002 

341 ) -> list[Constraint]: 

342 """ 

343 Get the constraints that shall be applied to a certain entity within this module. 

344 

345 Arguments: 

346 files_include: Optionally filter to only include these files. 

347 files_avoid: Optionally filter to discard these files. 

348 kwargs: Further parameters that can be sent by build/simulation flow to control what 

349 constraints are included. 

350 

351 Return: 

352 The constraints. 

353 """ 

354 # Could be made into a class property, like e.g. 'synthesis_folders', if any user needs it. 

355 folders = [ 

356 self.path / "scoped_constraints", 

357 self.path / "entity_constraints", 

358 self.path / "hdl" / "constraints", 

359 ] 

360 

361 constraint_files = self._get_file_list( 

362 folders=folders, 

363 file_endings=(".tcl", ".xdc"), 

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 

381 return constraints 

382 

383 def get_documentation_files( 

384 self, 

385 files_include: set[Path] | None = None, 

386 files_avoid: set[Path] | None = None, 

387 include_vhdl_files: bool = True, 

388 include_verilog_files: bool = True, 

389 include_systemverilog_files: bool = True, 

390 **kwargs: Any, # noqa: ANN401, ARG002 

391 ) -> list[HdlFile]: 

392 """ 

393 Get a list of files that shall be included in a documentation build. 

394 

395 It will return all files from the module except testbenches and any generated 

396 register package. 

397 Overwrite in a subclass if you want to change this behavior. 

398 

399 See :meth:`.get_simulation_files` for instructions on how to use ``files_include`` 

400 and ``files_avoid``. 

401 

402 Arguments: 

403 files_include: Optionally filter to only include these files. 

404 files_avoid: Optionally filter to discard these files. 

405 include_vhdl_files: Optionally disable inclusion of files with VHDL 

406 file endings. 

407 include_verilog_files: Optionally disable inclusion of files with Verilog 

408 file endings. 

409 include_systemverilog_files: Optionally disable inclusion of files with SystemVerilog 

410 file endings. 

411 kwargs: Further parameters that can be sent by documentation build flow to control what 

412 files are included. 

413 

414 Return: 

415 Files that should be included in documentation. 

416 """ 

417 # Do not include generated register code in the documentation. 

418 files_to_avoid = set( 

419 self._get_file_list( 

420 folders=[self.register_synthesis_folder, self.register_simulation_folder], 

421 file_endings=HdlFile.file_endings, 

422 ) 

423 ) 

424 if files_avoid: 

425 files_to_avoid |= files_avoid 

426 

427 return self._get_hdl_file_list( 

428 folders=self.synthesis_folders + self.sim_folders, 

429 files_include=files_include, 

430 files_avoid=files_to_avoid, 

431 include_vhdl_files=include_vhdl_files, 

432 include_verilog_files=include_verilog_files, 

433 include_systemverilog_files=include_systemverilog_files, 

434 ) 

435 

436 def registers_hook(self) -> None: 

437 """ 

438 Will be called directly after creating this module's registers from 

439 the TOML definition file in :meth:`.registers`. 

440 If the TOML file does not exist this hook will still be called, but the module's registers 

441 will be ``None``. 

442 

443 This is a good place if you want to add or modify some registers from Python. 

444 Override this method and implement the desired behavior in a subclass. 

445 

446 .. Note:: 

447 This default method does nothing. 

448 Shall be overridden by modules that utilize this mechanism. 

449 """ 

450 

451 def create_register_simulation_files(self) -> None: 

452 """ 

453 Create the register artifacts that are needed for simulation. 

454 Does not create the synthesis files, which are also technically needed for simulation. 

455 So a call to :meth:`.create_register_synthesis_files` must also be done. 

456 

457 If this module does not have registers, this method does nothing. 

458 """ 

459 if self.registers is not None: 

460 if self.create_simulation_read_write_package: 

461 VhdlSimulationReadWritePackageGenerator( 

462 register_list=self.registers, output_folder=self.register_simulation_folder 

463 ).create_if_needed() 

464 

465 if self.create_simulation_check_package: 

466 VhdlSimulationCheckPackageGenerator( 

467 register_list=self.registers, output_folder=self.register_simulation_folder 

468 ).create_if_needed() 

469 

470 if self.create_simulation_wait_until_package: 

471 VhdlSimulationWaitUntilPackageGenerator( 

472 register_list=self.registers, output_folder=self.register_simulation_folder 

473 ).create_if_needed() 

474 

475 def create_register_synthesis_files(self) -> None: 

476 """ 

477 Create the register artifacts that are needed for synthesis/implementation. 

478 

479 If this module does not have registers, this method does nothing. 

480 """ 

481 if self.registers is not None: 

482 if self.create_register_package: 

483 VhdlRegisterPackageGenerator( 

484 register_list=self.registers, output_folder=self.register_synthesis_folder 

485 ).create_if_needed() 

486 

487 if self.create_record_package: 

488 VhdlRecordPackageGenerator( 

489 register_list=self.registers, output_folder=self.register_synthesis_folder 

490 ).create_if_needed() 

491 

492 if self.create_axi_lite_wrapper: 

493 VhdlAxiLiteWrapperGenerator( 

494 register_list=self.registers, output_folder=self.register_synthesis_folder 

495 ).create_if_needed() 

496 

497 def pre_build( 

498 self, 

499 project: BuildProject, # noqa: ARG002 

500 **kwargs: Any, # noqa: ANN401, ARG002 

501 ) -> bool: 

502 """ 

503 Will be called by the build flow before the EDA build tool is called. 

504 A typical use case for this mechanism is to set a register constant or default value 

505 based on the generics that are passed to the project. 

506 Could also be used to, e.g., generate BRAM init files based on project information, etc. 

507 

508 .. Note:: 

509 This default method does nothing. 

510 Should be overridden by modules that utilize this mechanism. 

511 

512 Arguments: 

513 project: The project that is being built. Either a :class:`.VivadoProject` or a 

514 :class:`.YosysNetlistBuild`. 

515 kwargs: All other parameters to the build flow. Includes arguments to 

516 :meth:`.VivadoProject.build`/:meth:`.YosysNetlistBuild.build` method as well as 

517 other arguments set in :meth:`.VivadoProject.__init__`/ 

518 :meth:`.YosysNetlistBuild.__init__`. 

519 

520 Return: 

521 True if everything went well. 

522 """ 

523 return True 

524 

525 def add_vunit_config( # noqa: PLR0913, PLR0917 

526 self, 

527 test: Test | TestBench, 

528 name: str | None = None, 

529 generics: dict[str, Any] | None = None, 

530 count: int = 1, 

531 set_random_seed: bool | int | None = False, 

532 pre_config: Callable[..., bool] | None = None, 

533 post_check: Callable[..., bool] | None = None, 

534 ) -> None: 

535 """ 

536 Convenient wrapper to add VUnit test configuration(s). 

537 Call from :meth:`.setup_vunit` in a child class. 

538 

539 Arguments: 

540 test: VUnit test or testbench object. 

541 name: Optional designated name for this config. 

542 Will be used to form the name of the config together with the ``generics`` value. 

543 generics: Generic values that will be applied to the testbench entity. 

544 The values will also be used to form the name of the config. 

545 count: Number of times to setup the same test configuration. 

546 Is useful for testbenches that randomize their behavior based on the VUnit seed. 

547 set_random_seed: Controls setting of the ``seed`` generic: 

548 

549 * When this argument is not assigned, or assigned ``False``, the generic will not 

550 be set. 

551 * When set to boolean ``True``, a random natural (non-negative integer) 

552 generic value will be set. 

553 * When set to an integer value, that specific value will be set for the generic. 

554 This is useful to get a static test case name for waveform inspection. 

555 

556 If the generic is to be set it must exist in the testbench entity, and should have 

557 VHDL type ``natural``. 

558 

559 .. deprecated:: 13.2.2 

560 Use VUnit mechanism for random seed instead rather than this clunky variant. 

561 pre_config: Function to be run before the test. 

562 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details. 

563 post_check: Function to be run after the test. 

564 See `VUnit documentation <https://vunit.github.io/py/ui.html>`_ for details. 

565 """ 

566 generics = {} if generics is None else generics 

567 

568 if set_random_seed != False: # noqa: E712 

569 print( 

570 f"DEPRECATED: {self.__class__.__name__}.add_vunit_config() argument " 

571 "'set_random_seed' is deprecated and will be removed." 

572 ) 

573 

574 # Note that "bool" is a sub-class of "int" in python, so isinstance(set_random_seed, int) 

575 # returns True if it is an integer or a bool. 

576 if isinstance(set_random_seed, bool): 

577 if set_random_seed: 

578 # Use the maximum range for a natural in VHDL-2008 

579 # Does not need to be cryptographically secure. 

580 generics["seed"] = random.randint(0, 2**31 - 1) # noqa: S311 

581 

582 elif isinstance(set_random_seed, int): 

583 generics["seed"] = set_random_seed 

584 

585 base_name = self.test_case_name(name=name, generics=generics) 

586 

587 for count_index in range(count): 

588 if base_name == "": 

589 # VUnit does not allow an empty name, which can happen if both 'name' and 'generics' 

590 # to this method are 'None', but the user sets for example a 'pre_config'. 

591 # Avoid this error mode by setting the count index, even if count is 1. 

592 name = str(count_index) 

593 elif count > 1: 

594 name = f"{base_name}.{count_index}" 

595 else: 

596 name = base_name 

597 

598 test.add_config( 

599 name=name, generics=generics, pre_config=pre_config, post_check=post_check 

600 ) 

601 

602 @staticmethod 

603 def test_case_name(name: str | None = None, generics: dict[str, Any] | None = None) -> str: 

604 """ 

605 Construct a string suitable for naming test cases. 

606 Is typically used only internally by :meth:`.add_vunit_config`, could be a private method, 

607 but there might be use cases where this is called in a child class. 

608 

609 Note that this method can return an empty string if both arguments are ``None``. 

610 

611 Arguments: 

612 name: Optional base name. 

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

614 

615 Return: 

616 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

617 """ 

618 test_case_name = name or "" 

619 

620 if generics: 

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

622 

623 test_case_name = f"{name}.{generics_string}" if test_case_name else generics_string 

624 

625 return test_case_name 

626 

627 def netlist_build_name(self, name: str, generics: dict[str, Any] | None = None) -> str: 

628 """ 

629 Construct a string suitable for naming a netlist build project. 

630 Call from :meth:`.get_build_projects` in a child class. 

631 

632 Arguments: 

633 name: Base name. 

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

635 

636 Return: 

637 For example ``LibraryName.MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

638 """ 

639 return self.test_case_name(name=f"{self.library_name}.{name}", generics=generics) 

640 

641 @staticmethod 

642 def _get_file_list( 

643 folders: list[Path], 

644 file_endings: str | tuple[str, ...], 

645 files_include: set[Path] | None = None, 

646 files_avoid: set[Path] | None = None, 

647 ) -> list[Path]: 

648 """ 

649 Return a list of files given a list of folders. 

650 

651 Arguments: 

652 folders: The folders to search. 

653 file_endings: File endings to include. 

654 files_include: Optionally filter to only include these files. 

655 files_avoid: Optionally filter to discard these files. 

656 """ 

657 files = [] 

658 for folder in folders: 

659 for path in folder.glob("*"): 

660 if not path.is_file(): 

661 continue 

662 

663 if not path.name.lower().endswith(file_endings): 

664 continue 

665 

666 if files_include is not None and path not in files_include: 

667 continue 

668 

669 if files_avoid is not None and path in files_avoid: 

670 continue 

671 

672 files.append(path) 

673 

674 return files 

675 

676 def _get_hdl_file_list( 

677 self, 

678 folders: list[Path], 

679 files_include: set[Path] | None = None, 

680 files_avoid: set[Path] | None = None, 

681 include_vhdl_files: bool = True, 

682 include_verilog_files: bool = True, 

683 include_systemverilog_files: bool = True, 

684 ) -> list[HdlFile]: 

685 """ 

686 Return a list of HDL file objects. 

687 """ 

688 return [ 

689 HdlFile(path=file_path) 

690 for file_path in self._get_file_list( 

691 folders=folders, 

692 file_endings=get_hdl_file_endings( 

693 include_vhdl=include_vhdl_files, 

694 include_verilog=include_verilog_files, 

695 include_systemverilog=include_systemverilog_files, 

696 ), 

697 files_include=files_include, 

698 files_avoid=files_avoid, 

699 ) 

700 ] 

701 

702 def __str__(self) -> str: 

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

704 

705 

706def get_modules( 

707 modules_folder: Path | None = None, 

708 modules_folders: list[Path] | None = None, 

709 names_include: set[str] | None = None, 

710 names_avoid: set[str] | None = None, 

711 library_name_has_lib_suffix: bool = False, 

712 default_registers: list[Register] | None = None, 

713) -> ModuleList: 

714 """ 

715 Get a list of module objects (:class:`BaseModule` or subclasses thereof) based on the source 

716 code folders. 

717 

718 Arguments: 

719 modules_folder: The path to the folder containing modules. 

720 modules_folders: Optionally, you can specify many folders with modules that will all 

721 be searched. 

722 names_include: If specified, only modules with these names will be included. 

723 names_avoid: If specified, modules with these names will be discarded. 

724 library_name_has_lib_suffix: If set, the VHDL library name will be ``<module name>_lib``, 

725 otherwise it is just ``<module name>``. 

726 default_registers: Default registers. 

727 

728 Return: 

729 The modules created from the specified folders. 

730 """ 

731 modules = ModuleList() 

732 

733 folders = [] 

734 if modules_folder is not None: 

735 folders.append(modules_folder) 

736 if modules_folders is not None: 

737 folders += modules_folders 

738 

739 for module_folder in _iterate_module_folders(folders): 

740 module_name = module_folder.name 

741 

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

743 continue 

744 

745 if names_avoid is not None and module_name in names_avoid: 

746 continue 

747 

748 modules.append( 

749 _get_module_object( 

750 path=module_folder, 

751 name=module_name, 

752 library_name_has_lib_suffix=library_name_has_lib_suffix, 

753 default_registers=default_registers, 

754 ) 

755 ) 

756 

757 return modules 

758 

759 

760def get_module( 

761 name: str, 

762 modules_folder: Path | None = None, 

763 modules_folders: list[Path] | None = None, 

764 library_name_has_lib_suffix: bool = False, 

765 default_registers: list[Register] | None = None, 

766) -> BaseModule: 

767 """ 

768 Get a single module object, for a module found in one of the specified source code folders. 

769 Is a wrapper around :func:`.get_modules`. 

770 

771 Arguments: 

772 name: The name of the module. 

773 modules_folder: The path to the folder containing modules. 

774 modules_folders: Optionally, you can specify many folders with modules that will all 

775 be searched. 

776 library_name_has_lib_suffix: If set, the VHDL library name will be ``<module name>_lib``, 

777 otherwise it is just ``<module name>``. 

778 default_registers: Default registers. 

779 

780 Return: 

781 The requested module. 

782 """ 

783 modules = get_modules( 

784 modules_folder=modules_folder, 

785 modules_folders=modules_folders, 

786 names_include={name}, 

787 library_name_has_lib_suffix=library_name_has_lib_suffix, 

788 default_registers=default_registers, 

789 ) 

790 

791 if not modules: 

792 raise RuntimeError(f'Could not find module "{name}".') 

793 

794 if len(modules) > 1: 

795 raise RuntimeError(f'Found multiple modules named "{name}".') 

796 

797 return modules[0] 

798 

799 

800def _iterate_module_folders(modules_folders: list[Path]) -> Iterable[Path]: 

801 for modules_folder in modules_folders: 

802 for module_folder in modules_folder.glob("*"): 

803 if module_folder.is_dir(): 

804 yield module_folder 

805 

806 

807def _get_module_object( 

808 path: Path, 

809 name: str, 

810 library_name_has_lib_suffix: bool, 

811 default_registers: list[Register] | None, 

812) -> BaseModule: 

813 module_file = path / f"module_{name}.py" 

814 library_name = f"{name}_lib" if library_name_has_lib_suffix else name 

815 

816 if module_file.exists(): 

817 # We assume that the user lets their 'Module' class inherit from 'BaseModule'. 

818 module: BaseModule = load_python_module(module_file).Module( 

819 path=path, 

820 library_name=library_name, 

821 default_registers=default_registers, 

822 ) 

823 return module 

824 

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