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

209 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 20:52 +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 

9# Standard libraries 

10import shutil 

11from copy import deepcopy 

12from pathlib import Path 

13from typing import TYPE_CHECKING, Any, Optional, Union 

14 

15# First party libraries 

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 

21# Local folder libraries 

22from .build_result import BuildResult 

23from .common import run_vivado_gui, run_vivado_tcl 

24from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

25from .logic_level_distribution_parser import LogicLevelDistributionParser 

26from .tcl import VivadoTcl 

27 

28if TYPE_CHECKING: 

29 # First party libraries 

30 from tsfpga.module_list import ModuleList 

31 

32 # Local folder libraries 

33 from .build_result_checker import MaximumLogicLevel, SizeChecker 

34 

35 

36class VivadoProject: 

37 """ 

38 Used for handling a Xilinx Vivado HDL project 

39 """ 

40 

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

42 def __init__( 

43 self, 

44 name: str, 

45 modules: "ModuleList", 

46 part: str, 

47 top: Optional[str] = None, 

48 generics: Optional[dict[str, Any]] = None, 

49 constraints: Optional[list["Constraint"]] = None, 

50 tcl_sources: Optional[list[Path]] = None, 

51 build_step_hooks: Optional[list["BuildStepTclHook"]] = None, 

52 vivado_path: Optional[Path] = None, 

53 default_run_index: int = 1, 

54 impl_explore: bool = False, 

55 defined_at: Optional[Path] = None, 

56 **other_arguments: Any, 

57 ): # pylint: disable=too-many-locals 

58 """ 

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

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

61 

62 Arguments: 

63 name: Project name. 

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

65 part: Part identification. 

66 top: Name of top level entity. 

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

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

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

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

71 

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

73 

74 The generic value shall be of type 

75 

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

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

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

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

80 ``unsigned``, etc.), or 

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

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

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

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

85 vivado_path: A path to the Vivado executable. 

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

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

88 project. 

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

90 specify at build-time. 

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

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

93 projects set up. 

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

95 instead be passed on to 

96 

97 * :func:`BaseModule.get_synthesis_files() 

98 <tsfpga.module.BaseModule.get_synthesis_files>` 

99 * :func:`BaseModule.get_ip_core_files() 

100 <tsfpga.module.BaseModule.get_ip_core_files>` 

101 * :func:`BaseModule.get_scoped_constraints() 

102 <tsfpga.module.BaseModule.get_scoped_constraints>` 

103 * :func:`VivadoProject.pre_create` 

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

105 * :func:`VivadoProject.pre_build` 

106 * :func:`VivadoProject.post_build` 

107 

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

109 :meth:`.build`. 

110 

111 .. note:: 

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

113 """ 

114 self.name = name 

115 self.modules = modules.copy() 

116 self.part = part 

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

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

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

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

121 self._vivado_path = vivado_path 

122 self.default_run_index = default_run_index 

123 self.impl_explore = impl_explore 

124 self.defined_at = defined_at 

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

126 

127 # Will be set by subclass when applicable 

128 self.is_netlist_build = False 

129 self.analyze_synthesis_timing = True 

130 self.report_logic_level_distribution = False 

131 self.ip_cores_only = False 

132 

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

134 

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

136 

137 for constraint in self.constraints: 

138 if not isinstance(constraint, Constraint): 

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

140 

141 for tcl_source in self.tcl_sources: 

142 if not isinstance(tcl_source, Path): 

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

144 

145 for build_step_hook in self.build_step_hooks: 

146 if not isinstance(build_step_hook, BuildStepTclHook): 

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

148 

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

150 """ 

151 Arguments: 

152 project_path: A path containing a Vivado project. 

153 

154 Return: 

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

156 """ 

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

158 

159 def _setup_tcl_sources(self) -> None: 

160 tsfpga_tcl_sources = [ 

161 TSFPGA_TCL / "vivado_default_run.tcl", 

162 TSFPGA_TCL / "vivado_fast_run.tcl", 

163 TSFPGA_TCL / "vivado_messages.tcl", 

164 ] 

165 

166 if self.impl_explore: 

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

