Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the tsfpga project. 

5# https://tsfpga.com 

6# https://gitlab.com/tsfpga/tsfpga 

7# -------------------------------------------------------------------------------------------------- 

8 

9from copy import deepcopy 

10import shutil 

11 

12from tsfpga import TSFPGA_TCL 

13from tsfpga.system_utils import create_file, read_file 

14from tsfpga.build_step_tcl_hook import BuildStepTclHook 

15from .build_result import BuildResult 

16from .common import run_vivado_tcl, run_vivado_gui 

17from .hierarchical_utilization_parser import HierarchicalUtilizationParser 

18from .logic_level_distribution_parser import LogicLevelDistributionParser 

19from .tcl import VivadoTcl 

20 

21 

22class VivadoProject: 

23 """ 

24 Used for handling a Xilinx Vivado HDL project 

25 """ 

26 

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

28 def __init__( 

29 self, 

30 name, 

31 modules, 

32 part, 

33 top=None, 

34 generics=None, 

35 constraints=None, 

36 tcl_sources=None, 

37 build_step_hooks=None, 

38 vivado_path=None, 

39 default_run_index=1, 

40 defined_at=None, 

41 **other_arguments, 

42 ): 

43 """ 

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

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

46 

47 Arguments: 

48 name (str): Project name. 

49 modules (list(:class:`Module <.BaseModule>`)): Modules that shall be included in the 

50 project. 

51 part (str): Part identification. 

52 top (str): Name of top level entity. If left out, the top level name will be 

53 inferred from the ``name``. 

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

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

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

57 

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

59 constraints (list(Constraint)): Constraints that will be applied to the project. 

60 tcl_sources (list(`pathlib.Path`)): A list of TCL files. Use for e.g. block design, 

61 pinning, settings, etc. 

62 build_step_hooks (list(BuildStepTclHook)): Build step hooks that will be applied to the 

63 project. 

64 vivado_path (`pathlib.Path`): A path to the Vivado executable. If omitted, 

65 the default location from the system PATH will be used. 

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

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

68 specify at build-time. 

69 defined_at (`pathlib.Path`): Optional path to the file where you defined this 

70 project. To get a useful ``build.py --list`` message. Is useful when you have many 

71 projects set up. 

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

73 instead be passed on to 

74 

75 * :func:`BaseModule.get_synthesis_files() 

76 <tsfpga.module.BaseModule.get_synthesis_files>` 

77 * :func:`BaseModule.get_ip_core_files() 

78 <tsfpga.module.BaseModule.get_ip_core_files>` 

79 * :func:`BaseModule.get_scoped_constraints() 

80 <tsfpga.module.BaseModule.get_scoped_constraints>` 

81 * :func:`VivadoProject.pre_create` 

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

83 * :func:`VivadoProject.pre_build` 

84 * :func:`VivadoProject.post_build` 

85 

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

87 :meth:`.build`. 

88 

89 .. note:: 

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

91 """ 

92 self.name = name 

93 self.modules = modules.copy() 

94 self.part = part 

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

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

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

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

99 self._vivado_path = vivado_path 

100 self.default_run_index = default_run_index 

101 self.defined_at = defined_at 

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

103 

104 # Will be set by child class when applicable 

105 self.is_netlist_build = False 

106 self.analyze_synthesis_timing = True 

107 self.report_logic_level_distribution = False 

108 self.ip_cores_only = False 

109 

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

111 

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

113 

114 def project_file(self, project_path): 

115 """ 

116 Arguments: 

117 project_path (`pathlib.Path`): A path containing a Vivado project. 

118 Return: 

119 `pathlib.Path`: The project file of this project, in the given folder 

120 """ 

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

122 

123 def _setup_tcl_sources(self): 

124 tsfpga_tcl_sources = [ 

125 TSFPGA_TCL / "vivado_default_run.tcl", 

126 TSFPGA_TCL / "vivado_fast_run.tcl", 

127 TSFPGA_TCL / "vivado_messages.tcl", 

128 ] 

129 

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

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

