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

269 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-17 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 contextlib 

12import re 

13import shutil 

14from copy import deepcopy 

15from pathlib import Path 

16from typing import TYPE_CHECKING, Any, NoReturn 

17 

18from tsfpga import TSFPGA_TCL 

19from tsfpga.build_step_tcl_hook import BuildStepTclHook 

20from tsfpga.constraint import Constraint 

21from tsfpga.hdl_file import HdlFile 

22from tsfpga.system_utils import create_file, read_file 

23 

24from .build_result import BuildResult 

25from .common import run_vivado_gui, run_vivado_tcl, to_tcl_path 

26from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

27from .logic_level_distribution_parser import LogicLevelDistributionParser 

28from .tcl import VivadoTcl 

29from .timing_parser import FoundNoSlackError, TimingParser 

30 

31if TYPE_CHECKING: 

32 from tsfpga.module_list import ModuleList 

33 from tsfpga.vivado.generics import BitVectorGenericValue, StringGenericValue 

34 

35 from .build_result_checker import MaximumLogicLevel, SizeChecker 

36 

37 

38class VivadoProject: 

39 """ 

40 Used for handling a Xilinx Vivado HDL project 

41 """ 

42 

43 def __init__( # noqa: PLR0913 

44 self, 

45 name: str, 

46 modules: ModuleList, 

47 part: str, 

48 top: str | None = None, 

49 generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue] 

50 | None = None, 

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

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

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

54 vivado_path: Path | None = None, 

55 default_run_index: int = 1, 

56 impl_explore: bool = False, 

57 defined_at: Path | None = None, 

58 **other_arguments: Any, # noqa: ANN401 

59 ) -> None: 

60 """ 

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

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

63 

64 Arguments: 

65 name: Project name. 

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

67 part: Part identification. 

68 top: Name of top level entity. 

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

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

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

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

73 

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

75 

76 The generic value shall be of type 

77 

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

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

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

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

82 ``unsigned``, etc.), or 

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

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

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

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

87 vivado_path: A path to the Vivado executable. 

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

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

90 project. 

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

92 specify at build-time. 

93 impl_explore: Run multiple implementation strategies in parallel. 

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

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

96 projects set up. 

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

98 instead be passed on to 

99 

100 * :func:`BaseModule.get_synthesis_files() 

101 <tsfpga.module.BaseModule.get_synthesis_files>` 

102 * :func:`BaseModule.get_ip_core_files() 

103 <tsfpga.module.BaseModule.get_ip_core_files>` 

104 * :func:`BaseModule.get_scoped_constraints() 

105 <tsfpga.module.BaseModule.get_scoped_constraints>` 

106 * :func:`VivadoProject.pre_create` 

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

108 * :func:`VivadoProject.pre_build` 

109 * :func:`VivadoProject.post_build` 

110 

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

112 :meth:`.build`. 

113 

114 .. note:: 

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

116 """ 

117 self.name = name 

118 self.modules = modules.copy() 

119 self.part = part 

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

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

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

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

124 self._vivado_path = vivado_path 

125 self.default_run_index = default_run_index 

126 self.impl_explore = impl_explore 

127 self.defined_at = defined_at 

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

129 

130 # Will be set by subclass when applicable 

131 self.is_netlist_build = False 

132 self.open_and_analyze_synthesized_design = True 

133 self.ip_cores_only = False 

134 

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

136 

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

138 

139 for constraint in self.constraints: 

140 if not isinstance(constraint, Constraint): 

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

142 

143 for tcl_source in self.tcl_sources: 

144 if not isinstance(tcl_source, Path): 

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

146 

147 for build_step_hook in self.build_step_hooks: 

148 if not isinstance(build_step_hook, BuildStepTclHook): 

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

150 

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

152 """ 

153 Arguments: 

154 project_path: A path containing a Vivado project. 

155 

156 Return: 

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

158 """ 

159 return project_path / f"{self.name}.xpr" 

160 

161 def _setup_tcl_sources(self) -> None: 

162 tsfpga_tcl_sources = [ 

163 TSFPGA_TCL / "vivado_default_run.tcl", 

164 TSFPGA_TCL / "vivado_fast_run.tcl", 

165 TSFPGA_TCL / "vivado_messages.tcl", 

166 ] 

167 

168 if self.impl_explore: 

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

170 

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

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

173 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

174 

175 def _setup_and_create_build_step_hooks( 

176 self, project_path: Path 

177 ) -> dict[str, tuple[Path, list[BuildStepTclHook]]]: 

