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

208 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-21 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 shutil 

12from copy import deepcopy 

13from pathlib import Path 

14from typing import TYPE_CHECKING, Any, NoReturn 

15 

16from tsfpga import TSFPGA_TCL 

17from tsfpga.build_step_tcl_hook import BuildStepTclHook 

18from tsfpga.constraint import Constraint 

19from tsfpga.system_utils import create_file, read_file 

20 

21from .build_result import BuildResult 

22from .common import run_vivado_gui, run_vivado_tcl 

23from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

24from .logic_level_distribution_parser import LogicLevelDistributionParser 

25from .tcl import VivadoTcl 

26 

27if TYPE_CHECKING: 

28 from tsfpga.module_list import ModuleList 

29 

30 from .build_result_checker import MaximumLogicLevel, SizeChecker 

31 

32 

33class VivadoProject: 

34 """ 

35 Used for handling a Xilinx Vivado HDL project 

36 """ 

37 

38 def __init__( # noqa: PLR0913 

39 self, 

40 name: str, 

41 modules: ModuleList, 

42 part: str, 

43 top: str | None = None, 

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

45 constraints: list[Constraint] | None = None, 

46 tcl_sources: list[Path] | None = None, 

47 build_step_hooks: list[BuildStepTclHook] | None = None, 

48 vivado_path: Path | None = None, 

49 default_run_index: int = 1, 

50 impl_explore: bool = False, 

51 defined_at: Path | None = None, 

52 **other_arguments: Any, # noqa: ANN401 

53 ) -> None: 

54 """ 

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

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

57 

58 Arguments: 

59 name: Project name. 

60 modules: Modules that shall be included in the project. 

61 part: Part identification. 

62 top: Name of top level entity. 

63 If left out, the top level name will be inferred from the ``name``. 

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

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

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

67 

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

69 

70 The generic value shall be of type 

71 

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

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

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

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

76 ``unsigned``, etc.), or 

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

78 constraints: Constraints that will be applied to the project. 

79 tcl_sources: A list of TCL files. Use for e.g. block design, pinning, settings, etc. 

80 build_step_hooks: Build step hooks that will be applied to the project. 

81 vivado_path: A path to the Vivado executable. 

82 If omitted, the default location from the system PATH will be used. 

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

84 project. 

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

86 specify at build-time. 

87 impl_explore: Run multiple implementation strategies in parallel. 

88 defined_at: Optional path to the file where you defined this project. 

89 To get a useful ``build_fpga.py --list`` message. Is useful when you have many 

90 projects set up. 

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

92 instead be passed on to 

93 

94 * :func:`BaseModule.get_synthesis_files() 

95 <tsfpga.module.BaseModule.get_synthesis_files>` 

96 * :func:`BaseModule.get_ip_core_files() 

97 <tsfpga.module.BaseModule.get_ip_core_files>` 

98 * :func:`BaseModule.get_scoped_constraints() 

99 <tsfpga.module.BaseModule.get_scoped_constraints>` 

100 * :func:`VivadoProject.pre_create` 

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

102 * :func:`VivadoProject.pre_build` 

103 * :func:`VivadoProject.post_build` 

104 

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

106 :meth:`.build`. 

107 

108 .. note:: 

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

110 """ 

111 self.name = name 

112 self.modules = modules.copy() 

113 self.part = part 

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

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

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

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

118 self._vivado_path = vivado_path 

119 self.default_run_index = default_run_index 

120 self.impl_explore = impl_explore 

121 self.defined_at = defined_at 

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

123 

124 # Will be set by subclass when applicable 

125 self.is_netlist_build = False 

126 self.analyze_synthesis_timing = True 

127 self.report_logic_level_distribution = False 

128 self.ip_cores_only = False 

129 

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

131 

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

133 

134 for constraint in self.constraints: 

135 if not isinstance(constraint, Constraint): 

136 raise TypeError(f'Got bad type for "constraints" element: {constraint}') 

137 

138 for tcl_source in self.tcl_sources: 

139 if not isinstance(tcl_source, Path): 

140 raise TypeError(f'Got bad type for "tcl_sources" element: {tcl_source}') 

141 

142 for build_step_hook in self.build_step_hooks: 

143 if not isinstance(build_step_hook, BuildStepTclHook): 

144 raise TypeError(f'Got bad type for "build_step_hooks" element: {build_step_hook}') 

145 

146 def project_file(self, project_path: Path) -> Path: 

147 """ 

148 Arguments: 

149 project_path: A path containing a Vivado project. 

150 

151 Return: 

152 The project file of this project, in the given folder 

153 """ 

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

155 

156 def _setup_tcl_sources(self) -> None: 

157 tsfpga_tcl_sources = [ 

158 TSFPGA_TCL / "vivado_default_run.tcl", 

159 TSFPGA_TCL / "vivado_fast_run.tcl", 

160 TSFPGA_TCL / "vivado_messages.tcl", 

161 ] 

