Coverage for tsfpga/vivado/project.py: 84%

175 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 

9from copy import deepcopy 

10import shutil 

11 

12from tsfpga import TSFPGA_TCL 

13from tsfpga.system_utils import create_file, read_file 

14from tsfpga.build_step_tcl_hook import BuildStepTclHook 

15from .build_result import BuildResult 

16from .common import run_vivado_tcl, run_vivado_gui 

17from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

18from .logic_level_distribution_parser import LogicLevelDistributionParser 

19from .tcl import VivadoTcl 

20 

21 

22class VivadoProject: 

23 """ 

24 Used for handling a Xilinx Vivado HDL project 

25 """ 

26 

27 # pylint: disable=too-many-arguments,too-many-instance-attributes 

28 def __init__( 

29 self, 

30 name, 

31 modules, 

32 part, 

33 top=None, 

34 generics=None, 

35 constraints=None, 

36 tcl_sources=None, 

37 build_step_hooks=None, 

38 vivado_path=None, 

39 default_run_index=1, 

40 defined_at=None, 

41 **other_arguments, 

42 ): 

43 """ 

44 Class constructor. Performs a shallow copy of the mutable arguments, so that the user 

45 can e.g. append items to their list after creating an object. 

46 

47 Arguments: 

48 name (str): Project name. 

49 modules (list(BaseModule)): Modules that shall be included in the project. 

50 part (str): Part identification. 

51 top (str): Name of top level entity. If left out, the top level name will be 

52 inferred from the ``name``. 

53 generics: A dict with generics values (`dict(name: value)`). Use this parameter 

54 for "static" generics that do not change between multiple builds of this 

55 project. These will be set in the project when it is created. 

56 

57 Compare to the build-time generic argument in :meth:`build`. 

58 

59 The generic value shall be of type 

60 

61 * :class:`bool` (suitable for VHDL type ``boolean`` and ``std_logic``), 

62 * :class:`int` (suitable for VHDL type ``integer``, ``natural``, etc.), 

63 * :class:`float` (suitable for VHDL type ``real``), 

64 * :class:`.BitVectorGenericValue` (suitable for VHDL type ``std_logic_vector``, 

65 ``unsigned``, etc.), or 

66 * :class:`.StringGenericValue` (suitable for VHDL type ``string``). 

67 constraints (list(Constraint)): Constraints that will be applied to the project. 

68 tcl_sources (list(pathlib.Path)): A list of TCL files. Use for e.g. block design, 

69 pinning, settings, etc. 

70 build_step_hooks (list(BuildStepTclHook)): Build step hooks that will be applied to the 

71 project. 

72 vivado_path (pathlib.Path): A path to the Vivado executable. If omitted, 

73 the default location from the system PATH will be used. 

74 default_run_index (int): Default run index (synth_X and impl_X) that is set in the 

75 project. Can also use the argument to :meth:`build() <VivadoProject.build>` to 

76 specify at build-time. 

77 defined_at (pathlib.Path): Optional path to the file where you defined this 

78 project. To get a useful ``build.py --list`` message. Is useful when you have many 

79 projects set up. 

80 other_arguments: Optional further arguments. Will not be used by tsfpga, but will 

81 instead be passed on to 

82 

83 * :func:`BaseModule.get_synthesis_files() 

84 <tsfpga.module.BaseModule.get_synthesis_files>` 

85 * :func:`BaseModule.get_ip_core_files() 

86 <tsfpga.module.BaseModule.get_ip_core_files>` 

87 * :func:`BaseModule.get_scoped_constraints() 

88 <tsfpga.module.BaseModule.get_scoped_constraints>` 

89 * :func:`VivadoProject.pre_create` 

90 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>` 

91 * :func:`VivadoProject.pre_build` 

92 * :func:`VivadoProject.post_build` 

93 

94 along with further arguments supplied at build-time to :meth:`.create` and 

95 :meth:`.build`. 

96 

97 .. note:: 

98 This is a "kwargs" style argument. You can pass any number of named arguments. 

99 """ 