178 """ 

179 Add all necessary tsfpga build step hooks to the list of hooks supplied by the user. 

180 Create the TCL files for these hooks in the project folder. 

181 """ 

182 # Shallow copy so that we do not append the state of this object. 

183 # If this method is called twice, once at create-time and once at build-time, we do not 

184 # want duplicates. 

185 build_step_hooks = self.build_step_hooks.copy() 

186 

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

188 # after implementation. 

189 build_step_hooks.append( 

190 BuildStepTclHook( 

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

192 ) 

193 ) 

194 build_step_hooks.append( 

195 BuildStepTclHook( 

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

197 ) 

198 ) 

199 

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

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

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

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

204 # a post-synthesis hook. 

205 build_step_hooks.append( 

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

207 ) 

208 build_step_hooks.append( 

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

210 ) 

211 build_step_hooks.append( 

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

213 ) 

214 

215 if not self.open_and_analyze_synthesized_design: 

216 # In this special case, used only by the fastest netlist builds, the synthesized design 

217 # is never opened (to save execution time). 

218 # So in order to get access to some design metrics, we need to add hooks instead. 

219 

220 # Note that this report is does not report numbers from IP cores within the design, 

221 # when the report is generated via a hook. 

222 # But since this mode is used exclusively by netlist builds, which very rarely include 

223 # IP cores, this is deemed acceptable on order to save time. 

224 build_step_hooks.append( 

225 BuildStepTclHook( 

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

227 ) 

228 ) 

229 

230 # Note that this report is better if generated on an open design, since it will then 

231 # list each clock domain separately. 

232 # If done like this with a hook, all paths will be on one line, no matter which clock 

233 # domain they belong to. 

234 build_step_hooks.append( 

235 BuildStepTclHook( 

236 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

237 "STEPS.SYNTH_DESIGN.TCL.POST", 

238 ) 

239 ) 

240 

241 organized_build_step_hooks = self._organize_build_step_hooks( 

242 build_step_hooks=build_step_hooks, project_folder=project_path 

243 ) 

244 self._create_build_step_hook_files(build_step_hooks=organized_build_step_hooks) 

245 

246 return organized_build_step_hooks 

247 

248 @staticmethod 

249 def _organize_build_step_hooks( 

250 build_step_hooks: list[BuildStepTclHook], project_folder: Path 

251 ) -> dict[str, tuple[Path, list[BuildStepTclHook]]]: 

252 """ 

253 Since there can be many hooks for the same step, reorganize them into a dict: 

254 {step name: (script file in project, [list of hooks for that step])} 

255 

256 Vivado will only accept one TCL script as hook for each step. 

257 So if we want to add more we have to create a new TCL file, that sources the other files, 

258 and add that as the hook to Vivado. 

259 """ 

260 result = {} 

261 for build_step_hook in build_step_hooks: 

262 if build_step_hook.hook_step in result: 

263 result[build_step_hook.hook_step][1].append(build_step_hook) 

264 else: 

265 tcl_file = project_folder / ( 

266 "hook_" + build_step_hook.hook_step.replace(".", "_") + ".tcl" 

267 ) 

268 result[build_step_hook.hook_step] = (tcl_file, [build_step_hook]) 

269 

270 return result 

271 

272 def _create_build_step_hook_files( 

273 self, build_step_hooks: dict[str, tuple[Path, list[BuildStepTclHook]]] 

274 ) -> None: 

275 for step_name, (tcl_file, hooks) in build_step_hooks.items(): 

276 source_hooks_tcl = "\n".join( 

277 [f"source {{{to_tcl_path(hook.tcl_file)}}}" for hook in hooks] 

278 ) 

279 create_file( 

280 tcl_file, 

281 f"""\ 

282# ------------------------------------------------------------------------------ 

283# Hook script for the "{step_name}" build step. 

284# This file is auto-generated by tsfpga. Do not edit manually. 

285{source_hooks_tcl} 

286""", 

287 ) 

288 

289 def _create_tcl( 

290 self, 

291 project_path: Path, 

292 ip_cache_path: Path | None, 

293 build_step_hooks: dict[str, tuple[Path, list[BuildStepTclHook]]], 

294 all_arguments: dict[str, Any], 

295 ) -> Path: 

296 """ 

297 Make a TCL file that creates a Vivado project 

298 """ 

299 project_file = self.project_file(project_path=project_path) 

