Coverage for tsfpga/module.py: 99%

129 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-29 20:01 +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://gitlab.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9# Standard libraries 

10import random 

11 

12# Third party libraries 

13from hdl_registers.parser import from_toml 

14 

15# First party libraries 

16from tsfpga.constraint import Constraint 

17from tsfpga.hdl_file import HdlFile 

18from tsfpga.ip_core_file import IpCoreFile 

19from tsfpga.module_list import ModuleList 

20from tsfpga.system_utils import load_python_module 

21 

22 

23class BaseModule: 

24 """ 

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

26 

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

28 different catalog structure. 

29 """ 

30 

31 def __init__(self, path, library_name, default_registers=None): 

32 """ 

33 Arguments: 

34 path (pathlib.Path): Path to the module folder. 

35 library_name (str): VHDL library name. 

36 default_registers (list(hdl_registers.register.Register)): Default registers. 

37 """ 

38 self.path = path.resolve() 

39 self.name = path.name 

40 self.library_name = library_name 

41 

42 self._default_registers = default_registers 

43 self._registers = None 

44 

45 @staticmethod 

46 def _get_file_list(folders, file_endings, files_include=None, files_avoid=None): 

47 """ 

48 Returns a list of files given a list of folders. 

49 

50 Arguments: 

51 folders (pathlib.Path): The folders to search. 

52 file_endings (tuple(str)): File endings to include. 

53 files_include (set(pathlib.Path)): Optionally filter to only include these files. 

54 files_avoid (set(pathlib.Path)): Optionally filter to discard these files. 

55 """ 

56 files = [] 

57 for folder in folders: 

58 for file in folder.glob("*"): 

59 if not file.is_file(): 

60 continue 

61 

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

63 continue 

64 

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

66 continue 

67 

68 if files_avoid is not None and file in files_avoid: 

69 continue 

70 

71 files.append(file) 

72 

73 return files 

74 

75 def _get_hdl_file_list(self, folders, files_include=None, files_avoid=None): 

76 """ 

77 Return a list of HDL file objects. 

78 """ 

79 return [ 

80 HdlFile(file_path) 

81 for file_path in self._get_file_list( 

82 folders=folders, 

83 file_endings=HdlFile.file_endings, 

84 files_include=files_include, 

85 files_avoid=files_avoid, 

86 ) 

87 ] 

88 

89 @property 

90 def registers(self): 

91 """ 

92 hdl_registers.register_list.RegisterList: Get the registers for this module. Can be 

93 ``None`` if no TOML file exists and no hook creates registers. 

94 """ 

95 if self._registers is not None: 

96 # Only create object once 

97 return self._registers 

98 

99 toml_file = self.path / f"regs_{self.name}.toml" 

100 if toml_file.exists(): 

101 self._registers = from_toml(self.name, toml_file, self._default_registers) 

102 

103 self.registers_hook() 

104 return self._registers 

105 

106 def registers_hook(self): 

107 """ 

108 This function will be called directly after creating this module's registers from 

109 the TOML definition file. If the TOML file does not exist this hook will still be called, 

110 but the module's registers will be ``None``. 

111 

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

113 Override this method and implement the desired behavior in a child class. 

114 

115 .. Note:: 

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

117 this mechanism. 

118 """ 

119 

120 def create_regs_vhdl_package(self): 

121 """ 

122 Create a VHDL package in this module with register definitions. 

123 """ 

124 if self.registers is not None: 

125 self.registers.create_vhdl_package(self.path) 

126 

127 @property 

128 def synthesis_folders(self): 

129 """ 

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

131 

132 Return: 

133 list(pathlib.Path): Folder paths. 

134 """ 

135 return [ 

136 self.path, 

137 self.path / "src", 

138 self.path / "rtl", 

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

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

141 ] 

142 

143 @property 

144 def sim_folders(self): 

145 """ 

146 Files with simulation models (the ``sim`` folder) will be gathered from these folders. 

147 

148 Return: 

149 list(pathlib.Path): Folder paths. 

150 """ 

151 return [ 

152 self.path / "sim", 

153 ] 

154 

155 @property 

156 def test_folders(self): 

157 """ 

158 Testbench files will be gathered from these folders. 

159 

160 Return: 

161 list(pathlib.Path): Folder paths. 

162 """ 

163 return [ 

164 self.path / "test", 

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

166 ] 

167 

168 def get_synthesis_files( 

169 self, files_include=None, files_avoid=None, **kwargs 

170 ): # pylint: disable=unused-argument 

171 """ 

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

173 

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

175 included. 

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

177 IP core shall be avoided. 

178 It is recommended to overload this function in a child class in your ``module_*.py``, 

179 and call this super method with the arguments supplied. 

180 

181 Arguments: 

182 files_include (set(`pathlib.Path`)): Optionally filter to only include these files. 

183 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files. 

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

185 files are included. 

186 

187 Return: 

188 list(HdlFile): Files that should be included in a synthesis project. 

189 """ 