162 

163 if self.impl_explore: 

164 tsfpga_tcl_sources.append(TSFPGA_TCL / "vivado_strategies.tcl") 

165 

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

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

168 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

169 

170 def _setup_build_step_hooks(self) -> None: 

171 # Check that no ERROR messages have been sent by Vivado. After synthesis as well as 

172 # after implementation. 

173 self.build_step_hooks.append( 

174 BuildStepTclHook( 

175 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.SYNTH_DESIGN.TCL.POST" 

176 ) 

177 ) 

178 self.build_step_hooks.append( 

179 BuildStepTclHook( 

180 TSFPGA_TCL / "check_no_error_messages.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE" 

181 ) 

182 ) 

183 

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

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

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

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

188 # a post-synthesis hook. 

189 self.build_step_hooks.append( 

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

191 ) 

192 self.build_step_hooks.append( 

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

194 ) 

195 self.build_step_hooks.append( 

196 BuildStepTclHook(TSFPGA_TCL / "check_cdc.tcl", "STEPS.WRITE_BITSTREAM.TCL.PRE") 

197 ) 

198 

199 if not self.analyze_synthesis_timing: 

200 # In this special case however, the synthesized design is never opened (to save 

201 # execution time), meaning 'report_utilization' is not run by the 

202 # 'build_vivado_project.tcl' script. 

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

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

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

206 # IP cores. 

207 self.build_step_hooks.append( 

208 BuildStepTclHook( 

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

210 ) 

211 ) 

212 

213 if self.report_logic_level_distribution: 

214 # Used by netlist builds 

215 self.build_step_hooks.append( 

216 BuildStepTclHook( 

217 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

218 "STEPS.SYNTH_DESIGN.TCL.POST", 

219 ) 

220 ) 

221 

222 def _create_tcl( 

223 self, project_path: Path, ip_cache_path: Path | None, all_arguments: dict[str, Any] 

224 ) -> Path: 

225 """ 

226 Make a TCL file that creates a Vivado project 

227 """ 

228 if project_path.exists(): 

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

230 project_path.mkdir(parents=True) 

231 

232 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

233 tcl = self.tcl.create( 

234 project_folder=project_path, 

235 modules=self.modules, 

236 part=self.part, 

237 top=self.top, 

238 run_index=self.default_run_index, 

239 generics=self.static_generics, 

240 constraints=self.constraints, 

241 tcl_sources=self.tcl_sources, 

242 build_step_hooks=self.build_step_hooks, 

243 ip_cache_path=ip_cache_path, 

244 disable_io_buffers=self.is_netlist_build, 

245 ip_cores_only=self.ip_cores_only, 

246 other_arguments=all_arguments, 

247 ) 

248 create_file(create_vivado_project_tcl, tcl) 

249 

250 return create_vivado_project_tcl 

251 

252 def create( 

253 self, 

254 project_path: Path, 

255 ip_cache_path: Path | None = None, 

256 **other_arguments: Any, # noqa: ANN401 

257 ) -> bool: 

258 """ 

259 Create a Vivado project 

260 

261 Arguments: 

262 project_path: Path where the project shall be placed. 

263 ip_cache_path: Path to a folder where the Vivado IP cache can be 

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

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

266 instead be sent to 

267 

268 * :func:`BaseModule.get_synthesis_files() 

269 <tsfpga.module.BaseModule.get_synthesis_files>` 

270 * :func:`BaseModule.get_ip_core_files() 

271 <tsfpga.module.BaseModule.get_ip_core_files>` 

272 * :func:`BaseModule.get_scoped_constraints() 

273 <tsfpga.module.BaseModule.get_scoped_constraints>` 

274 * :func:`VivadoProject.pre_create` 

275 

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

277 

278 .. note:: 

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

280 

281 Return: 

282 True if everything went well. 

283 """ 

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

285 self._setup_tcl_sources() 

286 self._setup_build_step_hooks() 

287 

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

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

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

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

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

293 # an issue. 

294 self.modules = deepcopy(self.modules) 

295 

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

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

298 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

299 all_arguments.update( 

300 generics=self.static_generics, 

301 part=self.part, 

302 ) 

303 

304 if not self.pre_create( 

305 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

306 ): 

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

308 return False 

309 

310 create_vivado_project_tcl = self._create_tcl( 

311 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

312 ) 

313 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

314 

315 def pre_create( 

316 self, 

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

318 ) -> bool: 

319 """ 

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

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

322 

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

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

325 Vivado system PATH. 

326 

327 .. Note:: 

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

329 this mechanism. 

330 

331 Arguments: 

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

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

334 

335 Return: 

336 True if everything went well. 

337 """ 