100 self.name = name 

101 self.modules = modules.copy() 

102 self.part = part 

103 self.static_generics = {} if generics is None else generics.copy() 

104 self.constraints = [] if constraints is None else constraints.copy() 

105 self.tcl_sources = [] if tcl_sources is None else tcl_sources.copy() 

106 self.build_step_hooks = [] if build_step_hooks is None else build_step_hooks.copy() 

107 self._vivado_path = vivado_path 

108 self.default_run_index = default_run_index 

109 self.defined_at = defined_at 

110 self.other_arguments = None if other_arguments is None else other_arguments.copy() 

111 

112 # Will be set by child class when applicable 

113 self.is_netlist_build = False 

114 self.analyze_synthesis_timing = True 

115 self.report_logic_level_distribution = False 

116 self.ip_cores_only = False 

117 

118 self.top = name + "_top" if top is None else top 

119 

120 self.tcl = VivadoTcl(name=self.name) 

121 

122 def project_file(self, project_path): 

123 """ 

124 Arguments: 

125 project_path (pathlib.Path): A path containing a Vivado project. 

126 Return: 

127 pathlib.Path: The project file of this project, in the given folder 

128 """ 

129 return project_path / (self.name + ".xpr") 

130 

131 def _setup_tcl_sources(self): 

132 tsfpga_tcl_sources = [ 

133 TSFPGA_TCL / "vivado_default_run.tcl", 

134 TSFPGA_TCL / "vivado_fast_run.tcl", 

135 TSFPGA_TCL / "vivado_messages.tcl", 

136 ] 

137 

138 # Add tsfpga TCL sources first. The user might want to change something in the tsfpga 

139 # settings. Conversely, tsfpga should not modify something that the user has set up. 

140 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

141 

142 def _setup_build_step_hooks(self): 

143 # Check the implemented timing and resource utilization via TCL build hooks. 

144 # This is different than for synthesis, where it is embedded in the build script. 

145 # This is due to Vivado limitations related to post-synthesis hooks. 

146 # Specifically, the report_utilization figures do not include IP cores when it is run in 

147 # a post-synthesis hook. 

148 self.build_step_hooks.append( 

149 BuildStepTclHook(TSFPGA_TCL / "report_utilization.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE") 

150 ) 

151 self.build_step_hooks.append( 

152 BuildStepTclHook(TSFPGA_TCL / "check_timing.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE") 

153 ) 

154 

155 if not self.analyze_synthesis_timing: 

156 # In this special case however, the synthesized design is never opened, and 

157 # report_utilization is not run by the build_vivado_project.tcl. 

158 # So in order to get a utilization report anyway we add it as a hook. 

159 # This mode is exclusively used by netlist builds, which very rarely include IP cores, 

160 # so it is acceptable that the utilization report might be erroneous with regards to 

161 # IP cores. 

162 self.build_step_hooks.append( 

163 BuildStepTclHook( 

164 TSFPGA_TCL / "report_utilization.tcl", "STEPS.SYNTH_DESIGN.TCL.POST" 

165 ) 

166 ) 

167 

168 if self.report_logic_level_distribution: 

169 # Used by netlist builds 

170 self.build_step_hooks.append( 

171 BuildStepTclHook( 

172 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

173 "STEPS.SYNTH_DESIGN.TCL.POST", 

174 ) 

175 ) 

176 

177 def _create_tcl(self, project_path, ip_cache_path, all_arguments): 

178 """ 

179 Make a TCL file that creates a Vivado project 

180 """ 

181 if project_path.exists(): 

182 raise ValueError(f"Folder already exists: {project_path}") 

183 project_path.mkdir(parents=True) 

184 

185 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