132 self.tcl_sources = tsfpga_tcl_sources + self.tcl_sources 

133 

134 def _setup_build_step_hooks(self): 

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

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

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

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

139 # a post-synthesis hook. 

140 self.build_step_hooks.append( 

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

142 ) 

143 self.build_step_hooks.append( 

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

145 ) 

146 

147 if not self.analyze_synthesis_timing: 

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

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

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

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

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

153 # IP cores. 

154 self.build_step_hooks.append( 

155 BuildStepTclHook( 

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

157 ) 

158 ) 

159 

160 if self.report_logic_level_distribution: 

161 # Used by netlist builds 

162 self.build_step_hooks.append( 

163 BuildStepTclHook( 

164 TSFPGA_TCL / "report_logic_level_distribution.tcl", 

165 "STEPS.SYNTH_DESIGN.TCL.POST", 

166 ) 

167 ) 

168 

169 def _create_tcl(self, project_path, ip_cache_path, all_arguments): 

170 """ 

171 Make a TCL file that creates a Vivado project 

172 """ 

173 if project_path.exists(): 

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

175 project_path.mkdir(parents=True) 

176 

177 create_vivado_project_tcl = project_path / "create_vivado_project.tcl" 

178 tcl = self.tcl.create( 

179 project_folder=project_path, 

180 modules=self.modules, 

181 part=self.part, 

182 top=self.top, 

183 run_index=self.default_run_index, 

184 generics=self.static_generics, 

185 constraints=self.constraints, 

186 tcl_sources=self.tcl_sources, 

187 build_step_hooks=self.build_step_hooks, 

188 ip_cache_path=ip_cache_path, 

189 disable_io_buffers=self.is_netlist_build, 

190 ip_cores_only=self.ip_cores_only, 

191 other_arguments=all_arguments, 

192 ) 

193 create_file(create_vivado_project_tcl, tcl) 

194 

195 return create_vivado_project_tcl 

196 

197 def create(self, project_path, ip_cache_path=None, **other_arguments): 

198 """ 

199 Create a Vivado project 

200 

201 Arguments: 

202 project_path (`pathlib.Path`): Path where the project shall be placed. 

203 ip_cache_path (`pathlib.Path`): Path to a folder where the Vivado IP cache can be 

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

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

206 instead be sent to 

207 

208 * :func:`BaseModule.get_synthesis_files() 

209 <tsfpga.module.BaseModule.get_synthesis_files>` 

210 * :func:`BaseModule.get_ip_core_files() 

211 <tsfpga.module.BaseModule.get_ip_core_files>` 

212 * :func:`BaseModule.get_scoped_constraints() 

213 <tsfpga.module.BaseModule.get_scoped_constraints>` 

214 * :func:`VivadoProject.pre_create` 

215 

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

217 

218 .. note:: 

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

220 Returns: 

221 bool: True if everything went well. 

222 """ 

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

224 self._setup_tcl_sources() 

225 self._setup_build_step_hooks() 

226 

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

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

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

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

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

232 # an issue. 

233 self.modules = deepcopy(self.modules) 

234 

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

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

237 all_arguments = copy_and_combine_dicts(self.other_arguments, other_arguments) 

238 all_arguments.update( 

239 generics=self.static_generics, 

240 part=self.part, 

241 ) 

242 

243 if not self.pre_create( 

244 project_path=project_path, ip_cache_path=ip_cache_path, **all_arguments 

245 ): 

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

247 return False 

248 

249 create_vivado_project_tcl = self._create_tcl( 

250 project_path=project_path, ip_cache_path=ip_cache_path, all_arguments=all_arguments 

251 ) 

252 return run_vivado_tcl(self._vivado_path, create_vivado_project_tcl) 

253 