190 self.create_regs_vhdl_package() 

191 

192 return self._get_hdl_file_list( 

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

194 ) 

195 

196 def get_simulation_files( 

197 self, include_tests=True, files_include=None, files_avoid=None, **kwargs 

198 ): 

199 """ 

200 See :meth:`.get_synthesis_files` for instructions on how to use ``files_include`` 

201 and ``files_avoid``. 

202 

203 Arguments: 

204 include_tests (bool): When ``False``, the ``test`` files are not included 

205 (the ``sim`` files are always included). 

206 files_include (set(`pathlib.Path`)): Optionally filter to only include these files. 

207 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files. 

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

209 files are included. 

210 

211 Return: 

212 list(HdlFile): Files that should be included in a simulation project. 

213 """ 

214 test_folders = self.sim_folders.copy() 

215 

216 if include_tests: 

217 test_folders += self.test_folders 

218 

219 test_files = self._get_hdl_file_list( 

220 folders=test_folders, files_include=files_include, files_avoid=files_avoid 

221 ) 

222 

223 synthesis_files = self.get_synthesis_files( 

224 files_include=files_include, files_avoid=files_avoid, **kwargs 

225 ) 

226 

227 return synthesis_files + test_files 

228 

229 def get_documentation_files( 

230 self, files_include=None, files_avoid=None, **kwargs 

231 ): # pylint: disable=unused-argument 

232 """ 

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

234 

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

236 register package. 

237 Overwrite in a child class if you want to change this behavior. 

238 

239 Return: 

240 list(HdlFile): Files that should be included in documentation. 

241 """ 

242 return self._get_hdl_file_list( 

243 folders=self.synthesis_folders + self.sim_folders, 

244 files_include=files_include, 

245 files_avoid=files_avoid, 

246 ) 

247 

248 # pylint: disable=unused-argument 

249 def get_ip_core_files(self, files_include=None, files_avoid=None, **kwargs): 

250 """ 

251 Get IP cores for this module. 

252 

253 Note that the :class:`.ip_core_file.IpCoreFile` class accepts a ``variables`` argument that 

254 can be used to parameterize IP core creation. By overloading this method in a child class 

255 you can pass on ``kwargs`` arguments from the build/simulation flow to 

256 :class:`.ip_core_file.IpCoreFile` creation to achieve this parameterization. 

257 

258 Arguments: 

259 files_include (set(`pathlib.Path`)): Optionally filter to only include these files. 

260 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files. 

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

262 IP cores are included and what their variables are. 

263 

264 Return: 

265 list(IpCoreFile): The IP cores for this module. 

266 """ 

267 folders = [ 

268 self.path / "ip_cores", 

269 ] 

270 file_endings = "tcl" 

271 return [ 

272 IpCoreFile(ip_core_file) 

273 for ip_core_file in self._get_file_list( 

274 folders=folders, 

275 file_endings=file_endings, 

276 files_include=files_include, 

277 files_avoid=files_avoid, 

278 ) 

279 ] 

280 

281 # pylint: disable=unused-argument 

282 def get_scoped_constraints(self, files_include=None, files_avoid=None, **kwargs): 

283 """ 

284 Constraints that shall be applied to a certain entity within this module. 

285 

286 Arguments: 

287 files_include (set(`pathlib.Path`)): Optionally filter to only include these files. 

288 files_avoid (set(`pathlib.Path`)): Optionally filter to discard these files. 

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

290 constraints are included. 

291 

292 Return: 

293 list(Constraint): The constraints. 

294 """ 

295 folders = [ 

296 self.path / "scoped_constraints", 

297 self.path / "entity_constraints", 

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

299 ] 

300 file_endings = ("tcl", "xdc") 

301 constraint_files = self._get_file_list( 

302 folders=folders, 

303 file_endings=file_endings, 

304 files_include=files_include, 

305 files_avoid=files_avoid, 

306 ) 

307 

308 constraints = [] 

309 if constraint_files: 

310 synthesis_files = self.get_synthesis_files() 

311 for constraint_file in constraint_files: 

312 # Scoped constraints often depend on clocks having been created by another 

313 # constraint file before they can work. Set processing order to "late" to make 

314 # this more probable. 

315 constraint = Constraint( 

316 constraint_file, scoped_constraint=True, processing_order="late" 

317 ) 

318 constraint.validate_scoped_entity(synthesis_files) 

319 constraints.append(constraint) 

320 return constraints 

321 

322 def setup_vunit(self, vunit_proj, **kwargs): 

323 """ 

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

325 

326 .. Note:: 

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

328 any test benches that operate via generics. 

329 

330 Arguments: 

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

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

333 to the module where you set up your tests. This could be, e.g., data dimensions, 

334 location of test files, etc. 

335 """ 