168 

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

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

171 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

172 

173 def _setup_build_step_hooks(self) -> None: 

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

175 # after implementation. 

176 self.build_step_hooks.append( 

177 BuildStepTclHook( 

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

179 ) 

180 ) 

181 self.build_step_hooks.append( 

182 BuildStepTclHook( 

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

184 ) 

185 ) 

186 

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

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

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

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

191 # a post-synthesis hook. 

192 self.build_step_hooks.append( 

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

194 ) 

195 self.build_step_hooks.append( 

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

197 ) 

198 self.build_step_hooks.append( 

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

200 ) 

201 

202 if not self.analyze_synthesis_timing: 

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

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

205 # 'build_vivado_project.tcl' script. 

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

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

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

209 # IP cores. 

210 self.build_step_hooks.append( 

211 BuildStepTclHook( 

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

213 ) 

214 ) 

215 

216 if self.report_logic_level_distribution: 

217 # Used by netlist builds 

218 self.build_step_hooks.append( 

219 BuildStepTclHook( 

220 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

221 "STEPS.SYNTH_DESIGN.TCL.POST", 

222 ) 

223 ) 

224 

225 def _create_tcl( 

226 self, project_path: Path, ip_cache_path: Optional[Path], all_arguments: dict[str, Any] 

227 ) -> Path: 

228 """ 

229 Make a TCL file that creates a Vivado project 

230 """ 

231 if project_path.exists(): 

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

233 project_path.mkdir(parents=True) 

234 

235 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

236 tcl = self.tcl.create( 

237 project_folder=project_path, 

238 modules=self.modules, 

239 part=self.part, 

240 top=self.top, 

241 run_index=self.default_run_index, 

242 generics=self.static_generics, 

243 constraints=self.constraints, 

244 tcl_sources=self.tcl_sources, 

245 build_step_hooks=self.build_step_hooks, 

246 ip_cache_path=ip_cache_path, 

247 disable_io_buffers=self.is_netlist_build, 

248 ip_cores_only=self.ip_cores_only, 

249 other_arguments=all_arguments, 

250 ) 

251 create_file(create_vivado_project_tcl, tcl) 

252 

253 return create_vivado_project_tcl 

254 

255 def create( 

256 self, 

257 project_path: Path, 

258 ip_cache_path: Optional[Path] = None, 

259 **other_arguments: Any, 

260 ) -> bool: 

261 """ 

262 Create a Vivado project 

263 

264 Arguments: 

265 project_path: Path where the project shall be placed. 

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

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

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

269 instead be sent to 

270 

271 * :func:`BaseModule.get_synthesis_files() 

272 <tsfpga.module.BaseModule.get_synthesis_files>` 

273 * :func:`BaseModule.get_ip_core_files() 

274 <tsfpga.module.BaseModule.get_ip_core_files>` 

275 * :func:`BaseModule.get_scoped_constraints() 

276 <tsfpga.module.BaseModule.get_scoped_constraints>` 

277 * :func:`VivadoProject.pre_create` 

278 

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

280 

281 .. note:: 

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

283 Return: 

284 True if everything went well. 

285 """ 

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

287 self._setup_tcl_sources() 

288 self._setup_build_step_hooks() 

289 

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

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

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

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

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

295 # an issue. 

296 self.modules = deepcopy(self.modules) 

297 

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

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

300 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

301 all_arguments.update( 

302 generics=self.static_generics, 

303 part=self.part, 

304 ) 

305 

306 if not self.pre_create( 

307 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

308 ): 

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

310 return False 

311 

312 create_vivado_project_tcl = self._create_tcl( 

313 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

314 ) 

315 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

316 

317 def pre_create(self, **kwargs: Any) -> bool: # pylint: disable=unused-argument 

318 """ 

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

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

321 

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

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

324 Vivado system PATH. 

325 

326 .. Note:: 

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

328 this mechanism. 

329 

330 Arguments: 

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

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

333 

334 Return: 

335 True if everything went well. 

336 """ 

337 return True 

338 