300 if project_file.exists(): 

301 raise ValueError(f'Project "{self.name}" already exists: {project_file}') 

302 project_path.mkdir(parents=True, exist_ok=True) 

303 

304 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

305 tcl = self.tcl.create( 

306 project_folder=project_path, 

307 modules=self.modules, 

308 part=self.part, 

309 top=self.top, 

310 run_index=self.default_run_index, 

311 generics=self.static_generics, 

312 constraints=self.constraints, 

313 tcl_sources=self.tcl_sources, 

314 build_step_hooks=build_step_hooks, 

315 ip_cache_path=ip_cache_path, 

316 disable_io_buffers=self.is_netlist_build, 

317 ip_cores_only=self.ip_cores_only, 

318 other_arguments=all_arguments, 

319 ) 

320 create_file(create_vivado_project_tcl, tcl) 

321 

322 return create_vivado_project_tcl 

323 

324 def create( 

325 self, 

326 project_path: Path, 

327 ip_cache_path: Path | None = None, 

328 **other_arguments: Any, # noqa: ANN401 

329 ) -> bool: 

330 """ 

331 Create a Vivado project 

332 

333 Arguments: 

334 project_path: Path where the project shall be placed. 

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

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

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

338 instead be sent to 

339 

340 * :func:`BaseModule.get_synthesis_files() 

341 <tsfpga.module.BaseModule.get_synthesis_files>` 

342 * :func:`BaseModule.get_ip_core_files() 

343 <tsfpga.module.BaseModule.get_ip_core_files>` 

344 * :func:`BaseModule.get_scoped_constraints() 

345 <tsfpga.module.BaseModule.get_scoped_constraints>` 

346 * :func:`VivadoProject.pre_create` 

347 

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

349 

350 .. note:: 

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

352 

353 Return: 

354 True if everything went well. 

355 """ 

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

357 self._setup_tcl_sources() 

358 build_step_hooks = self._setup_and_create_build_step_hooks(project_path=project_path) 

359 

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

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

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

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

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

365 # an issue. 

366 self.modules = deepcopy(self.modules) 

367 

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

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

370 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

371 all_arguments.update(generics=self.static_generics, part=self.part) 

372 

373 if not self.pre_create( 

374 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

375 ): 

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

377 return False 

378 

379 create_vivado_project_tcl = self._create_tcl( 

380 project_path=project_path, 

381 ip_cache_path=ip_cache_path, 

382 build_step_hooks=build_step_hooks, 

383 all_arguments=all_arguments, 

384 ) 

385 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

386 

387 def pre_create( 

388 self, 

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

390 ) -> bool: 

391 """ 

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

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

394 

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

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

397 Vivado system PATH. 

398 

399 .. Note:: 

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

401 this mechanism. 

402 

403 Arguments: 

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

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

406 

407 Return: 

408 True if everything went well. 

409 """ 

410 return True 

411 

412 def _build_tcl( # noqa: PLR0913 

413 self, 

414 project_path: Path, 

415 output_path: Path | None, 

416 num_threads: int, 

417 run_index: int, 

418 all_generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue], 

419 synth_only: bool, 

420 from_impl: bool, 

421 impl_explore: bool, 

422 ) -> Path: 

423 """ 

424 Make a TCL file that builds a Vivado project 

425 """ 

426 project_file = self.project_file(project_path=project_path) 

427 if not project_file.exists(): 

428 raise ValueError( 

429 f'Project "{self.name}" does not exist in the specified location: {project_file}' 

430 ) 

431 

432 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

433 tcl = self.tcl.build( 

434 project_file=project_file, 

435 output_path=output_path, 

436 num_threads=num_threads, 

437 run_index=run_index, 

438 generics=all_generics, 

439 synth_only=synth_only, 

440 from_impl=from_impl, 

441 open_and_analyze_synthesized_design=self.open_and_analyze_synthesized_design, 

442 impl_explore=impl_explore, 

443 ) 

444 create_file(build_vivado_project_tcl, tcl) 

445 

446 return build_vivado_project_tcl 

447 

448 def pre_build( 

449 self, 

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

451 ) -> bool: 

452 """ 

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

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

455 

456 Arguments: 

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

458 parameters from the user. 

459 

460 Return: 

461 True if everything went well. 

462 """ 

463 return True 

464 

465 def post_build( 

466 self, 

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

468 ) -> bool: 