338 return True 

339 

340 def _build_tcl( # noqa: PLR0913 

341 self, 

342 project_path: Path, 

343 output_path: Path, 

344 num_threads: int, 

345 run_index: int, 

346 all_generics: dict[str, Any], 

347 synth_only: bool, 

348 from_impl: bool, 

349 impl_explore: bool, 

350 ) -> Path: 

351 """ 

352 Make a TCL file that builds a Vivado project 

353 """ 

354 project_file = self.project_file(project_path) 

355 if not project_file.exists(): 

356 raise ValueError( 

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

358 ) 

359 

360 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

361 tcl = self.tcl.build( 

362 project_file=project_file, 

363 output_path=output_path, 

364 num_threads=num_threads, 

365 run_index=run_index, 

366 generics=all_generics, 

367 synth_only=synth_only, 

368 from_impl=from_impl, 

369 analyze_synthesis_timing=self.analyze_synthesis_timing, 

370 impl_explore=impl_explore, 

371 ) 

372 create_file(build_vivado_project_tcl, tcl) 

373 

374 return build_vivado_project_tcl 

375 

376 def pre_build( 

377 self, 

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

379 ) -> bool: 

380 """ 

381 Override this function in a subclass if you wish to do something useful with it. 

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

383 

384 Arguments: 

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

386 parameters from the user. 

387 

388 Return: 

389 True if everything went well. 

390 """ 

391 return True 

392 

393 def post_build( 

394 self, 

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

396 ) -> bool: 

397 """ 

398 Override this function in a subclass if you wish to do something useful with it. 

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

400 

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

402 material that shall be included in FPGA release artifacts. 

403 

404 .. Note:: 

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

406 this mechanism. 

407 

408 Arguments: 

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

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

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

412 utilization. 

413 

414 Return: 

415 True if everything went well. 

416 """ 

417 return True 

418 

419 def build( # noqa: C901, PLR0912, PLR0913 

420 self, 

421 project_path: Path, 

422 output_path: Path | None = None, 

423 run_index: int | None = None, 

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

425 synth_only: bool = False, 

426 from_impl: bool = False, 

427 num_threads: int = 12, 

428 **pre_and_post_build_parameters: Any, # noqa: ANN401 

429 ) -> BuildResult: 

430 """ 

431 Build a Vivado project 

432 

433 Arguments: 

434 project_path: A path containing a Vivado project. 

435 output_path: Results (bit file, ...) will be placed here. 

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

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

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

439 

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

441 

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

443 synth_only: Run synthesis and then stop. 

444 from_impl: Run the ``impl`` steps and onward on an existing synthesized design. 

445 num_threads: Number of parallel threads to use during run. 

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

447 but will instead be sent to 

448 

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

450 * :func:`VivadoProject.pre_build` 

451 * :func:`VivadoProject.post_build` 

452 

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

454 

455 .. note:: 

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

457 

458 Return: 

459 Result object with build information. 

460 """ 

461 synth_only = synth_only or self.is_netlist_build 

462 

463 if output_path is None and not synth_only: 

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

465 

466 if synth_only: 

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

468 else: 

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

470 

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

472 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

473 

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

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

476 

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

478 # over the static arguments. 

479 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

480 all_parameters.update( 

481 project_path=project_path, 

482 output_path=output_path, 

483 run_index=run_index, 

484 generics=all_generics, 

485 synth_only=synth_only, 

486 from_impl=from_impl, 

487 num_threads=num_threads, 

488 ) 

489 

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

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

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

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

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

495 # is not an issue. 

496 self.modules = deepcopy(self.modules) 

497 

498 result = BuildResult(self.name) 

499 

500 for module in self.modules: 

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

502 print( 

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

504 ) 

505 result.success = False 

506 return result 

507 

508 # Make sure register packages are up to date 

509 module.create_register_synthesis_files() 

510 

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

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

513 result.success = False 

514 return result 

515 

516 # We ignore the type of 'output_path' going from 'Path | None' to 'Path'. 

517 # It is only used if 'synth_only' is False, and we have an assertion that 'output_path' is 

518 # not None in that case above. 

519 

520 build_vivado_project_tcl = self._build_tcl( 

521 project_path=project_path, 

522 output_path=output_path, 

523 num_threads=num_threads, 

524 run_index=run_index, 

525 all_generics=all_generics, 

526 synth_only=synth_only, 

527 from_impl=from_impl, 

528 impl_explore=self.impl_explore, 

529 ) 

530 

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

532 result.success = False 

533 return result 

534 

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

536 if self.report_logic_level_distribution: 

537 result.logic_level_distribution = self._get_logic_level_distribution( 

538 project_path, f"synth_{run_index}" 

539 ) 

540 

541 if not synth_only: 

542 if self.impl_explore: 

543 runs_path = project_path / f"{self.name}.runs" 