254 def pre_create(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

255 """ 

256 Override this function in a child class if you wish to do something useful with it. 

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

258 

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

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

261 Vivado system PATH. 

262 

263 .. Note:: 

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

265 this mechanism. 

266 

267 Arguments: 

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

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

270 

271 Return: 

272 bool: True if everything went well. 

273 """ 

274 return True 

275 

276 def _build_tcl( 

277 self, project_path, output_path, num_threads, run_index, all_generics, synth_only 

278 ): 

279 """ 

280 Make a TCL file that builds a Vivado project 

281 """ 

282 project_file = self.project_file(project_path) 

283 if not project_file.exists(): 

284 raise ValueError( 

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

286 ) 

287 

288 build_vivado_project_tcl = project_path / "build_vivado_project.tcl" 

289 tcl = self.tcl.build( 

290 project_file=project_file, 

291 output_path=output_path, 

292 num_threads=num_threads, 

293 run_index=run_index, 

294 generics=all_generics, 

295 synth_only=synth_only, 

296 analyze_synthesis_timing=self.analyze_synthesis_timing, 

297 ) 

298 create_file(build_vivado_project_tcl, tcl) 

299 

300 return build_vivado_project_tcl 

301 

302 def pre_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

303 """ 

304 Override this function in a child class if you wish to do something useful with it. 

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

306 

307 Arguments: 

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

309 parameters from the user. 

310 

311 Return: 

312 bool: True if everything went well. 

313 """ 

314 return True 

315 

316 def post_build(self, **kwargs): # pylint: disable=no-self-use, unused-argument 

317 """ 

318 Override this function in a child class if you wish to do something useful with it. 

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

320 

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

322 material that shall be included in FPGA release artifacts. 

323 

324 .. Note:: 

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

326 this mechanism. 

327 

328 Arguments: 

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

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

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

332 utilization. 

333 

334 Return: 

335 bool: True if everything went well. 

336 """ 

337 return True 

338 

339 def build( 

340 self, 

341 project_path, 

342 output_path=None, 

343 run_index=None, 

344 generics=None, 

345 synth_only=False, 

346 num_threads=12, 

347 **pre_and_post_build_parameters, 

348 ): 

349 """ 

350 Build a Vivado project 

351 

352 Arguments: 

353 project_path (`pathlib.Path`): A path containing a Vivado project. 

354 output_path (`pathlib.Path`): Results (bit file, ...) will be placed here. 

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

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

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

358 

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

360 synth_only (bool): Run synthesis and then stop. 

361 num_threads (int): Number of parallel threads to use during run. 

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

363 but will instead be sent to 

364 

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

366 * :func:`VivadoProject.pre_build` 

367 * :func:`VivadoProject.post_build` 

368 

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

370 

371 .. note:: 

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

373 

374 Return: 

375 :class:`.build_result.BuildResult`: Result object with build information. 

376 """ 

377 synth_only = synth_only or self.is_netlist_build 

378 

379 if output_path is None and not synth_only: 

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

381 

382 if synth_only: 

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

384 else: 

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

386 

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

388 all_generics = copy_and_combine_dicts(self.static_generics, generics) 

389 

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

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

392 

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

394 # over the static arguments. 

395 all_parameters = copy_and_combine_dicts(self.other_arguments, pre_and_post_build_parameters) 

396 all_parameters.update( 

397 project_path=project_path, 

398 output_path=output_path, 

399 run_index=run_index, 

400 generics=all_generics, 

401 synth_only=synth_only, 

402 num_threads=num_threads, 

403 ) 

404 

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

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

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

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

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

410 # is not an issue. 

411 self.modules = deepcopy(self.modules) 

412 

413 result = BuildResult(self.name) 

414 

415 for module in self.modules: 

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

417 print( 

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

419 ) 

420 result.success = False 

421 return result 

422 

423 # Make sure register packages are up to date 

424 module.create_regs_vhdl_package() 

425 

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

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

428 result.success = False 

429 return result 

430 

431 build_vivado_project_tcl = self._build_tcl( 

432 project_path=project_path, 

433 output_path=output_path, 

434 num_threads=num_threads, 

435 run_index=run_index, 

436 all_generics=all_generics, 

437 synth_only=synth_only, 

438 ) 

439 

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

441 result.success = False 

442 return result 

443 

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

445 if self.report_logic_level_distribution: 

446 result.logic_level_distribution = self._get_logic_level_distribution( 

447 project_path, f"synth_{run_index}" 

448 ) 

449 

450 if not synth_only: 

451 impl_folder = project_path / f"{self.name}.runs" / f"impl_{run_index}" 

452 shutil.copy2(impl_folder / f"{self.top}.bit", output_path / f"{self.name}.bit") 

453 shutil.copy2(impl_folder / f"{self.top}.bin", output_path / f"{self.name}.bin") 

454 result.implementation_size = self._get_size(project_path, f"impl_{run_index}") 

455 

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

457 all_parameters.update(build_result=result) 

458 

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

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

461 result.success = False 

462 

463 return result 

464 

465 def open(self, project_path): 

466 """ 

467 Open the project in Vivado GUI. 

468 

469 Arguments: 

470 project_path (`pathlib.Path`): A path containing a Vivado project. 

471 

472 Returns: 

473 bool: True if everything went well. 

474 """ 

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

476 

477 def _get_size(self, project_path, run): 

478 """ 

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

480 for the specified run. 

481 """ 

482 report_as_string = read_file( 

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

484 ) 

485 return HierarchicalUtilizationParser.get_size(report_as_string) 

486 

487 def _get_logic_level_distribution(self, project_path, run): 

488 """ 

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

490 for the specified run. 

491 """ 

492 report_as_string = read_file( 

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

494 ) 

495 return LogicLevelDistributionParser.get_table(report_as_string) 

496 

497 def __str__(self): 

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

499 

500 if self.defined_at is not None: 

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

502 

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

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

505 

506 if self.static_generics: 

507 generics = self._dict_to_string(self.static_generics) 

508 else: 

509 generics = "-" 

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

511 

512 if self.other_arguments: 

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

514 

515 return result 

516 

517 @staticmethod 

518 def _dict_to_string(data): 

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

520 

521 

522class VivadoNetlistProject(VivadoProject): 

523 """ 

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

525 """ 

526 

527 def __init__(self, analyze_synthesis_timing=False, build_result_checkers=None, **kwargs): 

528 """ 

529 Arguments: 

530 analyze_synthesis_timing (bool): Enable analysis of the synthesized design's timing. 

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

532 crossings and pulse width violations. 

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

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

535 using a constraint file. 

536 build_result_checkers (list(:class:`.SizeChecker`, :class:`.MaximumLogicLevel`)): 

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

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

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

540 """ 

541 super().__init__(**kwargs) 

542 

543 self.is_netlist_build = True 

544 self.analyze_synthesis_timing = analyze_synthesis_timing 

545 self.report_logic_level_distribution = True 

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

547 

548 def build(self, **kwargs): # pylint: disable=arguments-differ 

549 """ 

550 Build the project. 

551 

552 Arguments: 

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

554 """ 

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

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

557 

558 return result 

559 

560 def _check_size(self, build_result): 

561 if not build_result.success: 

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

563 return False 

564 

565 success = True 

566 for build_result_checker in self.build_result_checkers: 

567 checker_result = build_result_checker.check(build_result) 

568 success = success and checker_result 

569 

570 return success 

571 

572 

573class VivadoIpCoreProject(VivadoProject): 

574 """ 

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

576 """ 

577 

578 def __init__(self, **kwargs): 

579 """ 

580 Arguments: 

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

582 """ 

583 super().__init__(**kwargs) 

584 

585 self.ip_cores_only = True 

586 

587 def build(self, **kwargs): # pylint: disable=arguments-differ 

588 """ 

589 Not implemented. 

590 """ 

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

592 

593 

594def copy_and_combine_dicts(dict_first, dict_second): 

595 """ 

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

597 Will return ``None`` if both are ``None``. 

598 """ 

599 if dict_first is None and dict_second is None: 

600 return None 

601 

602 if dict_first is None: 

603 return dict_second.copy() 

604 

605 if dict_second is None: 

606 return dict_first.copy() 

607 

608 result = dict_first.copy() 

609 result.update(dict_second) 

610 return result