339 def _build_tcl( 

340 self, 

341 project_path: Path, 

342 output_path: Path, 

343 num_threads: int, 

344 run_index: int, 

345 all_generics: dict[str, Any], 

346 synth_only: bool, 

347 from_impl: bool, 

348 impl_explore: bool, 

349 ) -> Path: 

350 """ 

351 Make a TCL file that builds a Vivado project 

352 """ 

353 project_file = self.project_file(project_path) 

354 if not project_file.exists(): 

355 raise ValueError( 

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

357 ) 

358 

359 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

360 tcl = self.tcl.build( 

361 project_file=project_file, 

362 output_path=output_path, 

363 num_threads=num_threads, 

364 run_index=run_index, 

365 generics=all_generics, 

366 synth_only=synth_only, 

367 from_impl=from_impl, 

368 analyze_synthesis_timing=self.analyze_synthesis_timing, 

369 impl_explore=impl_explore, 

370 ) 

371 create_file(build_vivado_project_tcl, tcl) 

372 

373 return build_vivado_project_tcl 

374 

375 def pre_build(self, **kwargs: Any) -> bool: # pylint: disable=unused-argument 

376 """ 

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

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

379 

380 Arguments: 

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

382 parameters from the user. 

383 

384 Return: 

385 True if everything went well. 

386 """ 

387 return True 

388 

389 def post_build(self, **kwargs: Any) -> bool: # pylint: disable=unused-argument 

390 """ 

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

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

393 

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

395 material that shall be included in FPGA release artifacts. 

396 

397 .. Note:: 

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

399 this mechanism. 

400 

401 Arguments: 

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

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

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

405 utilization. 

406 

407 Return: 

408 True if everything went well. 

409 """ 

410 return True 

411 

412 def build( # pylint: disable=too-many-locals,too-many-branches 

413 self, 

414 project_path: Path, 

415 output_path: Optional[Path] = None, 

416 run_index: Optional[int] = None, 

417 generics: Optional[dict[str, Any]] = None, 

418 synth_only: bool = False, 

419 from_impl: bool = False, 

420 num_threads: int = 12, 

421 **pre_and_post_build_parameters: Any, 

422 ) -> BuildResult: 

423 """ 

424 Build a Vivado project 

425 

426 Arguments: 

427 project_path: A path containing a Vivado project. 

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

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

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

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

432 

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

434 

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

436 synth_only: Run synthesis and then stop. 

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

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

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

440 but will instead be sent to 

441 

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

443 * :func:`VivadoProject.pre_build` 

444 * :func:`VivadoProject.post_build` 

445 

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

447 

448 .. note:: 

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

450 

451 Return: 

452 Result object with build information. 

453 """ 

454 synth_only = synth_only or self.is_netlist_build 

455 

456 if output_path is None and not synth_only: 

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

458 

459 if synth_only: 

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

461 else: 

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

463 

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

465 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

466 

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

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

469 

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

471 # over the static arguments. 

472 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

473 all_parameters.update( 

474 project_path=project_path, 

475 output_path=output_path, 

476 run_index=run_index, 

477 generics=all_generics, 

478 synth_only=synth_only, 

479 from_impl=from_impl, 

480 num_threads=num_threads, 

481 ) 

482 

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

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

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

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

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

488 # is not an issue. 

489 self.modules = deepcopy(self.modules) 

490 

491 result = BuildResult(self.name) 

492 

493 for module in self.modules: 

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

495 print( 

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

497 ) 

498 result.success = False 

499 return result 

500 

501 # Make sure register packages are up to date 

502 module.create_register_synthesis_files() 

503 

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

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

506 result.success = False 

507 return result 

508 

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

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

511 # not None in that case above. 

512 

513 build_vivado_project_tcl = self._build_tcl( 

514 project_path=project_path, 

515 output_path=output_path, # type: ignore[arg-type] 

516 num_threads=num_threads, 

517 run_index=run_index, 

518 all_generics=all_generics, 

519 synth_only=synth_only, 

520 from_impl=from_impl, 

521 impl_explore=self.impl_explore, 

522 ) 

523 

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

525 result.success = False 

526 return result 

527 

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

