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

208 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-07 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 

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 

199 if not self.analyze_synthesis_timing: 

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

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

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

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

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

205 # IP cores. 

206 self.build_step_hooks.append( 

207 BuildStepTclHook( 

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

209 ) 

210 ) 

211 

212 if self.report_logic_level_distribution: 

213 # Used by netlist builds 

214 self.build_step_hooks.append( 

215 BuildStepTclHook( 

216 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

217 "STEPS.SYNTH_DESIGN.TCL.POST", 

218 ) 

219 ) 

220 

221 def _create_tcl( 

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

223 ) -> Path: 

224 """ 

225 Make a TCL file that creates a Vivado project 

226 """ 

227 if project_path.exists(): 

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

229 project_path.mkdir(parents=True) 

230 

231 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

232 tcl = self.tcl.create( 

233 project_folder=project_path, 

234 modules=self.modules, 

235 part=self.part, 

236 top=self.top, 

237 run_index=self.default_run_index, 

238 generics=self.static_generics, 

239 constraints=self.constraints, 

240 tcl_sources=self.tcl_sources, 

241 build_step_hooks=self.build_step_hooks, 

242 ip_cache_path=ip_cache_path, 

243 disable_io_buffers=self.is_netlist_build, 

244 ip_cores_only=self.ip_cores_only, 

245 other_arguments=all_arguments, 

246 ) 

247 create_file(create_vivado_project_tcl, tcl) 

248 

249 return create_vivado_project_tcl 

250 

251 def create( 

252 self, 

253 project_path: Path, 

254 ip_cache_path: Optional[Path] = None, 

255 **other_arguments: Any, 

256 ) -> bool: 

257 """ 

258 Create a Vivado project 

259 

260 Arguments: 

261 project_path: Path where the project shall be placed. 

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

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

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

265 instead be sent to 

266 

267 * :func:`BaseModule.get_synthesis_files() 

268 <tsfpga.module.BaseModule.get_synthesis_files>` 

269 * :func:`BaseModule.get_ip_core_files() 

270 <tsfpga.module.BaseModule.get_ip_core_files>` 

271 * :func:`BaseModule.get_scoped_constraints() 

272 <tsfpga.module.BaseModule.get_scoped_constraints>` 

273 * :func:`VivadoProject.pre_create` 

274 

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

276 

277 .. note:: 

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

279 Return: 

280 True if everything went well. 

281 """ 

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

283 self._setup_tcl_sources() 

284 self._setup_build_step_hooks() 

285 

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

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

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

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

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

291 # an issue. 

292 self.modules = deepcopy(self.modules) 

293 

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

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

296 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

297 all_arguments.update( 

298 generics=self.static_generics, 

299 part=self.part, 

300 ) 

301 

302 if not self.pre_create( 

303 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

304 ): 

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

306 return False 

307 

308 create_vivado_project_tcl = self._create_tcl( 

309 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

310 ) 

311 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

312 

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

314 """ 

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

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

317 

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

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

320 Vivado system PATH. 

321 

322 .. Note:: 

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

324 this mechanism. 

325 

326 Arguments: 

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

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

329 

330 Return: 

331 True if everything went well. 

332 """ 

333 return True 

334 

335 def _build_tcl( 

336 self, 

337 project_path: Path, 

338 output_path: Path, 

339 num_threads: int, 

340 run_index: int, 

341 all_generics: dict[str, Any], 

342 synth_only: bool, 

343 from_impl: bool, 

344 impl_explore: bool, 

345 ) -> Path: 

346 """ 

347 Make a TCL file that builds a Vivado project 

348 """ 

349 project_file = self.project_file(project_path) 

350 if not project_file.exists(): 

351 raise ValueError( 

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

353 ) 

354 

355 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

356 tcl = self.tcl.build( 

357 project_file=project_file, 

358 output_path=output_path, 

359 num_threads=num_threads, 

360 run_index=run_index, 

361 generics=all_generics, 

362 synth_only=synth_only, 

363 from_impl=from_impl, 

364 analyze_synthesis_timing=self.analyze_synthesis_timing, 

365 impl_explore=impl_explore, 

366 ) 

367 create_file(build_vivado_project_tcl, tcl) 

368 

369 return build_vivado_project_tcl 

370 

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

372 """ 

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

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

375 

376 Arguments: 

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

378 parameters from the user. 

379 

380 Return: 

381 True if everything went well. 

382 """ 

383 return True 

384 

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

386 """ 

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

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

389 

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

391 material that shall be included in FPGA release artifacts. 

392 

393 .. Note:: 

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

395 this mechanism. 

396 

397 Arguments: 

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

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

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

401 utilization. 

402 

403 Return: 

404 True if everything went well. 

405 """ 

406 return True 

407 

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

409 self, 

410 project_path: Path, 

411 output_path: Optional[Path] = None, 

412 run_index: Optional[int] = None, 

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

414 synth_only: bool = False, 

415 from_impl: bool = False, 

416 num_threads: int = 12, 

417 **pre_and_post_build_parameters: Any, 

418 ) -> BuildResult: 