336 

337 def pre_build(self, project, **kwargs): # pylint: disable=unused-argument 

338 """ 

339 This method hook will be called before an FPGA build is run. A typical use case for this 

340 mechanism is to set a register constant or default value based on the generics that 

341 are passed to the project. Could also be used to, e.g., generate BRAM init files 

342 based on project information, etc. 

343 

344 .. Note:: 

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

346 utilize this mechanism. 

347 

348 Arguments: 

349 project (VivadoProject): The project that is being built. 

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

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

352 :meth:`.VivadoProject.__init__`. 

353 

354 Return: 

355 bool: True if everything went well. 

356 """ 

357 return True 

358 

359 def get_build_projects(self): 

360 """ 

361 Get FPGA build projects defined by this module. 

362 

363 .. Note:: 

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

365 build projects. 

366 

367 Return: 

368 list(VivadoProject): FPGA build projects. 

369 """ 

370 return [] 

371 

372 @staticmethod 

373 def test_case_name(name=None, generics=None): 

374 """ 

375 Construct a string suitable for naming test cases. 

376 

377 Arguments: 

378 name (str): Optional base name. 

379 generics (dict): Dictionary of values that will be included in the name. 

380 

381 Returns: 

382 str: For example ``MyBaseName.GenericA_ValueA.GenericB_ValueB``. 

383 """ 

384 if name: 

385 test_case_name = name 

386 else: 

387 test_case_name = "" 

388 

389 if generics: 

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

391 

392 if test_case_name: 

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

394 else: 

395 test_case_name = generics_string 

396 

397 return test_case_name 

398 

399 def add_vunit_config( 

400 self, 

401 test, 

402 name=None, 

403 generics=None, 

404 set_random_seed=False, 

405 pre_config=None, 

406 post_check=None, 

407 ): # pylint: disable=too-many-arguments 

408 """ 

409 Add config for VUnit test case. Wrapper that sets a suitable name and can set a random 

410 seed generic. 

411 

412 Arguments: 

413 test: VUnit test object. Can be testbench or test case. 

414 name (str): Optional designated name for this config. Will be used to form the name of 

415 the config together with the ``generics`` value. 

416 generics (dict): Generic values that will be applied to the testbench entity. The values 

417 will also be used to form the name of the config. 

418 set_random_seed (bool, int): Controls setting of the ``seed`` generic: 

419 

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

421 be set. 

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

423 generic value will be set. 

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

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

426 

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

428 VHDL type ``natural``. 

429 pre_config: Function to be run before the test. See VUnit documentation for details. 

430 post_check: Function to be run after the test. See VUnit documentation for details. 

431 """ 

432 generics = {} if generics is None else generics 

433 

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

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

436 if isinstance(set_random_seed, bool): 

437 if set_random_seed: 

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

439 generics["seed"] = random.randint(0, 2**31 - 1) 

440 elif isinstance(set_random_seed, int): 

441 generics["seed"] = set_random_seed 

442 

443 name = self.test_case_name(name, generics) 

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

445 

446 def __str__(self): 

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

448 

449 

450def get_modules( 

451 modules_folders, 

452 names_include=None, 

453 names_avoid=None, 

454 library_name_has_lib_suffix=False, 

455 default_registers=None, 

456): 

457 """ 

458 Get a list of Module objects based on the source code folders. 

459 

460 Arguments: 

461 modules_folders (list(pathlib.Path)): A list of paths where your modules are located. 

462 names_include (list(str)): If specified, only modules with these names will be included. 

463 names_avoid (list(str)): If specified, modules with these names will be discarded. 

464 library_name_has_lib_suffix (bool): If set, the library name will be 

465 ``<module name>_lib``, otherwise it is just ``<module name>``. 

466 default_registers (list(hdl_registers.register.Register)): Default registers. 

467 

468 Return: 

469 ModuleList: List of module objects (:class:`BaseModule` or child classes thereof) 

470 created from the specified folders. 

471 """ 

472 modules = ModuleList() 

473 

474 for module_folder in _iterate_module_folders(modules_folders): 

475 module_name = module_folder.name 

476 

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

478 continue 

479 

480 if names_avoid is not None and module_name in names_avoid: 

481 continue 

482 

483 modules.append( 

484 _get_module_object( 

485 module_folder, module_name, library_name_has_lib_suffix, default_registers 

486 ) 

487 ) 

488 

489 return modules 

490 

491 

492def _iterate_module_folders(modules_folders): 

493 for modules_folder in modules_folders: 

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

495 if module_folder.is_dir(): 

496 yield module_folder 

497 

498 

499def _get_module_object(path, name, library_name_has_lib_suffix, default_registers): 

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

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

502 

503 if module_file.exists(): 

504 return load_python_module(module_file).Module(path, library_name, default_registers) 

505 return BaseModule(path, library_name, default_registers)