529 if self.report_logic_level_distribution: 

530 result.logic_level_distribution = self._get_logic_level_distribution( 

531 project_path, f"synth_{run_index}" 

532 ) 

533 

534 if not synth_only: 

535 if self.impl_explore: 

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

537 for run in runs_path.iterdir(): 

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

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

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

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

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

543 impl_folder = run 

544 run_name = run.resolve().name 

545 break 

546 else: 

547 run_name = f"impl_{run_index}" 

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

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

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

551 

552 shutil.copy2(bit_file, output_path / f"{self.name}.bit") # type: ignore[operator] 

553 shutil.copy2(bin_file, output_path / f"{self.name}.bin") # type: ignore[operator] 

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

555 

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

557 all_parameters.update(build_result=result) 

558 

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

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

561 result.success = False 

562 

563 return result 

564 

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

566 """ 

567 Open the project in Vivado GUI. 

568 

569 Arguments: 

570 project_path: A path containing a Vivado project. 

571 

572 Return: 

573 True if everything went well. 

574 """ 

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

576 

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

578 """ 

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

580 for the specified run. 

581 """ 

582 report_as_string = read_file( 

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

584 ) 

585 return HierarchicalUtilizationParser.get_size(report_as_string) 

586 

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

588 """ 

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

590 for the specified run. 

591 """ 

592 report_as_string = read_file( 

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

594 ) 

595 return LogicLevelDistributionParser.get_table(report_as_string) 

596 

597 def __str__(self) -> str: 

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

599 

600 if self.defined_at is not None: 

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

602 

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

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

605 

606 if self.static_generics: 

607 generics = self._dict_to_string(self.static_generics) 

608 else: 

609 generics = "-" 

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

611 

612 if self.other_arguments: 

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

614 

615 return result 

616 

617 @staticmethod 

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

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

620 

621 

622class VivadoNetlistProject(VivadoProject): 

623 """ 

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

625 """ 

626 

627 def __init__( 

628 self, 

629 analyze_synthesis_timing: bool = False, 

630 build_result_checkers: Optional[list[Union["SizeChecker", "MaximumLogicLevel"]]] = None, 

631 **kwargs: Any, 

632 ) -> None: 

633 """ 

634 Arguments: 

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

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

637 crossings and pulse width violations. 

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

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

640 using a constraint file. 

641 build_result_checkers: 

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

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

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

645 """ 

646 super().__init__(**kwargs) 

647 

648 self.is_netlist_build = True 

649 self.analyze_synthesis_timing = analyze_synthesis_timing 

650 self.report_logic_level_distribution = True 

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

652 

653 def build( # type: ignore # pylint: disable=arguments-differ 

654 self, **kwargs: Any 

655 ) -> BuildResult: 

656 """ 

657 Build the project. 

658 

659 Arguments: 

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

661 """ 

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

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

664 

665 return result 

666 

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

668 if not build_result.success: 

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

670 return False 

671 

672 success = True 

673 for build_result_checker in self.build_result_checkers: 

674 checker_result = build_result_checker.check(build_result) 

675 success = success and checker_result 

676 

677 return success 

678 

679 

680class VivadoIpCoreProject(VivadoProject): 

681 """ 

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

683 """ 

684 

685 ip_cores_only = True 

686 

687 def __init__(self, **kwargs: Any) -> None: 

688 """ 

689 Arguments: 

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

691 """ 

692 super().__init__(**kwargs) 

693 

694 def build(self, **kwargs: Any): # type: ignore # pylint: disable=arguments-differ 

695 """ 

696 Not implemented. 

697 """ 

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

699 

700 

701def copy_and_combine_dicts( 

702 dict_first: Optional[dict[str, Any]], dict_second: Optional[dict[str, Any]] 

703) -> dict[str, Any]: 

704 """ 

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

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

707 """ 

708 if dict_first is None: 

709 if dict_second is None: 

710 return dict() 

711 

712 return dict_second.copy() 

713 

714 if dict_second is None: 

715 return dict_first.copy() 

716 

717 result = dict_first.copy() 

718 result.update(dict_second) 

719 

720 return result