Coverage for tsfpga/module.py: 93%

129 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-28 04:01 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project. 

5# https://tsfpga.com 

6# https://gitlab.com/tsfpga/tsfpga 

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

8 

9import random 

10 

11from hdl_registers.parser import from_toml 

12 

13import tsfpga 

14from tsfpga.constraint import Constraint 

15from tsfpga.hdl_file import HdlFile 

16from tsfpga.ip_core_file import IpCoreFile 

17from tsfpga.module_list import ModuleList 

18from tsfpga.system_utils import load_python_module 

19 

20 

21class BaseModule: 

22 """ 

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

24 

25 Files are gathered from a lot of different subfolders, to accommodate for projects having 

26 different catalog structure. 

27 """ 

28 

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

30 """ 

31 Arguments: 

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

33 library_name (str): VHDL library name. 

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

35 """ 

36 self.path = path.resolve() 

37 self.name = path.name 

38 self.library_name = library_name 

39 

40 self._default_registers = default_registers 

41 self._registers = None 

42 

43 @staticmethod 

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

45 """ 

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

47 

48 Arguments: 

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

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

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

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

53 """ 

54 files = [] 

55 for folder in folders: 

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

57 if not file.is_file(): 

58 continue 

59 

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

61 continue 

62 

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

64 continue 

65 

66 if files_avoid is not None and file in files_avoid: 

67 continue 

68 

69 files.append(file) 

70 

71 return files 

72 

73 def _get_hdl_file_list(self, folders, files_include, files_avoid): 

74 """ 

75 Return a list of HDL file objects. 

76 """ 

77 return [ 

78 HdlFile(file_path) 

79 for file_path in self._get_file_list( 

80 folders=folders, 

81 file_endings=HdlFile.file_endings, 

82 files_include=files_include, 

83 files_avoid=files_avoid, 

84 ) 

85 ] 

86 

87 @property 

88 def registers(self): 

89 """ 

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

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

92 """ 

93 if self._registers is not None: 

94 # Only create object once 

95 return self._registers 

96 

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

98 if toml_file.exists(): 

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

100 

101 self.registers_hook() 

102 return self._registers 

103 

104 def registers_hook(self): 

105 """ 

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

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

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

109 

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

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

112 

113 .. Note:: 

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

115 this mechanism. 

116 """ 

117 

118 def create_regs_vhdl_package(self): 

119 """ 

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

121 """ 

122 if self.registers is not None: 

123 self.registers.create_vhdl_package(self.path) 

124 

125 # pylint: disable=unused-argument 

126 def get_synthesis_files(self, files_include=None, files_avoid=None, **kwargs): 

127 """ 

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

129 

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

131 included. 

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

133 IP core shall be avoided. 

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

135 and call this super method with the arguments supplied. 

136 

137 Arguments: 

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

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

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

141 files are included. 

142 

143 Return: 

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

145 """ 

146 self.create_regs_vhdl_package() 

147 

148 folders = [ 

149 self.path, 

150 self.path / "src", 

151 self.path / "rtl", 

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

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

154 ] 

155 return self._get_hdl_file_list( 

156 folders=folders, files_include=files_include, files_avoid=files_avoid 

157 ) 

158 

159 def get_simulation_files( 

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

161 ): 

162 """ 

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

164 and ``files_avoid``. 

165 

166 Arguments: 

167 include_tests (bool): When ``False`` the ``test`` folder, where testbenches usually 

168 reside, is not included. The ``sim`` folder is always included. 

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

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

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

172 files are included. 

173 

174 Return: 

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

176 """ 

177 test_folders = [ 

178 self.path / "sim", 

179 ] 

180 

181 if include_tests: 

182 test_folders += [self.path / "rtl" / "tb", self.path / "test"] 

183 

184 synthesis_files = self.get_synthesis_files( 

185 files_include=files_include, files_avoid=files_avoid, **kwargs 

186 ) 

187 test_files = self._get_hdl_file_list( 

188 test_folders, files_include=files_include, files_avoid=files_avoid 

189 ) 

190 

191 return synthesis_files + test_files 

192 

193 # pylint: disable=unused-argument 

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

195 """ 

196 Get IP cores for this module. 

197 

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

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

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

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

202 

203 Arguments: 

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

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

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

207 IP cores are included and what their variables are. 

208 

209 Return: 

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

211 """ 

212 folders = [ 

213 self.path / "ip_cores", 

214 ] 

215 file_endings = "tcl" 

216 return [ 

217 IpCoreFile(ip_core_file) 

218 for ip_core_file in self._get_file_list( 

219 folders=folders, 

220 file_endings=file_endings, 

221 files_include=files_include, 

222 files_avoid=files_avoid, 

223 ) 

224 ] 

225 

226 # pylint: disable=unused-argument 

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

228 """ 

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

230 

231 Arguments: 

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

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

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

235 constraints are included. 

236 

237 Return: 

238 list(Constraint): The constraints. 

239 """ 

240 folders = [ 

241 self.path / "scoped_constraints", 

242 self.path / "entity_constraints", 

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

244 ] 

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

246 constraint_files = self._get_file_list( 

247 folders=folders, 

248 file_endings=file_endings, 

249 files_include=files_include, 

250 files_avoid=files_avoid, 

251 ) 

252 

253 constraints = [] 

254 if constraint_files: 

255 synthesis_files = self.get_synthesis_files() 