469 """ 

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

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

472 

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

474 material that shall be included in FPGA release artifacts. 

475 

476 .. Note:: 

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

478 this mechanism. 

479 

480 Arguments: 

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

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

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

484 utilization. 

485 

486 Return: 

487 True if everything went well. 

488 """ 

489 return True 

490 

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

492 self, 

493 project_path: Path, 

494 output_path: Path | None = None, 

495 run_index: int | None = None, 

496 generics: dict[str, bool | float | StringGenericValue | BitVectorGenericValue] 

497 | None = None, 

498 synth_only: bool = False, 

499 from_impl: bool = False, 

500 num_threads: int = 12, 

501 **pre_and_post_build_parameters: Any, # noqa: ANN401 

502 ) -> BuildResult: 

503 """ 

504 Build a Vivado project 

505 

506 Arguments: 

507 project_path: A path containing a Vivado project. 

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

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

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

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

512 

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

514 

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

516 synth_only: Run synthesis and then stop. 

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

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

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

520 but will instead be sent to 

521 

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

523 * :func:`VivadoProject.pre_build` 

524 * :func:`VivadoProject.post_build` 

525 

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

527 

528 .. note:: 

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

530 

531 Return: 

532 Result object with build information. 

533 """ 

534 synth_only = synth_only or self.is_netlist_build 

535 

536 if synth_only: 

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

538 else: 

539 if output_path is None: 

540 raise ValueError("Must specify 'output_path' when doing an implementation build.") 

541 

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

543 

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

545 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

546 

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

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

549 

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

551 # over the static arguments. 

552 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

553 all_parameters.update( 

554 project_path=project_path, 

555 output_path=output_path, 

556 run_index=run_index, 

557 generics=all_generics, 

558 synth_only=synth_only, 

559 from_impl=from_impl, 

560 num_threads=num_threads, 

561 ) 

562 

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

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

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

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

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

568 # is not an issue. 

569 self.modules = deepcopy(self.modules) 

570 

571 result = BuildResult(name=self.name, synthesis_run_name=f"synth_{run_index}") 

572 

573 for module in self.modules: 

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

575 print( 

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

577 ) 

578 result.success = False 

579 return result 

580 

581 # Make sure register packages are up to date 

582 module.create_register_synthesis_files() 

583 

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

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

586 result.success = False 

587 return result 

588 

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

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

591 # not None in that case above. 

592 

593 build_vivado_project_tcl = self._build_tcl( 

594 project_path=project_path, 

595 output_path=output_path, 

596 num_threads=num_threads, 

597 run_index=run_index, 

598 all_generics=all_generics, 

599 synth_only=synth_only, 

600 from_impl=from_impl, 

601 impl_explore=self.impl_explore, 

602 ) 

603 

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

605 result.success = False 

606 return result 

607 

608 result.synthesis_size = self._get_size( 

609 project_path=project_path, run_name=f"synth_{run_index}" 

610 ) 

611 

612 if not synth_only: 

613 if self.impl_explore: 

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

615 for run_path in runs_path.iterdir(): 

616 if "impl_explore_" in run_path.name: 

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

618 bit_file = run_path / f"{self.top}.bit" 

619 bin_file = run_path / f"{self.top}.bin" 

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

621 result.implementation_run_name = run_path.name 

622 break 

623 else: 

624 result.implementation_run_name = f"impl_{run_index}" 

625 impl_folder = project_path / f"{self.name}.runs" / result.implementation_run_name 

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

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

628 

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

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

631 result.implementation_size = self._get_size( 

632 project_path=project_path, run_name=result.implementation_run_name 

633 ) 

634 

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

636 all_parameters.update(build_result=result) 

637 

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

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

640 result.success = False 

641 

642 return result 

643 

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

645 """ 

646 Open the project in Vivado GUI. 

647 

648 Arguments: 

649 project_path: A path containing a Vivado project. 

650 

651 Return: 

652 True if everything went well. 

653 """ 

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

655 

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

657 """ 

658 Read the hierarchical utilization report and return the top level size 

659 for the specified run. 

660 """ 

661 report_as_string = read_file( 

662 project_path / f"{self.name}.runs" / run_name / "hierarchical_utilization.rpt" 

663 ) 

664 return HierarchicalUtilizationParser.get_size(report_as_string) 

665 

666 def __str__(self) -> str: 

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

668 

669 if self.defined_at is not None: 

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

671 

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

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

674 

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

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

677 

678 if self.other_arguments: 

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

680 

681 return result 

682 