186 tcl = self.tcl.create( 

187 project_folder=project_path, 

188 modules=self.modules, 

189 part=self.part, 

190 top=self.top, 

191 run_index=self.default_run_index, 

192 generics=self.static_generics, 

193 constraints=self.constraints, 

194 tcl_sources=self.tcl_sources, 

195 build_step_hooks=self.build_step_hooks, 

196 ip_cache_path=ip_cache_path, 

197 disable_io_buffers=self.is_netlist_build, 

198 ip_cores_only=self.ip_cores_only, 

199 other_arguments=all_arguments, 

200 ) 

201 create_file(create_vivado_project_tcl, tcl) 

202 

203 return create_vivado_project_tcl 

204 

205 def create(self, project_path, ip_cache_path=None, **other_arguments): 

206 """ 

207 Create a Vivado project 

208 

209 Arguments: 

210 project_path (pathlib.Path): Path where the project shall be placed. 

211 ip_cache_path (pathlib.Path): Path to a folder where the Vivado IP cache can be 

212 placed. If omitted, the Vivado IP cache mechanism will not be enabled. 

213 other_arguments: Optional further arguments. Will not be used by tsfpga, but will 

214 instead be sent to 

215 

216 * :func:`BaseModule.get_synthesis_files() 

217 <tsfpga.module.BaseModule.get_synthesis_files>` 

218 * :func:`BaseModule.get_ip_core_files() 

219 <tsfpga.module.BaseModule.get_ip_core_files>` 

220 * :func:`BaseModule.get_scoped_constraints() 

221 <tsfpga.module.BaseModule.get_scoped_constraints>` 

222 * :func:`VivadoProject.pre_create` 

223 

224 along with further ``other_arguments`` supplied to :meth:`.__init__`. 

225 

226 .. note:: 

227 This is a "kwargs" style argument. You can pass any number of named arguments. 

228 Returns: 

229 bool: True if everything went well. 

230 """ 

231 print(f"Creating Vivado project in {project_path}") 

232 self._setup_tcl_sources() 

233 self._setup_build_step_hooks() 

234 

235 # The pre-create hook might have side effects. E.g. change some register constants. 

236 # So we make a deep copy of the module list before the hook is called. 

237 # Note that the modules are copied before the pre-build hooks as well, 

238 # since we do not know if we might be performing a create-only or 

239 # build-only operation. The copy does not take any significant time, so this is not 

240 # an issue. 

241 self.modules = deepcopy(self.modules) 

242 

243 # Send all available arguments that are reasonable to use in pre-create and module getter 

244 # functions. Prefer run-time values over the static. 

245 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

246 all_arguments.update( 

247 generics=self.static_generics, 

248 part=self.part, 

249 ) 

250 

251 if not self.pre_create( 

252 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

253 ): 

254 print("ERROR: Project pre-create hook returned False. Failing the build.") 

255 return False 

256 

257 create_vivado_project_tcl = self._create_tcl( 

258 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

259 ) 

260 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

261 