256 for constraint_file in constraint_files: 

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

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

259 # this more probable. 

260 constraint = Constraint( 

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

262 ) 

263 constraint.validate_scoped_entity(synthesis_files) 

264 constraints.append(constraint) 

265 return constraints 

266 

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

268 """ 

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

270 

271 .. Note:: 

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

273 any test benches that operate via generics. 

274 

275 Arguments: 

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

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

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

279 location of test files, etc. 

280 """ 

281 

282 # pylint: disable=unused-argument, no-self-use 

283 def pre_build(self, project, **kwargs): 

284 """ 

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

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

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

288 based on project information, etc. 

289 

290 .. Note:: 

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

292 utilize this mechanism. 

293 

294 Arguments: 

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

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

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

298 :meth:`.VivadoProject.__init__`. 

299 

300 Return: 

301 bool: True if everything went well. 

302 """ 

303 return True 

304 

305 def get_build_projects(self): # pylint: disable=no-self-use 

306 """ 

307 Get FPGA build projects defined by this module. 

308 

309 .. Note:: 

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

311 build projects. 

312 

313 Return: 

314 list(VivadoProject): FPGA build projects. 

315 """ 

316 return [] 

317 

318 @staticmethod 

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

320 """ 

321 Construct a string suitable for naming test cases. 

322 

323 Arguments: 

324 name (str): Optional base name. 

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

326 

327 Returns: 

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

329 """ 

330 if name: 

331 test_case_name = name 

332 else: 

333 test_case_name = "" 

334 

335 if generics: 

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

337 

338 if test_case_name: 

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

340 else: 

341 test_case_name = generics_string 

342 

343 return test_case_name 

344 

345 def add_vunit_config( 

346 self, 

347 test, 

348 name=None, 

349 generics=None, 

350 set_random_seed=False, 

351 pre_config=None, 

352 post_check=None, 

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

354 """ 

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

356 seed generic. 

357 

358 Arguments: 

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

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

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

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

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

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

365 

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

367 be set. 

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

369 generic value will be set. 

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

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

372 

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

374 VHDL type ``natural``. 

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

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

377 """ 

378 generics = {} if generics is None else generics 

379 

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

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

382 if isinstance(set_random_seed, bool): 

383 if set_random_seed: 

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

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

386 elif isinstance(set_random_seed, int): 

387 generics["seed"] = set_random_seed 

388 

389 name = self.test_case_name(name, generics) 

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

391 

392 def __str__(self): 

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

394 

395 

396def get_modules( 

397 modules_folders, 

398 names_include=None, 

399 names_avoid=None, 

400 library_name_has_lib_suffix=False, 

401 default_registers=None, 

402): 

403 """ 

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

405 

406 Arguments: 

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

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

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

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

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

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

413 

414 Return: 

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

416 created from the specified folders. 

417 """ 

418 modules = ModuleList() 

419 

420 for module_folder in _iterate_module_folders(modules_folders): 

421 module_name = module_folder.name 

422 

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

424 continue 

425 

426 if names_avoid is not None and module_name in names_avoid: 

427 continue 

428 

429 modules.append( 

430 _get_module_object( 

431 module_folder, module_name, library_name_has_lib_suffix, default_registers 

432 ) 

433 ) 

434 

435 return modules 

436 

437 

438def _iterate_module_folders(modules_folders): 

439 for modules_folder in modules_folders: 

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

441 if module_folder.is_dir(): 

442 yield module_folder 

443 

444 

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

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

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

448 

449 if module_file.exists(): 

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

451 return BaseModule(path, library_name, default_registers) 

452 

453 

454def get_hdl_modules(names_include=None, names_avoid=None): 

455 """ 

456 Wrapper of :func:`.get_modules` which returns the ``hdl_modules`` module objects. 

457 If this is a PyPI release of tsfpga, a release of ``hdl_modules`` will be bundled and modules 

458 from there will be returned. 

459 

460 If this is a repo checkout of tsfpga, the function will try to find the ``hdl_modules`` repo 

461 if it is next to the tsfpga repo. 

462 

463 If neither of those alternatives work, the function will assert False. 

464 

465 Arguments will be passed on to :func:`.get_modules`. 

466 

467 Return: 

468 :class:`.ModuleList`: The module objects. 

469 """ 

470 if tsfpga.HDL_MODULES_LOCATION is not None: 

471 if not tsfpga.HDL_MODULES_LOCATION.exists(): 

472 raise FileNotFoundError( 

473 f"The marked location {tsfpga.HDL_MODULES_LOCATION} does not exist. " 

474 "There is something wrong with the release script." 

475 ) 

476 

477 return get_modules( 

478 modules_folders=[tsfpga.HDL_MODULES_LOCATION], 

479 names_include=names_include, 

480 names_avoid=names_avoid, 

481 ) 

482 

483 # Presumed location of the hdl_modules repo 

484 hdl_modules_repo_root = (tsfpga.REPO_ROOT.parent / "hdl_modules").resolve() 

485 if (hdl_modules_repo_root / "modules").exists(): 

486 return get_modules( 

487 modules_folders=[hdl_modules_repo_root / "modules"], 

488 names_include=names_include, 

489 names_avoid=names_avoid, 

490 ) 

491 

492 raise FileNotFoundError( 

493 f"The hdl_modules modules could not be found. Searched in {hdl_modules_repo_root}" 

494 )