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

259 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-21 23:39 +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 copy_and_combine_dicts, create_file, read_file 

23 

24from .build_result import VivadoBuildResult 

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.generics import GenericValues 

33 from tsfpga.module_list import ModuleList 

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, PLR0917 

44 self, 

45 name: str, 

46 modules: ModuleList, 

47 part: str, 

48 top: str | None = None, 

49 generics: GenericValues | None = None, 

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

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

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

53 vivado_path: Path | None = None, 

54 default_run_index: int = 1, 

55 impl_explore: bool = False, 

56 defined_at: Path | None = None, 

57 **other_arguments: Any, # noqa: ANN401 

58 ) -> None: 

59 """ 

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

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

62 

63 Arguments: 

64 name: Project name. 

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

66 part: Part identification. 

67 top: Name of top level entity. 

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

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

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

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

72 

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

74 

75 The generic value shall be of type 

76 

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

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

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

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

81 ``unsigned``, etc.), or 

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

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

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

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

86 vivado_path: A path to the Vivado executable. 

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

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

89 project. 

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

91 specify at build-time. 

92 impl_explore: Run multiple implementation strategies in parallel. 

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

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

95 projects set up. 

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

97 instead be passed on to 

98 

99 * :func:`BaseModule.get_synthesis_files() 

100 <tsfpga.module.BaseModule.get_synthesis_files>` 

101 * :func:`BaseModule.get_ip_core_files() 

102 <tsfpga.module.BaseModule.get_ip_core_files>` 

103 * :func:`BaseModule.get_scoped_constraints() 

104 <tsfpga.module.BaseModule.get_scoped_constraints>` 

105 * :func:`VivadoProject.pre_create` 

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

107 * :func:`VivadoProject.pre_build` 

108 * :func:`VivadoProject.post_build` 

109 

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

111 :meth:`.build`. 

112 

113 .. note:: 

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

115 """ 

116 self.name = name 

117 self.modules = modules.copy() 

118 self.part = part 

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

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

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

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

123 self._vivado_path = vivado_path 

124 self.default_run_index = default_run_index 

125 self.impl_explore = impl_explore 

126 self.defined_at = defined_at 

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

128 

129 # Will be set by subclass when applicable 

130 self.is_netlist_build = False 

131 self.open_and_analyze_synthesized_design = True 

132 self.ip_cores_only = False 

133 

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

135 

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

137 

138 for constraint in self.constraints: 

139 if not isinstance(constraint, Constraint): 

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

141 

142 for tcl_source in self.tcl_sources: 

143 if not isinstance(tcl_source, Path): 

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

145 

146 for build_step_hook in self.build_step_hooks: 

147 if not isinstance(build_step_hook, BuildStepTclHook): 

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

149 

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

151 """ 

152 Arguments: 

153 project_path: A path containing a Vivado project. 

154 

155 Return: 

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

157 """ 

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

159 

160 def _setup_tcl_sources(self) -> None: 

161 tsfpga_tcl_sources = [ 

162 TSFPGA_TCL / "vivado_default_run.tcl", 

163 TSFPGA_TCL / "vivado_fast_run.tcl", 

164 TSFPGA_TCL / "vivado_messages.tcl", 

165 ] 

166 

167 if self.impl_explore: 

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

169 

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

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

172 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

173 