544 for run in runs_path.iterdir(): 

545 if "impl_explore_" in run.resolve().name: 

546 # Check files for existence, since not all runs may have completed 

547 bit_file = run / f"{self.top}.bit" 

548 bin_file = run / f"{self.top}.bin" 

549 if bit_file.exists() or bin_file.exists(): 

550 impl_folder = run 

551 run_name = run.resolve().name 

552 break 

553 else: 

554 run_name = f"impl_{run_index}" 

555 impl_folder = project_path / f"{self.name}.runs" / run_name 

556 bit_file = impl_folder / f"{self.top}.bit" 

557 bin_file = impl_folder / f"{self.top}.bin" 

558 

559 shutil.copy2(bit_file, output_path / f"{self.name}.bit") 

560 shutil.copy2(bin_file, output_path / f"{self.name}.bin") 

561 result.implementation_size = self._get_size(project_path, run_name) 

562 

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

564 all_parameters.update(build_result=result) 

565 

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

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

568 result.success = False 

569 

570 return result 

571 

572 def open(self, project_path: Path) -> bool: 

573 """ 

574 Open the project in Vivado GUI. 

575 

576 Arguments: 

577 project_path: A path containing a Vivado project. 

578 

579 Return: 

580 True if everything went well. 

581 """ 

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

583 

584 def _get_size(self, project_path: Path, run: str) -> dict[str, int]: 

585 """ 

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

587 for the specified run. 

588 """ 

589 report_as_string = read_file( 

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

591 ) 

592 return HierarchicalUtilizationParser.get_size(report_as_string) 

593 

594 def _get_logic_level_distribution(self, project_path: Path, run: str) -> str: 

595 """ 

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

597 for the specified run. 

598 """ 

599 report_as_string = read_file( 

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

601 ) 

602 return LogicLevelDistributionParser.get_table(report_as_string) 

603 

604 def __str__(self) -> str: 

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

606 

607 if self.defined_at is not None: 

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

609 

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

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

612 

613 generics = self._dict_to_string(self.static_generics) if self.static_generics else "-" 

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

615 

616 if self.other_arguments: 

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

618 

619 return result 

620 

621 @staticmethod 

622 def _dict_to_string(data: dict[str, Any]) -> str: 

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

624 

625 

626class VivadoNetlistProject(VivadoProject): 

627 """ 

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

629 """ 

630 

631 def __init__( 

632 self, 

633 analyze_synthesis_timing: bool = False, 

634 build_result_checkers: list[SizeChecker | MaximumLogicLevel] | None = None, 

635 **kwargs: Any, # noqa: ANN401 

636 ) -> None: 

637 """ 

638 Arguments: 

639 analyze_synthesis_timing: Enable analysis of the synthesized design's timing. 

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

641 crossings and pulse width violations. 

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

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

644 using a constraint file. 

645 build_result_checkers: 

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

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

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

649 """ 

650 super().__init__(**kwargs) 

651 

652 self.is_netlist_build = True 

653 self.analyze_synthesis_timing = analyze_synthesis_timing 

654 self.report_logic_level_distribution = True 

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

656 

657 def build( 

658 self, 

659 **kwargs: Any, # noqa: ANN401 

660 ) -> BuildResult: 

661 """ 

662 Build the project. 

663 

664 Arguments: 

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

666 """ 

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

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

669 

670 return result 

671 

672 def _check_size(self, build_result: BuildResult) -> bool: 

673 if not build_result.success: 

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

675 return False 

676 

677 success = True 

678 for build_result_checker in self.build_result_checkers: 

679 checker_result = build_result_checker.check(build_result) 

680 success = success and checker_result 

681 

682 return success 

683 

684 

685class VivadoIpCoreProject(VivadoProject): 

686 """ 

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

688 """ 

689 

690 ip_cores_only = True 

691 

692 def __init__( 

693 self, 

694 **kwargs: Any, # noqa: ANN401 

695 ) -> None: 

696 """ 

697 Arguments: 

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

699 """ 

700 super().__init__(**kwargs) 

701 

702 def build( 

703 self, 

704 **kwargs: Any, # noqa: ANN401 

705 ) -> NoReturn: 

706 """ 

707 Not implemented. 

708 """ 

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

710 

711 

712def copy_and_combine_dicts( 

713 dict_first: dict[str, Any] | None, dict_second: dict[str, Any] | None 

714) -> dict[str, Any]: 

715 """ 

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

717 Will return an empty dictionary if both are ``None``. 

718 """ 

719 if dict_first is None: 

720 if dict_second is None: 

721 return {} 

722 

723 return dict_second.copy() 

724 

725 if dict_second is None: 

726 return dict_first.copy() 

727 

728 result = dict_first.copy() 

729 result.update(dict_second) 

730 

731 return result