262 def pre_create(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

263 """ 

264 Override this function in a child class if you wish to do something useful with it. 

265 Will be called from :meth:`.create` right before the call to Vivado. 

266 

267 An example use case for this function is when TCL source scripts for the Vivado project 

268 have to be auto generated. This could e.g. be scripts that set IP repo paths based on the 

269 Vivado system PATH. 

270 

271 .. Note:: 

272 This default method does nothing. Shall be overridden by project that utilize 

273 this mechanism. 

274 

275 Arguments: 

276 kwargs: Will have all the :meth:`.create` parameters in it, as well as everything in 

277 the ``other_arguments`` argument to :func:`VivadoProject.__init__`. 

278 

279 Return: 

280 bool: True if everything went well. 

281 """ 

282 return True 

283 

284 def _build_tcl( 

285 self, 

286 project_path, 

287 output_path, 

288 num_threads, 

289 run_index, 

290 all_generics, 

291 synth_only, 

292 from_impl, 

293 ): 

294 """ 

295 Make a TCL file that builds a Vivado project 

296 """ 

297 project_file = self.project_file(project_path) 

298 if not project_file.exists(): 

299 raise ValueError( 

300 f"Project file does not exist in the specified location: {project_file}" 

301 ) 

302 

303 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

304 tcl = self.tcl.build( 

305 project_file=project_file, 

306 output_path=output_path, 

307 num_threads=num_threads, 

308 run_index=run_index, 

309 generics=all_generics, 

310 synth_only=synth_only, 

311 from_impl=from_impl, 

312 analyze_synthesis_timing=self.analyze_synthesis_timing, 

313 ) 

314 create_file(build_vivado_project_tcl, tcl) 

315 

316 return build_vivado_project_tcl 

317 

318 def pre_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

319 """ 

320 Override this function in a child class if you wish to do something useful with it. 

321 Will be called from :meth:`.build` right before the call to Vivado. 

322 

323 Arguments: 

324 kwargs: Will have all the :meth:`.build` parameters in it. Including additional 

325 parameters from the user. 

326 

327 Return: 

328 bool: True if everything went well. 

329 """ 

330 return True 

331 

332 def post_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

333 """ 

334 Override this function in a child class if you wish to do something useful with it. 

335 Will be called from :meth:`.build` right after the call to Vivado. 

336 

337 An example use case for this function is to encrypt the bit file, or generate any other 

338 material that shall be included in FPGA release artifacts. 

339 

340 .. Note:: 

341 This default method does nothing. Shall be overridden by project that utilize 

342 this mechanism. 

343 

344 Arguments: 

345 kwargs: Will have all the :meth:`.build` parameters in it. Including additional 

346 parameters from the user. Will also include ``build_result`` with 

347 implemented/synthesized size, which can be used for asserting the expected resource 

348 utilization. 

349 

350 Return: 

351 bool: True if everything went well. 

352 """ 

353 return True 

354 

355 def build( 

356 self, 

357 project_path, 

358 output_path=None, 

359 run_index=None, 

360 generics=None, 

361 synth_only=False, 

362 from_impl=False, 

363 num_threads=12, 

364 **pre_and_post_build_parameters, 

365 ): 

366 """ 

367 Build a Vivado project 

368 

369 Arguments: 

370 project_path (pathlib.Path): A path containing a Vivado project. 

371 output_path (pathlib.Path): Results (bit file, ...) will be placed here. 

372 run_index (int): Select Vivado run (synth_X and impl_X) to build with. 

373 generics: A dict with generics values (`dict(name: value)`). Use for run-time 

374 generics, i.e. values that can change between each build of this project. 

375 

376 Compare to the create-time generics argument in :meth:`.__init__`. 

377 

378 The generic value types follow the same rules as for :meth:`.__init__`. 

379 synth_only (bool): Run synthesis and then stop. 

380 from_impl (bool): Run the ``impl`` steps and onward on an existing synthesized design. 

381 num_threads (int): Number of parallel threads to use during run. 

382 pre_and_post_build_parameters: Optional further arguments. Will not be used by tsfpga, 

383 but will instead be sent to 

384 

385 * :func:`BaseModule.pre_build() <tsfpga.module.BaseModule.pre_build>` 

386 * :func:`VivadoProject.pre_build` 

387 * :func:`VivadoProject.post_build` 

388 

389 along with further ``other_arguments`` supplied to :meth:`.__init__`. 

390 

391 .. note:: 

392 This is a "kwargs" style argument. You can pass any number of named arguments. 

393 

394 Return: 

395 :class:`.build_result.BuildResult`: Result object with build information. 

396 """ 

397 synth_only = synth_only or self.is_netlist_build 

398 

399 if output_path is None and not synth_only: 

400 raise ValueError("Must specify output_path when doing an implementation run") 

401 

402 if synth_only: 

403 print(f"Synthesizing Vivado project in {project_path}") 

404 else: 

405 print(f"Building Vivado project in {project_path}, placing artifacts in {output_path}") 

406 

407 # Combine to all available generics. Prefer run-time values over static. 

408 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

409 

410 # Run index is optional to specify at build-time 

411 run_index = self.default_run_index if run_index is None else run_index 

412 

413 # Send all available information to pre- and post build functions. Prefer build-time values 

414 # over the static arguments. 

415 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

416 all_parameters.update( 

417 project_path=project_path, 

418 output_path=output_path, 

419 run_index=run_index, 

420 generics=all_generics, 

421 synth_only=synth_only, 

422 from_impl=from_impl, 

423 num_threads=num_threads, 

424 ) 

425 

426 # The pre-build hooks (either project pre-build hook or any of the module's pre-build hooks) 

427 # might have side effects. E.g. change some register constants. So we make a deep copy of 

428 # the module list before any of these hooks are called. Note that the modules are copied 

429 # before the pre-create hook as well, since we do not know if we might be performing a 

430 # create-only or build-only operation. The copy does not take any significant time, so this 

431 # is not an issue. 

432 self.modules = deepcopy(self.modules) 

433 

434 result = BuildResult(self.name) 

435 

436 for module in self.modules: 

437 if not module.pre_build(project=self, **all_parameters): 

438 print( 

439 f"ERROR: Module {module.name} pre-build hook returned False. Failing the build." 

440 ) 

441 result.success = False 

442 return result 

443 

444 # Make sure register packages are up to date 

445 module.create_regs_vhdl_package() 

446 

447 if not self.pre_build(**all_parameters): 

448 print("ERROR: Project pre-build hook returned False. Failing the build.") 

449 result.success = False 

450 return result 

451 

452 build_vivado_project_tcl = self._build_tcl( 

453 project_path=project_path, 

454 output_path=output_path, 

455 num_threads=num_threads, 

456 run_index=run_index, 

457 all_generics=all_generics, 

458 synth_only=synth_only, 

459 from_impl=from_impl, 

460 ) 

461 

462 if not run_vivado_tcl(self._vivado_path, build_vivado_project_tcl): 

463 result.success = False 

464 return result 

465 

466 result.synthesis_size = self._get_size(project_path, f"synth_{run_index}") 

467 if self.report_logic_level_distribution: 

468 result.logic_level_distribution = self._get_logic_level_distribution( 

469 project_path, f"synth_{run_index}" 

470 ) 

471 

472 if not synth_only: 

473 impl_folder = project_path / f"{self.name}.runs" / f"impl_{run_index}" 

474 shutil.copy2(impl_folder / f"{self.top}.bit", output_path / f"{self.name}.bit") 

475 shutil.copy2(impl_folder / f"{self.top}.bin", output_path / f"{self.name}.bin") 

476 result.implementation_size = self._get_size(project_path, f"impl_{run_index}") 

477 

478 # Send the result object, along with everything else, to the post-build function 

479 all_parameters.update(build_result=result) 

480 

481 if not self.post_build(**all_parameters): 

482 print("ERROR: Project post-build hook returned False. Failing the build.") 

483 result.success = False 

484 

485 return result 

486 

487 def open(self, project_path): 

488 """ 

489 Open the project in Vivado GUI. 

490 

491 Arguments: 

492 project_path (pathlib.Path): A path containing a Vivado project. 

493 

494 Returns: 

495 bool: True if everything went well. 

496 """ 

497 return run_vivado_gui(self._vivado_path, self.project_file(project_path)) 

498 

499 def _get_size(self, project_path, run): 

500 """ 

501 Reads the hierarchical utilization report and returns the top level size 

502 for the specified run. 

503 """ 

504 report_as_string = read_file( 

505 project_path / f"{self.name}.runs" / run / "hierarchical_utilization.rpt" 

506 ) 

507 return HierarchicalUtilizationParser.get_size(report_as_string) 

508 

509 def _get_logic_level_distribution(self, project_path, run): 

510 """ 

511 Reads the hierarchical utilization report and returns the top level size 

512 for the specified run. 

513 """ 

514 report_as_string = read_file( 

515 project_path / f"{self.name}.runs" / run / "logical_level_distribution.rpt" 

516 ) 

517 return LogicLevelDistributionParser.get_table(report_as_string) 

518 

519 def __str__(self): 

520 result = f"{self.name}\n" 

521 

522 if self.defined_at is not None: 

523 result += f"Defined at: {self.defined_at.resolve()}\n" 

524 

525 result += f"Type: {self.__class__.__name__}\n" 

526 result += f"Top level: {self.top}\n" 

527 

528 if self.static_generics: 

529 generics = self._dict_to_string(self.static_generics) 

530 else: 

531 generics = "-" 

532 result += f"Generics: {generics}\n" 

533 

534 if self.other_arguments: 

535 result += f"Arguments: {self._dict_to_string(self.other_arguments)}\n" 

536 

537 return result 

538 

539 @staticmethod 

540 def _dict_to_string(data): 

541 return ", ".join([f"{name}={value}" for name, value in data.items()]) 

542 

543 

544class VivadoNetlistProject(VivadoProject): 

545 """ 

546 Used for handling Vivado build of a module without top level pinning. 

547 """ 

548 

549 def __init__(self, analyze_synthesis_timing=False, build_result_checkers=None, **kwargs): 

550 """ 

551 Arguments: 

552 analyze_synthesis_timing (bool): Enable analysis of the synthesized design's timing. 

553 This will make the build flow open the design, and check for unhandled clock 

554 crossings and pulse width violations. 

555 Enabling it will add significant build time (can be as much as +40%). 

556 Also, in order for clock crossing check to work, the clocks have to be created 

557 using a constraint file. 

558 build_result_checkers (list(SizeChecker, MaximumLogicLevel)): 

559 Checkers that will be executed after a successful build. Is used to automatically 

560 check that e.g. resource utilization is not greater than expected. 

561 kwargs: Further arguments accepted by :meth:`.VivadoProject.__init__`. 

562 """ 

563 super().__init__(**kwargs) 

564 

565 self.is_netlist_build = True 

566 self.analyze_synthesis_timing = analyze_synthesis_timing 

567 self.report_logic_level_distribution = True 

568 self.build_result_checkers = [] if build_result_checkers is None else build_result_checkers 

569 

570 def build(self, **kwargs): # pylint: disable=arguments-differ 

571 """ 

572 Build the project. 

573 

574 Arguments: 

575 kwargs: All arguments as accepted by :meth:`.VivadoProject.build`. 

576 """ 

577 result = super().build(**kwargs) 

578 result.success = result.success and self._check_size(result) 

579 

580 return result 

581 

582 def _check_size(self, build_result): 

583 if not build_result.success: 

584 print(f"Can not do post_build check for {self.name} since it did not succeed.") 

585 return False 

586 

587 success = True 

588 for build_result_checker in self.build_result_checkers: 

589 checker_result = build_result_checker.check(build_result) 

590 success = success and checker_result 

591 

592 return success 

593 

594 

595class VivadoIpCoreProject(VivadoProject): 

596 """ 

597 A Vivado project that is only used to generate simulation models of IP cores. 

598 """ 

599 

600 def __init__(self, **kwargs): 

601 """ 

602 Arguments: 

603 kwargs: Arguments as accepted by :meth:`.VivadoProject.__init__`. 

604 """ 

605 super().__init__(**kwargs) 

606 

607 self.ip_cores_only = True 

608 

609 def build(self, **kwargs): # pylint: disable=arguments-differ 

610 """ 

611 Not implemented. 

612 """ 

613 raise NotImplementedError("IP core project can not be built") 

614 

615 

616def copy_and_combine_dicts(dict_first, dict_second): 

617 """ 

618 Will prefer values in the second dict, in case the same key occurs in both. 

619 Will return ``None`` if both are ``None``. 

620 """ 

621 if dict_first is None and dict_second is None: 

622 return None 

623 

624 if dict_first is None: 

625 return dict_second.copy() 

626 

627 if dict_second is None: 

628 return dict_first.copy() 

629 

630 result = dict_first.copy() 

631 result.update(dict_second) 

632 return result