174 def _setup_and_create_build_step_hooks( 

175 self, project_path: Path 

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

177 """ 

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

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

180 """ 

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

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

183 # want duplicates. 

184 build_step_hooks = self.build_step_hooks.copy() 

185 

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

187 # after implementation. 

188 build_step_hooks.append( 

189 BuildStepTclHook( 

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

191 ) 

192 ) 

193 build_step_hooks.append( 

194 BuildStepTclHook( 

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

196 ) 

197 ) 

198 

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

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

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

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

203 # a post-synthesis hook. 

204 build_step_hooks.append( 

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

206 ) 

207 build_step_hooks.append( 

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

209 ) 

210 build_step_hooks.append( 

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

212 ) 

213 

214 if not self.open_and_analyze_synthesized_design: 

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

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

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

218 

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

220 # when the report is generated via a hook. 

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

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

223 build_step_hooks.append( 

224 BuildStepTclHook( 

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

226 ) 

227 ) 

228 

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

230 # list each clock domain separately. 

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

232 # domain they belong to. 

233 build_step_hooks.append( 

234 BuildStepTclHook( 

235 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

236 "STEPS.SYNTH_DESIGN.TCL.POST", 

237 ) 

238 ) 

239 

240 organized_build_step_hooks = self._organize_build_step_hooks( 

241 build_step_hooks=build_step_hooks, project_folder=project_path 

242 ) 

243 self._create_build_step_hook_files(build_step_hooks=organized_build_step_hooks) 

244 

245 return organized_build_step_hooks 

246 

247 @staticmethod 

248 def _organize_build_step_hooks( 

249 build_step_hooks: list[BuildStepTclHook], project_folder: Path 

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

251 """ 

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

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

254 

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

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

257 and add that as the hook to Vivado. 

258 """ 

259 result = {} 

260 for build_step_hook in build_step_hooks: 

261 if build_step_hook.hook_step in result: 

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

263 else: 

264 tcl_file = project_folder / ( 

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

266 ) 

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

268 

269 return result 

270 

271 def _create_build_step_hook_files( 

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

273 ) -> None: 

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

275 source_hooks_tcl = "\n".join( 

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

277 ) 

278 create_file( 

279 tcl_file, 

280 f"""\ 

281# ------------------------------------------------------------------------------ 

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

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

284{source_hooks_tcl} 

285""", 

286 ) 

287 

288 def _create_tcl( 

289 self, 

290 project_path: Path, 

291 ip_cache_path: Path | None, 

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

293 all_arguments: dict[str, Any], 

294 ) -> Path: 

295 """ 

296 Make a TCL file that creates a Vivado project 

297 """ 

298 project_file = self.project_file(project_path=project_path) 

299 if project_file.exists(): 

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

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

302 

303 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

304 tcl = self.tcl.create( 

305 project_folder=project_path, 

306 modules=self.modules, 

307 part=self.part, 

308 top=self.top, 

309 run_index=self.default_run_index, 

310 generics=self.static_generics, 

311 constraints=self.constraints, 

312 tcl_sources=self.tcl_sources, 

313 build_step_hooks=build_step_hooks, 

314 ip_cache_path=ip_cache_path, 

315 disable_io_buffers=self.is_netlist_build, 

316 ip_cores_only=self.ip_cores_only, 

317 other_arguments=all_arguments, 

318 ) 

319 create_file(create_vivado_project_tcl, tcl) 

320 

321 return create_vivado_project_tcl 

322 

323 def create( 

324 self, 

325 project_path: Path, 

326 ip_cache_path: Path | None = None, 

327 **other_arguments: Any, # noqa: ANN401 

328 ) -> bool: 

329 """ 

330 Create a Vivado project 

331 

332 Arguments: 

333 project_path: Path where the project shall be placed. 

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

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

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

337 instead be sent to 

338 

339 * :func:`BaseModule.get_synthesis_files() 

340 <tsfpga.module.BaseModule.get_synthesis_files>` 

341 * :func:`BaseModule.get_ip_core_files() 

342 <tsfpga.module.BaseModule.get_ip_core_files>` 

343 * :func:`BaseModule.get_scoped_constraints() 

344 <tsfpga.module.BaseModule.get_scoped_constraints>` 

345 * :func:`VivadoProject.pre_create` 

346 

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

348 

349 .. note:: 

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

351 

352 Return: 

353 True if everything went well. 

354 """ 

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

356 self._setup_tcl_sources() 

357 build_step_hooks = self._setup_and_create_build_step_hooks(project_path=project_path) 

358 

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

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

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

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

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

364 # an issue. 

365 self.modules = deepcopy(self.modules) 

366 

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

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

369 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

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

371 

372 if not self.pre_create( 

373 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

374 ): 

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

376 return False 

377 

378 create_vivado_project_tcl = self._create_tcl( 

379 project_path=project_path, 

380 ip_cache_path=ip_cache_path, 

381 build_step_hooks=build_step_hooks, 

382 all_arguments=all_arguments, 

383 ) 

384 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

385 

386 def pre_create( 

387 self, 

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

389 ) -> bool: 

390 """ 

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

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

393 

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

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

396 Vivado system PATH. 

397 

398 .. Note:: 

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

400 this mechanism. 

401 

402 Arguments: 

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

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

405 

406 Return: 

407 True if everything went well. 

408 """ 

409 return True 

410 

411 def _build_tcl( # noqa: PLR0913, PLR0917 

412 self, 

413 project_path: Path, 

414 output_path: Path | None, 

415 num_threads: int, 

416 run_index: int, 

417 all_generics: GenericValues, 

418 synth_only: bool, 

419 from_impl: bool, 

420 impl_explore: bool, 

421 ) -> Path: 

422 """ 

423 Make a TCL file that builds a Vivado project 

424 """ 

425 project_file = self.project_file(project_path=project_path) 

426 if not project_file.exists(): 

427 raise ValueError( 

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

429 ) 

430 

431 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

432 tcl = self.tcl.build( 

433 project_file=project_file, 

434 output_path=output_path, 

435 num_threads=num_threads, 

436 run_index=run_index, 

437 generics=all_generics, 

438 synth_only=synth_only, 

439 from_impl=from_impl, 

440 open_and_analyze_synthesized_design=self.open_and_analyze_synthesized_design, 

441 impl_explore=impl_explore, 

442 ) 

443 create_file(build_vivado_project_tcl, tcl) 

444 

445 return build_vivado_project_tcl 

446 

447 def pre_build( 

448 self, 

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

450 ) -> bool: 

451 """ 

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

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

454 

455 Arguments: 

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

457 parameters from the user. 

458 

459 Return: 

460 True if everything went well. 

461 """ 

462 return True 

463 

464 def post_build( 

465 self, 

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

467 ) -> bool: 

468 """ 

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

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

471 

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

473 material that shall be included in FPGA release artifacts. 

474 

475 .. Note:: 

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

477 this mechanism. 

478 

479 Arguments: 

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

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

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

483 utilization. 

484 

485 Return: 

486 True if everything went well. 

487 """ 

488 return True 

489 

490 def build( # noqa: C901, PLR0912, PLR0913, PLR0917 

491 self, 

492 project_path: Path, 

493 output_path: Path | None = None, 

494 run_index: int | None = None, 

495 generics: GenericValues | None = None, 

496 synth_only: bool = False, 

497 from_impl: bool = False, 

498 num_threads: int = 12, 

499 **pre_and_post_build_parameters: Any, # noqa: ANN401 

500 ) -> VivadoBuildResult: 

501 """ 

502 Build a Vivado project 

503 

504 Arguments: 

505 project_path: A path containing a Vivado project. 

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

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

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

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

510 

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

512 

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

514 synth_only: Run synthesis and then stop. 

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

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

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

518 but will instead be sent to 

519 

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

521 * :func:`VivadoProject.pre_build` 

522 * :func:`VivadoProject.post_build` 

523 

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

525 

526 .. note:: 

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

528 

529 Return: 

530 Result object with build information. 

531 """ 

532 synth_only = synth_only or self.is_netlist_build 

533 

534 if synth_only: 

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

536 else: 

537 if output_path is None: 

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

539 

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

541 

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

543 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

544 

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

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

547 

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

549 # over the static arguments. 

550 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

551 all_parameters.update( 

552 project_path=project_path, 

553 output_path=output_path, 

554 run_index=run_index, 

555 generics=all_generics, 

556 synth_only=synth_only, 

557 from_impl=from_impl, 

558 num_threads=num_threads, 

559 ) 

560 

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

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

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

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

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

566 # is not an issue. 

567 self.modules = deepcopy(self.modules) 

568 

569 result = VivadoBuildResult(name=self.name, synthesis_run_name=f"synth_{run_index}") 

570 

571 for module in self.modules: 

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

573 print( 

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

575 ) 

576 result.success = False 

577 return result 

578 

579 # Make sure register packages are up to date 

580 module.create_register_synthesis_files() 

581 

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

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

584 result.success = False 

585 return result 

586 

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

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

589 # not None in that case above. 

590 

591 build_vivado_project_tcl = self._build_tcl( 

592 project_path=project_path, 

593 output_path=output_path, 

594 num_threads=num_threads, 

595 run_index=run_index, 

596 all_generics=all_generics, 

597 synth_only=synth_only, 

598 from_impl=from_impl, 

599 impl_explore=self.impl_explore, 

600 ) 

601 

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

603 result.success = False 

604 return result 

605 

606 result.synthesis_size = self._get_size( 

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

608 ) 

609 

610 if not synth_only: 

611 if self.impl_explore: 

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

613 for run_path in runs_path.iterdir(): 

614 if "impl_explore_" in run_path.name: 

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

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

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

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

619 result.implementation_run_name = run_path.name 

620 break 

621 else: 

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

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

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

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

626 

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

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

629 result.implementation_size = self._get_size( 

630 project_path=project_path, run_name=result.implementation_run_name 

631 ) 

632 

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

634 all_parameters.update(build_result=result) 

635 

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

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

638 result.success = False 

639 

640 return result 

641 

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

643 """ 

644 Open the project in Vivado GUI. 

645 

646 Arguments: 

647 project_path: A path containing a Vivado project. 

648 

649 Return: 

650 True if everything went well. 

651 """ 

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

653 

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

655 """ 

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

657 for the specified run. 

658 """ 

659 report_as_string = read_file( 

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

661 ) 

662 return HierarchicalUtilizationParser.get_size(report_as_string) 

663 

664 def __str__(self) -> str: 

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

666 

667 if self.defined_at is not None: 

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

669 

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

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

672 

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

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

675 

676 if self.other_arguments: 

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

678 

679 return result 

680 

681 @staticmethod 

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

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

684 

685 

686class VivadoNetlistProject(VivadoProject): 

687 """ 

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

689 """ 

690 

691 _clock_period_ns = 2.0 

692 

693 def __init__( 

694 self, 

695 analyze_synthesis_timing: bool = False, 

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

697 **kwargs: Any, # noqa: ANN401 

698 ) -> None: 

699 """ 

700 Arguments: 

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

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

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

704 

705 .. note:: 

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

707 build_result_checkers: 

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

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

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

711 """ 

712 super().__init__(**kwargs) 

713 

714 self.is_netlist_build = True 

715 self.open_and_analyze_synthesized_design = analyze_synthesis_timing 

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

717 

718 def create( 

719 self, 

720 project_path: Path, 

721 **kwargs: Any, # noqa: ANN401 

722 ) -> bool: 

723 """ 

724 Create the project. 

725 

726 Arguments: 

727 project_path: Path where the project shall be placed. 

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

729 """ 

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

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

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

733 # of the project. 

734 tcl_path = create_file( 

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

736 ) 

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

738 # can override the clocks. 

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

740 

741 if self.open_and_analyze_synthesized_design: 

742 self._set_auto_clock_constraint(tcl_path=tcl_path) 

743 

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

745 

746 def build( 

747 self, 

748 project_path: Path, 

749 **kwargs: Any, # noqa: ANN401 

750 ) -> VivadoBuildResult: 

751 """ 

752 Build the project. 

753 

754 Arguments: 

755 project_path: A path containing a Vivado project. 

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

757 """ 

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

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

760 # post-synthesis hook step. 

761 self._setup_and_create_build_step_hooks(project_path=project_path) 

762 

763 if self.open_and_analyze_synthesized_design: 

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

765 self._set_auto_clock_constraint( 

766 tcl_path=self._get_auto_clock_constraint_path(project_path=project_path) 

767 ) 

768 

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

770 

771 if not result.success: 

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

773 return result 

774 

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

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

777 

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

779 

780 if self.open_and_analyze_synthesized_design: 

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

782 # if we could not auto detect any clocks. 

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

784 # or if our auto-detect failed. 

785 with contextlib.suppress(FileNotFoundError, FoundNoSlackError): 

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

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

788 # meaning higher frequency. 

789 # Hence the subtraction. 

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

791 

792 return result 

793 

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

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

796 

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

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

799 top_file = self._find_top_level_file() 

800 clock_names = ( 

801 self._find_vhdl_clock_names(vhdl_file=top_file) 

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

803 else [] 

804 ) 

805 

806 if clock_names: 

807 create_clock_tcl = "\n".join( 

808 [ 

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

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

811 for clock_name in clock_names 

812 ] 

813 ) 

814 tcl = f"""\ 

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

816# {top_file.path} 

817{create_clock_tcl} 

818""" 

819 create_file(tcl_path, tcl) 

820 

821 def _find_top_level_file(self) -> HdlFile: 

822 top_files = [ 

823 hdl_file 

824 for module in self.modules 

825 for hdl_file in module.get_synthesis_files() 

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

827 ] 

828 if len(top_files) == 0: 

829 raise ValueError( 

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

831 ) 

832 if len(top_files) > 1: 

833 raise ValueError( 

834 f"Found multiple HDL source files corresponding to " 

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

836 ) 

837 

838 return top_files[0] 

839 

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

841 """ 

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

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

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

845 (separated by underscore). 

846 """ 

847 top_vhd = read_file(vhdl_file.path) 

848 

849 entity_matches = re.findall( 

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

851 top_vhd, 

852 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

853 ) 

854 if len(entity_matches) == 0: 

855 raise ValueError( 

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

857 ) 

858 if len(entity_matches) > 1: 

859 raise ValueError( 

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

861 ) 

862 

863 entity_vhd = entity_matches[0] 

864 port_matches = re.findall( 

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

866 entity_vhd, 

867 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

868 ) 

869 

870 if len(port_matches) == 0: 

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

872 if len(port_matches) > 1: 

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

874 

875 port_vhd = port_matches[0][1] 

876 clock_matches = re.findall( 

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

878 port_vhd, 

879 re.DOTALL | re.MULTILINE | re.IGNORECASE, 

880 ) 

881 

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

883 

884 def _check_size(self, build_result: VivadoBuildResult) -> bool: 

885 success = True 

886 for build_result_checker in self.build_result_checkers: 

887 checker_result = build_result_checker.check(build_result) 

888 success = success and checker_result 

889 

890 return success 

891 

892 @staticmethod 

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

894 return LogicLevelDistributionParser.get_table( 

895 read_file(run_path / "logic_level_distribution.rpt") 

896 ) 

897 

898 

899class VivadoIpCoreProject(VivadoProject): 

900 """ 

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

902 """ 

903 

904 ip_cores_only = True 

905 

906 def __init__( 

907 self, 

908 **kwargs: Any, # noqa: ANN401 

909 ) -> None: 

910 """ 

911 Arguments: 

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

913 """ 

914 super().__init__(**kwargs) 

915 

916 def build( 

917 self, 

918 **kwargs: Any, # noqa: ANN401 

919 ) -> NoReturn: 

920 """ 

921 Not implemented. 

922 """ 

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