683 @staticmethod 

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

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

686 

687 

688class VivadoNetlistProject(VivadoProject): 

689 """ 

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

691 """ 

692 

693 _clock_period_ns = 2.0 

694 

695 def __init__( 

696 self, 

697 analyze_synthesis_timing: bool = False, 

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

699 **kwargs: Any, # noqa: ANN401 

700 ) -> None: 

701 """ 

702 Arguments: 

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

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

705 crossings, pulse width violations, etc, and calculate a maximum frequency estimate. 

706 

707 .. note:: 

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

709 build_result_checkers: 

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

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

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

713 """ 

714 super().__init__(**kwargs) 

715 

716 self.is_netlist_build = True 

717 self.open_and_analyze_synthesized_design = analyze_synthesis_timing 

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

719 

720 def create( 

721 self, 

722 project_path: Path, 

723 **kwargs: Any, # noqa: ANN401 

724 ) -> bool: 

725 """ 

726 Create the project. 

727 

728 Arguments: 

729 project_path: Path where the project shall be placed. 

730 kwargs: All arguments as accepted by :meth:`.VivadoProject.create`. 

731 """ 

732 # Create and add a TCL for auto-creating clocks. 

733 # Whether it is used or not depends on settings, but note that these settings can 

734 # change between subsequent builds, so we need to always have the file in place and be part 

735 # of the project. 

736 tcl_path = create_file( 

737 self._get_auto_clock_constraint_path(project_path=project_path), contents="# Unused.\n" 

738 ) 

739 # Add it "early" so that any other user constraints that might be in place 

740 # can override the clocks. 

741 self.constraints.append(Constraint(file=tcl_path, processing_order="early")) 

742 

743 if self.open_and_analyze_synthesized_design: 

744 self._set_auto_clock_constraint(tcl_path=tcl_path) 

745 

746 return super().create(project_path=project_path, **kwargs) 

747 

748 def build( 

749 self, 

750 project_path: Path, 

751 **kwargs: Any, # noqa: ANN401 

752 ) -> BuildResult: 

753 """ 

754 Build the project. 

755 

756 Arguments: 

757 project_path: A path containing a Vivado project. 

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

759 """ 

760 # Update hook script files, since the user might turn on and off the 

761 # 'analyze_synthesis_timing' flag, which affects what scripts are added to the 

762 # post-synthesis hook step. 

763 self._setup_and_create_build_step_hooks(project_path=project_path) 

764 

765 if self.open_and_analyze_synthesized_design: 

766 # Update, since the HDL (and details about clocks) might have changed since last time. 

767 self._set_auto_clock_constraint( 

768 tcl_path=self._get_auto_clock_constraint_path(project_path=project_path) 

769 ) 

770 

771 result = super().build(project_path=project_path, **kwargs) 

772 

773 if not result.success: 

774 print(f'Can not do post-build check for "{self.name}" since it did not succeed.') 

775 return result 

776 

777 result.success = result.success and self._check_size(build_result=result) 

778 

779 run_path = project_path / f"{self.name}.runs" / result.synthesis_run_name 

780 

781 if self.open_and_analyze_synthesized_design: 

782 # Report might not exist or might not contain any slack information, 

783 # if we could not auto detect any clocks. 

784 # Could happen if the top-level file is Verilog, or if there are no clocks at all, 

785 # or if our auto-detect failed. 

786 with contextlib.suppress(FileNotFoundError, FoundNoSlackError): 

787 slack_ns = TimingParser.get_slack_ns(read_file(run_path / "timing.rpt")) 

788 # Positive slack = margin, meaning we can use a lower period, 

789 # meaning higher frequency. 

790 # Hence the subtraction. 

791 result.maximum_synthesis_frequency_hz = 1e9 / (self._clock_period_ns - slack_ns) 

792 

793 result.logic_level_distribution = self._get_logic_level_distribution(run_path=run_path) 

794 

795 return result 

796 

797 def _get_auto_clock_constraint_path(self, project_path: Path) -> Path: 

798 return project_path / f"auto_create_{self.top}_clocks.tcl" 

799 

800 def _set_auto_clock_constraint(self, tcl_path: Path) -> None: 

801 # Try to auto-detect clocks in the top-level file, and create them automatically. 

802 top_file = self._find_top_level_file() 

803 clock_names = ( 

804 self._find_vhdl_clock_names(vhdl_file=top_file) 

805 if top_file.path.suffix.lower() in HdlFile.file_endings_mapping[HdlFile.Type.VHDL] 

806 else [] 

807 ) 

