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

205 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 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(generics=self.static_generics, part=self.part) 

300 

301 if not self.pre_create( 

302 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

303 ): 

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

305 return False 

306 

307 create_vivado_project_tcl = self._create_tcl( 

308 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

309 ) 

310 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

311 

312 def pre_create( 

313 self, 

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

315 ) -> bool: 

316 """ 

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

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

319 

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

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

322 Vivado system PATH. 

323 

324 .. Note:: 

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

326 this mechanism. 

327 

328 Arguments: 

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

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

331 

332 Return: 

333 True if everything went well. 

334 """ 

335 return True 

336 

337 def _build_tcl( # noqa: PLR0913 

338 self, 

339 project_path: Path, 

340 output_path: Path, 

341 num_threads: int, 

342 run_index: int, 

343 all_generics: dict[str, Any], 

344 synth_only: bool, 

345 from_impl: bool, 

346 impl_explore: bool, 

347 ) -> Path: 

348 """ 

349 Make a TCL file that builds a Vivado project 

350 """ 

351 project_file = self.project_file(project_path) 

352 if not project_file.exists(): 

353 raise ValueError( 

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

355 ) 

356 

357 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

358 tcl = self.tcl.build( 

359 project_file=project_file, 

360 output_path=output_path, 

361 num_threads=num_threads, 

362 run_index=run_index, 

363 generics=all_generics, 

364 synth_only=synth_only, 

365 from_impl=from_impl, 

366 analyze_synthesis_timing=self.analyze_synthesis_timing, 

367 impl_explore=impl_explore, 

368 ) 

369 create_file(build_vivado_project_tcl, tcl) 

370 

371 return build_vivado_project_tcl 

372 

373 def pre_build( 

374 self, 

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

376 ) -> bool: 

377 """ 

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

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

380 

381 Arguments: 

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

383 parameters from the user. 

384 

385 Return: 

386 True if everything went well. 

387 """ 

388 return True 

389 

390 def post_build( 

391 self, 

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

393 ) -> bool: 

394 """ 

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

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

397 

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

399 material that shall be included in FPGA release artifacts. 

400 

401 .. Note:: 

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

403 this mechanism. 

404 

405 Arguments: 

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

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

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

409 utilization. 

410 

411 Return: 

412 True if everything went well. 

413 """ 

414 return True 

415 

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

417 self, 

418 project_path: Path, 

419 output_path: Path | None = None, 

420 run_index: int | None = None, 

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

422 synth_only: bool = False, 

423 from_impl: bool = False, 

424 num_threads: int = 12, 

425 **pre_and_post_build_parameters: Any, # noqa: ANN401 

426 ) -> BuildResult: 

427 """ 

428 Build a Vivado project 

429 

430 Arguments: 

431 project_path: A path containing a Vivado project. 

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

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

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

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

436 

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

438 

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

440 synth_only: Run synthesis and then stop. 

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

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

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

444 but will instead be sent to 

445 

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

447 * :func:`VivadoProject.pre_build` 

448 * :func:`VivadoProject.post_build` 

449 

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

451 

452 .. note:: 

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

454 

455 Return: 

456 Result object with build information. 

457 """ 

458 synth_only = synth_only or self.is_netlist_build 

459 

460 if output_path is None and not synth_only: 

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

462 

463 if synth_only: 

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

465 else: 

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

467 

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

469 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

470 

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

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

473 

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

475 # over the static arguments. 

476 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

477 all_parameters.update( 

478 project_path=project_path, 

479 output_path=output_path, 

480 run_index=run_index, 

481 generics=all_generics, 

482 synth_only=synth_only, 

483 from_impl=from_impl, 

484 num_threads=num_threads, 

485 ) 

486 

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

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

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

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

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

492 # is not an issue. 

493 self.modules = deepcopy(self.modules) 

494 

495 result = BuildResult(self.name) 

496 

497 for module in self.modules: 

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

499 print( 

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

501 ) 

502 result.success = False 

503 return result 

504 

505 # Make sure register packages are up to date 

506 module.create_register_synthesis_files() 

507 

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

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

510 result.success = False 

511 return result 

