Coverage for tsfpga/module.py: 99%

186 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-08-29 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 

9from __future__ import annotations 

10 

11import random 

12from typing import TYPE_CHECKING, Any, Callable 

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 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 .vivado.project import VivadoProject 

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[VivadoProject]: 

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: VivadoProject, # 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. 

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

515 :meth:`.VivadoProject.build` method as well as other arguments set in 

516 :meth:`.VivadoProject.__init__`. 

517 

518 Return: 

519 True if everything went well. 

520 """ 

521 return True 

522 

523 def add_vunit_config( # noqa: PLR0913 

524 self, 

525 test: Test | TestBench, 

526 name: str | None = None, 

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

528 count: int = 1, 

529 set_random_seed: bool | int | None = False, 

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

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

532 ) -> None: 

533 """ 

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

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

536 

537 Arguments: 

538 test: VUnit test or testbench object. 

539 name: Optional designated name for this config. 

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

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

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

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

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

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

546 

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

548 be set. 

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

550 generic value will be set. 

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

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

553 

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

555 VHDL type ``natural``. 

556 

557 .. deprecated:: 13.2.2 

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

559 pre_config: Function to be run before the test. 

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

561 post_check: Function to be run after the test. 

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

563 """ 

564 generics = {} if generics is None else generics 

565 

566 if set_random_seed != False: # noqa: E712 

567 print( 

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

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

570 ) 

571 

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

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

574 if isinstance(set_random_seed, bool): 

575 if set_random_seed: 

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

577 # Does not need to be cryptographically secure. 

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

579 

580 elif isinstance(set_random_seed, int): 

581 generics["seed"] = set_random_seed 

582 

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

584 

585 for count_index in range(count): 

586 if base_name == "": 

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

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

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

590 name = str(count_index) 

591 elif count > 1: 

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

593 else: 

594 name = base_name 

595 

596 test.add_config( 

597 name=name, generics=generics, pre_config=pre_config, post_check=post_check 

598 ) 

599 

600 @staticmethod 

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

602 """ 

603 Construct a string suitable for naming test cases. 

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

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

606 

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

608 

609 Arguments: 

610 name: Optional base name. 

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

612 

613 Return: 

614 For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

615 """ 

616 test_case_name = name if name else "" 

617 

618 if generics: 

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

620 

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

622 

623 return test_case_name 

624 

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

626 """ 

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

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

629 

630 Arguments: 

631 name: Base name. 

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

633 

634 Return: 

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

636 """ 

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

638 

639 @staticmethod 

640 def _get_file_list( 

641 folders: list[Path], 

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

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

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

645 ) -> list[Path]: 

646 """ 

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

648 

649 Arguments: 

650 folders: The folders to search. 

651 file_endings: File endings to include. 

652 files_include: Optionally filter to only include these files. 

653 files_avoid: Optionally filter to discard these files. 

654 """ 

655 files = [] 

656 for folder in folders: 

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

658 if not path.is_file(): 

659 continue 

660 

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

662 continue 

663 

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

665 continue 

666 

667 if files_avoid is not None and path in files_avoid: 

668 continue 

669 

670 files.append(path) 

671 

672 return files 

673 

674 def _get_hdl_file_list( 

675 self, 

676 folders: list[Path], 

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

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

679 include_vhdl_files: bool = True, 

680 include_verilog_files: bool = True, 

681 include_systemverilog_files: bool = True, 

682 ) -> list[HdlFile]: 

683 """ 

684 Return a list of HDL file objects. 

685 """ 

686 return [ 

687 HdlFile(path=file_path) 

688 for file_path in self._get_file_list( 

689 folders=folders, 

690 file_endings=get_hdl_file_endings( 

691 include_vhdl=include_vhdl_files, 

692 include_verilog=include_verilog_files, 

693 include_systemverilog=include_systemverilog_files, 

694 ), 

695 files_include=files_include, 

696 files_avoid=files_avoid, 

697 ) 

698 ] 

699 

700 def __str__(self) -> str: 

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

702 

703 

704def get_modules( 

705 modules_folder: Path | None = None, 

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

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

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

709 library_name_has_lib_suffix: bool = False, 

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

711) -> ModuleList: 

712 """ 

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

714 code folders. 

715 

716 Arguments: 

717 modules_folder: The path to the folder containing modules. 

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

719 be searched. 

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

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

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

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

724 default_registers: Default registers. 

725 

726 Return: 

727 The modules created from the specified folders. 

728 """ 

729 modules = ModuleList() 

730 

731 folders = [] 

732 if modules_folder is not None: 

733 folders.append(modules_folder) 

734 if modules_folders is not None: 

735 folders += modules_folders 

736 

737 for module_folder in _iterate_module_folders(folders): 

738 module_name = module_folder.name 

739 

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

741 continue 

742 

743 if names_avoid is not None and module_name in names_avoid: 

744 continue 

745 

746 modules.append( 

747 _get_module_object( 

748 path=module_folder, 

749 name=module_name, 

750 library_name_has_lib_suffix=library_name_has_lib_suffix, 

751 default_registers=default_registers, 

752 ) 

753 ) 

754 

755 return modules 

756 

757 

758def get_module( 

759 name: str, 

760 modules_folder: Path | None = None, 

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

762 library_name_has_lib_suffix: bool = False, 

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

764) -> BaseModule: 

765 """ 

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

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

768 

769 Arguments: 

770 name: The name of the module. 

771 modules_folder: The path to the folder containing modules. 

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

773 be searched. 

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

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

776 default_registers: Default registers. 

777 

778 Return: 

779 The requested module. 

780 """ 

781 modules = get_modules( 

782 modules_folder=modules_folder, 

783 modules_folders=modules_folders, 

784 names_include={name}, 

785 library_name_has_lib_suffix=library_name_has_lib_suffix, 

786 default_registers=default_registers, 

787 ) 

788 

789 if not modules: 

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

791 

792 if len(modules) > 1: 

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

794 

795 return modules[0] 

796 

797 

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

799 for modules_folder in modules_folders: 

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

801 if module_folder.is_dir(): 

802 yield module_folder 

803 

804 

805def _get_module_object( 

806 path: Path, 

807 name: str, 

808 library_name_has_lib_suffix: bool, 

809 default_registers: list[Register] | None, 

810) -> BaseModule: 

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

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

813 

814 if module_file.exists(): 

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

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

817 path=path, 

818 library_name=library_name, 

819 default_registers=default_registers, 

820 ) 

821 return module 

822 

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