808 

809 if clock_names: 

810 create_clock_tcl = "\n".join( 

811 [ 

812 f'create_clock -name "{clock_name}" -period {self._clock_period_ns} ' 

813 f'[get_ports "{clock_name}"];' 

814 for clock_name in clock_names 

815 ] 

816 ) 

817 tcl = f"""\ 

818# Auto-create the clocks found in the top-level HDL file: 

819# {top_file.path} 

820{create_clock_tcl} 

821""" 

822 create_file(tcl_path, tcl) 

823 

824 def _find_top_level_file(self) -> HdlFile: 

825 top_files = [ 

826 hdl_file 

827 for module in self.modules 

828 for hdl_file in module.get_synthesis_files() 

829 if hdl_file.path.stem == self.top 

830 ] 

831 if len(top_files) == 0: 

832 raise ValueError( 

833 f'Could not find HDL source file corresponding to top-level "{self.top}".' 

834 ) 

835 if len(top_files) > 1: 

836 raise ValueError( 

837 f"Found multiple HDL source files corresponding to " 

838 f'top-level "{self.top}": {top_files}.' 

839 ) 

840 

841 return top_files[0] 

842 

843 def _find_vhdl_clock_names(self, vhdl_file: HdlFile) -> list[str]: 

844 """ 

845 Find a list of all clock port names in the VHDL file. 

846 It handles all ports that contain "clk" or "clock" in their name as clocks. 

847 This magic word can be either in the beginning, middle or end of the port name 

848 (separated by underscore). 

849 """ 

850 top_vhd = read_file(vhdl_file.path) 

851 

852 entity_matches = re.findall( 

853 rf"^\s*entity\s+{self.top}\s+is(.+)^\s*end\s+entity", 

854 top_vhd, 

855 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

856 ) 

857 if len(entity_matches) == 0: 

858 raise ValueError( 

859 f'Could not find "{self.top}" entity declaration in "{vhdl_file.path}".' 

860 ) 

861 if len(entity_matches) > 1: 

862 raise ValueError( 

863 f'Found multiple "{self.top}" entity declarations in "{vhdl_file.path}".' 

864 ) 

865 

866 entity_vhd = entity_matches[0] 

867 port_matches = re.findall( 

868 r"^\s*port(\s|\()(.+)$", 

869 entity_vhd, 

870 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

871 ) 

872 

873 if len(port_matches) == 0: 

874 raise ValueError(f'Could not find "port" block in "{vhdl_file.path}".') 

875 if len(port_matches) > 1: 

876 raise ValueError(f'Found multiple "port" blocks in "{vhdl_file.path}".') 

877 

878 port_vhd = port_matches[0][1] 

879 clock_matches = re.findall( 

880 r"^\s*(\w+_)?(clk|clock)(_\w+)?\s*:", 

881 port_vhd, 

882 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

883 ) 

884 

885 return [f"{prefix}{clock}{suffix}" for prefix, clock, suffix in clock_matches] 

886 

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

888 success = True 

889 for build_result_checker in self.build_result_checkers: 

890 checker_result = build_result_checker.check(build_result) 

891 success = success and checker_result 

892 

893 return success 

894 

895 @staticmethod 

896 def _get_logic_level_distribution(run_path: Path) -> str: 

897 return LogicLevelDistributionParser.get_table( 

898 read_file(run_path / "logic_level_distribution.rpt") 

899 ) 

900 

901 

902class VivadoIpCoreProject(VivadoProject): 

903 """ 

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

905 """ 

906 

907 ip_cores_only = True 

908 

909 def __init__( 

910 self, 

911 **kwargs: Any, # noqa: ANN401 

912 ) -> None: 

913 """ 

914 Arguments: 

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

916 """ 

917 super().__init__(**kwargs) 

918 

919 def build( 

920 self, 

921 **kwargs: Any, # noqa: ANN401 

922 ) -> NoReturn: 

923 """ 

924 Not implemented. 

925 """ 

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

927 

928 

929def copy_and_combine_dicts( 

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

931) -> dict[str, Any]: 

932 """ 

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

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

935 """ 

936 if dict_first is None: 

937 if dict_second is None: 

938 return {} 

939 

940 return dict_second.copy() 

941 

942 if dict_second is None: 

943 return dict_first.copy() 

944 

945 result = dict_first.copy() 

946 result.update(dict_second) 

947 

948 return result