419 """ 

420 Build a Vivado project 

421 

422 Arguments: 

423 project_path: A path containing a Vivado project. 

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

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

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

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

428 

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

430 

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

432 synth_only: Run synthesis and then stop. 

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

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

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

436 but will instead be sent to 

437 

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

439 * :func:`VivadoProject.pre_build` 

440 * :func:`VivadoProject.post_build` 

441 

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

443 

444 .. note:: 

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

446 

447 Return: 

448 Result object with build information. 

449 """ 

450 synth_only = synth_only or self.is_netlist_build 

451 

452 if output_path is None and not synth_only: 

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

454 

455 if synth_only: 

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

457 else: 

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

459 

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

461 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

462 

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

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

465 

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

467 # over the static arguments. 

468 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

469 all_parameters.update( 

470 project_path=project_path, 

471 output_path=output_path, 

472 run_index=run_index, 

473 generics=all_generics, 

474 synth_only=synth_only, 

475 from_impl=from_impl, 

476 num_threads=num_threads, 

477 ) 

478 

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

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

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

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

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

484 # is not an issue. 

485 self.modules = deepcopy(self.modules) 

486 

487 result = BuildResult(self.name) 

488 

489 for module in self.modules: 

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

491 print( 

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

493 ) 

494 result.success = False 

495 return result 

496 

497 # Make sure register packages are up to date 

498 module.create_register_synthesis_files() 

499 

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

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

502 result.success = False 

503 return result 

504 

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

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

507 # not None in that case above. 

508 

509 build_vivado_project_tcl = self._build_tcl( 

510 project_path=project_path, 

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

512 num_threads=num_threads, 

513 run_index=run_index, 

514 all_generics=all_generics, 

515 synth_only=synth_only, 

516 from_impl=from_impl, 

517 impl_explore=self.impl_explore, 

518 ) 

519 

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

521 result.success = False 

522 return result 

523 

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

525 if self.report_logic_level_distribution: 

526 result.logic_level_distribution = self._get_logic_level_distribution( 

527 project_path, f"synth_{run_index}" 

528 ) 

529 

530 if not synth_only: 

531 if self.impl_explore: 

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

533 for run in runs_path.iterdir(): 

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

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

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

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

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

539 impl_folder = run 

540 run_name = run.resolve().name 

541 break 

542 else: 

543 run_name = f"impl_{run_index}" 

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

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

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

547 

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

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

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

551 

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

553 all_parameters.update(build_result=result) 

554 

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

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

557 result.success = False 

558 

559 return result 

560 

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

562 """ 

563 Open the project in Vivado GUI. 

564 

565 Arguments: 

566 project_path: A path containing a Vivado project. 

567 

568 Return: 

569 True if everything went well. 

570 """ 

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

572 

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

574 """ 

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

576 for the specified run. 

577 """ 

578 report_as_string = read_file( 

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

580 ) 

581 return HierarchicalUtilizationParser.get_size(report_as_string) 

582 

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

584 """ 

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

586 for the specified run. 

587 """ 

588 report_as_string = read_file( 

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

590 ) 

591 return LogicLevelDistributionParser.get_table(report_as_string) 

592 

593 def __str__(self) -> str: 

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

595 

596 if self.defined_at is not None: 

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

598 

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

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

601 

602 if self.static_generics: 

603 generics = self._dict_to_string(self.static_generics) 

604 else: 

605 generics = "-" 

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

607 

608 if self.other_arguments: 

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

610 

611 return result 

612 

613 @staticmethod 

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

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

616 

617 

618class VivadoNetlistProject(VivadoProject): 

619 """ 

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

621 """ 

622 

623 def __init__( 

624 self, 

625 analyze_synthesis_timing: bool = False, 

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

627 **kwargs: Any, 

628 ) -> None: 

629 """ 

630 Arguments: 

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

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

633 crossings and pulse width violations. 

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

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

636 using a constraint file. 

637 build_result_checkers: 

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

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

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

641 """ 

642 super().__init__(**kwargs) 

643 

644 self.is_netlist_build = True 

645 self.analyze_synthesis_timing = analyze_synthesis_timing 

646 self.report_logic_level_distribution = True 

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

648 

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

650 self, **kwargs: Any 

651 ) -> BuildResult: 

652 """ 

653 Build the project. 

654 

655 Arguments: 

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

657 """ 

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

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

660 

661 return result 

662 

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

664 if not build_result.success: 

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

666 return False 

667 

668 success = True 

669 for build_result_checker in self.build_result_checkers: 

670 checker_result = build_result_checker.check(build_result) 

671 success = success and checker_result 

672 

673 return success 

674 

675 

676class VivadoIpCoreProject(VivadoProject): 

677 """ 

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

679 """ 

680 

681 ip_cores_only = True 

682 

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

684 """ 

685 Arguments: 

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

687 """ 

688 super().__init__(**kwargs) 

689 

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

691 """ 

692 Not implemented. 

693 """ 

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

695 

696 

697def copy_and_combine_dicts( 

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

699) -> dict[str, Any]: 

700 """ 

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

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

703 """ 

704 if dict_first is None: 

705 if dict_second is None: 

706 return dict() 

707 

708 return dict_second.copy() 

709 

710 if dict_second is None: 

711 return dict_first.copy() 

712 

713 result = dict_first.copy() 

714 result.update(dict_second) 

715 

716 return result