512 

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

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

515 # not None in that case above. 

516 

517 build_vivado_project_tcl = self._build_tcl( 

518 project_path=project_path, 

519 output_path=output_path, 

520 num_threads=num_threads, 

521 run_index=run_index, 

522 all_generics=all_generics, 

523 synth_only=synth_only, 

524 from_impl=from_impl, 

525 impl_explore=self.impl_explore, 

526 ) 

527 

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

529 result.success = False 

530 return result 

531 

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

533 if self.report_logic_level_distribution: 

534 result.logic_level_distribution = self._get_logic_level_distribution( 

535 project_path, f"synth_{run_index}" 

536 ) 

537 

538 if not synth_only: 

539 if self.impl_explore: 

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

541 for run in runs_path.iterdir(): 

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

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

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

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

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

547 impl_folder = run 

548 run_name = run.resolve().name 

549 break 

550 else: 

551 run_name = f"impl_{run_index}" 

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

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

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

555 

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

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

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

559 

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

561 all_parameters.update(build_result=result) 

562 

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

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

565 result.success = False 

566 

567 return result 

568 

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

570 """ 

571 Open the project in Vivado GUI. 

572 

573 Arguments: 

574 project_path: A path containing a Vivado project. 

575 

576 Return: 

577 True if everything went well. 

578 """ 

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

580 

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

582 """ 

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

584 for the specified run. 

585 """ 

586 report_as_string = read_file( 

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

588 ) 

589 return HierarchicalUtilizationParser.get_size(report_as_string) 

590 

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

592 """ 

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

594 for the specified run. 

595 """ 

596 report_as_string = read_file( 

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

598 ) 

599 return LogicLevelDistributionParser.get_table(report_as_string) 

600 

601 def __str__(self) -> str: 

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

603 

604 if self.defined_at is not None: 

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

606 

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

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

609 

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

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

612 

613 if self.other_arguments: 

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

615 

616 return result 

617 

618 @staticmethod 

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

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

621 

622 

623class VivadoNetlistProject(VivadoProject): 

624 """ 

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

626 """ 

627 

628 def __init__( 

629 self, 

630 analyze_synthesis_timing: bool = False, 

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

632 **kwargs: Any, # noqa: ANN401 

633 ) -> None: 

634 """ 

635 Arguments: 

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

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

638 crossings and pulse width violations. 

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

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

641 using a constraint file. 

642 build_result_checkers: 

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

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

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

646 """ 

647 super().__init__(**kwargs) 

648 

649 self.is_netlist_build = True 

650 self.analyze_synthesis_timing = analyze_synthesis_timing 

651 self.report_logic_level_distribution = True 

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

653 

654 def build( 

655 self, 

656 **kwargs: Any, # noqa: ANN401 

657 ) -> BuildResult: 

658 """ 

659 Build the project. 

660 

661 Arguments: 

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

663 """ 

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

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

666 

667 return result 

668 

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

670 if not build_result.success: 

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

672 return False 

673 

674 success = True 

675 for build_result_checker in self.build_result_checkers: 

676 checker_result = build_result_checker.check(build_result) 

677 success = success and checker_result 

678 

679 return success 

680 

681 

682class VivadoIpCoreProject(VivadoProject): 

683 """ 

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

685 """ 

686 

687 ip_cores_only = True 

688 

689 def __init__( 

690 self, 

691 **kwargs: Any, # noqa: ANN401 

692 ) -> None: 

693 """ 

694 Arguments: 

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

696 """ 

697 super().__init__(**kwargs) 

698 

699 def build( 

700 self, 

701 **kwargs: Any, # noqa: ANN401 

702 ) -> NoReturn: 

703 """ 

704 Not implemented. 

705 """ 

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

707 

708 

709def copy_and_combine_dicts( 

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

711) -> dict[str, Any]: 

712 """ 

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

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

715 """ 

716 if dict_first is None: 

717 if dict_second is None: 

718 return {} 

719 

720 return dict_second.copy() 

721 

722 if dict_second is None: 

723 return dict_first.copy() 

724 

725 result = dict_first.copy() 

726 result.update(dict_second) 

727 

728 return result