Coverage for tsfpga/vivado/tcl.py: 97%

213 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-10 20:51 +0000

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

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

3# 

4# This file is part of the tsfpga project, a project platform for modern FPGA development. 

5# https://tsfpga.com 

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

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

8 

9# Standard libraries 

10from pathlib import Path 

11from typing import TYPE_CHECKING, Any, Iterable, Optional 

12 

13# First party libraries 

14from tsfpga.system_utils import create_file 

15 

16# Local folder libraries 

17from .common import to_tcl_path 

18from .generics import get_vivado_tcl_generic_value 

19 

20if TYPE_CHECKING: 

21 # First party libraries 

22 from tsfpga.build_step_tcl_hook import BuildStepTclHook 

23 from tsfpga.constraint import Constraint 

24 from tsfpga.module_list import ModuleList 

25 

26# Number of available Vivado implementation strategies 

27NUM_VIVADO_STRATEGIES = 33 

28 

29 

30class VivadoTcl: 

31 """ 

32 Class with methods for translating a set of sources into Vivado TCL 

33 """ 

34 

35 def __init__( 

36 self, 

37 name: str, 

38 ) -> None: 

39 self.name = name 

40 

41 # pylint: disable=too-many-arguments 

42 def create( 

43 self, 

44 project_folder: Path, 

45 modules: "ModuleList", 

46 part: str, 

47 top: str, 

48 run_index: int, 

49 generics: Optional[dict[str, str]] = None, 

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

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

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

53 ip_cache_path: Optional[Path] = None, 

54 disable_io_buffers: bool = True, 

55 # Add no sources other than IP cores 

56 ip_cores_only: bool = False, 

57 # Will be passed on to module functions. Enables parameterization of e.g. IP cores. 

58 other_arguments: Optional[dict[str, Any]] = None, 

59 ) -> str: 

60 generics = {} if generics is None else generics 

61 other_arguments = {} if other_arguments is None else other_arguments 

62 

63 tcl = f"create_project {self.name} {{{to_tcl_path(project_folder)}}} -part {part}\n" 

64 tcl += "set_property target_language VHDL [current_project]\n" 

65 

66 if ip_cache_path is not None: 

67 tcl += f"config_ip_cache -use_cache_location {{{to_tcl_path(ip_cache_path)}}}\n" 

68 tcl += "\n" 

69 

70 tcl += self._add_tcl_sources(tcl_sources) 

71 tcl += "\n" 

72 

73 if not ip_cores_only: 

74 tcl += self._add_module_source_files(modules=modules, other_arguments=other_arguments) 

75 tcl += "\n" 

76 tcl += self._add_generics(generics) 

77 tcl += "\n" 

78 tcl += self._add_constraints( 

79 self._iterate_constraints( 

80 modules=modules, constraints=constraints, other_arguments=other_arguments 

81 ) 

82 ) 

83 tcl += "\n" 

84 tcl += self._add_build_step_hooks(build_step_hooks, project_folder) 

85 tcl += "\n" 

86 

87 tcl += self._add_ip_cores(modules=modules, other_arguments=other_arguments) 

88 tcl += "\n" 

89 tcl += self._add_project_settings() 

90 tcl += "\n" 

91 tcl += f"current_run [get_runs synth_{run_index}]\n" 

92 tcl += "\n" 

93 tcl += f"set_property top {top} [current_fileset]\n" 

94 tcl += "reorder_files -auto -disable_unused\n" 

95 tcl += "\n" 

96 

97 if disable_io_buffers: 

98 tcl += ( 

99 "set_property -name {STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS} " 

100 f"-value -no_iobuf -objects [get_runs synth_{run_index}]" 

101 ) 

102 tcl += "\n" 

103 

104 tcl += "exit\n" 

105 return tcl 

106 

107 def _add_module_source_files( 

108 self, modules: "ModuleList", other_arguments: dict[str, Any] 

109 ) -> str: 

110 tcl = "" 

111 for module in modules: 

112 vhdl_files = [] 

113 verilog_source_files = [] 

114 for hdl_file in module.get_synthesis_files(**other_arguments): 

115 if hdl_file.is_vhdl: 

116 vhdl_files.append(hdl_file.path) 

117 elif hdl_file.is_verilog_source: 

118 verilog_source_files.append(hdl_file.path) 

119 else: 

120 raise NotImplementedError(f"Can not handle file: {hdl_file}") 

121 # Verilog headers do not need to be handled at all if the 

122 # source file that uses them is in the same directory. If 

123 # it is not, the path needs to be added to include_dirs with 

124 # a tcl command like: 

125 # set_property include_dirs {/some/path /some/other/path} [current_fileset] 

126 # See https://www.xilinx.com/support/answers/54006.html 

127 

128 # Encrypted source files (verilog (.vp?), VHDL) I do not know how 

129 # to handle, since I have no use case for it at the moment. 

130 

131 if vhdl_files: 

132 files_string = self._to_file_list(vhdl_files) 

133 tcl += f"read_vhdl -library {module.library_name} -vhdl2008 {files_string}\n" 

134 if verilog_source_files: 

135 files_string = self._to_file_list(verilog_source_files) 

136 tcl += f"read_verilog {files_string}\n" 

137 

138 return tcl 

139 

140 @staticmethod 

141 def _to_file_list(file_paths: list[Path]) -> str: 

142 """ 

143 Return a TCL snippet for a file list, with each file enclosed in curly braces. 

144 E.g. "{file1}" or "{{file1} {file2} {file3}}" 

145 """ 

146 if len(file_paths) == 1: 

147 files_string = to_tcl_path(file_paths[0]) 

148 else: 

149 files_string = " ".join([f"{{{to_tcl_path(file_path)}}}" for file_path in file_paths]) 

150 

151 return f"{{{files_string}}}" 

152 

153 @staticmethod 

154 def _add_tcl_sources(tcl_sources: Optional[list[Path]]) -> str: 

155 if tcl_sources is None: 

156 return "" 

157 

158 tcl = "" 

159 for tcl_source_file in tcl_sources: 

160 tcl += f"source -notrace {{{to_tcl_path(tcl_source_file)}}}\n" 

161 return tcl 

162 

163 @staticmethod 

164 def _add_ip_cores(modules: "ModuleList", other_arguments: dict[str, Any]) -> str: 

165 tcl = "" 

166 for module in modules: 

167 for ip_core_file in module.get_ip_core_files(**other_arguments): 

168 create_function_name = f"create_ip_core_{ip_core_file.name}" 

169 tcl += f"proc {create_function_name} {{}} {{\n" 

170 

171 if ip_core_file.variables: 

172 for key, value in ip_core_file.variables.items(): 

173 tcl += f' set {key} "{value}"\n' 

174 

175 tcl += f"""\ 

176 source -notrace {{{to_tcl_path(ip_core_file.path)}}} 

177}} 

178{create_function_name} 

179""" 

180 

181 return tcl 

182 

183 def _add_build_step_hooks( 

184 self, build_step_hooks: Optional[list["BuildStepTclHook"]], project_folder: Path 

185 ) -> str: 

186 if build_step_hooks is None: 

187 return "" 

188 

189 # There can be many hooks for the same step. Reorganize them into a dict. 

190 hook_steps: dict[str, list["BuildStepTclHook"]] = {} 

191 for build_step_hook in build_step_hooks: 

192 if build_step_hook.hook_step in hook_steps: 

193 hook_steps[build_step_hook.hook_step].append(build_step_hook) 

194 else: 

195 hook_steps[build_step_hook.hook_step] = [build_step_hook] 

196 

197 tcl = "" 

198 for step, hooks in hook_steps.items(): 

199 # Vivado will only accept one TCL script as hook for each step. So if we want 

200 # to add more we have to create a new TCL file, that sources the other files, 

201 # and add that as the hook to Vivado. 

202 if len(hooks) == 1: 

203 tcl_file = hooks[0].tcl_file 

204 else: 

205 tcl_file = project_folder / ("hook_" + step.replace(".", "_") + ".tcl") 

206 source_hooks_tcl = "".join( 

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

208 ) 

209 create_file(tcl_file, source_hooks_tcl) 

210 

211 # Add to fileset to enable archive and other project based functionality 

212 tcl += f"add_files -fileset utils_1 -norecurse {{{to_tcl_path(tcl_file)}}}\n" 

213 

214 # Build step hook can only be applied to a run (e.g. impl_1), not on a project basis 

215 run_wildcard = "synth_*" if hooks[0].step_is_synth else "impl_*" 

216 tcl_block = f"set_property {step} {{{to_tcl_path(tcl_file)}}} ${{run}}" 

217 tcl += self._tcl_for_each_run(run_wildcard, tcl_block) 

218 

219 return tcl 

220 

221 def _add_project_settings(self) -> str: 

222 tcl = "" 

223 

224 # Default value for when opening project in GUI. 

225 # Will be overwritten if using build() function. 

226 tcl += "set_param general.maxThreads 7\n" 

227 

228 # Enable VHDL assert statements to be evaluated. A severity level of failure will 

229 # stop the synthesis and produce an error. 

230 tcl_block = "set_property STEPS.SYNTH_DESIGN.ARGS.ASSERT true ${run}" 

231 tcl += self._tcl_for_each_run("synth_*", tcl_block) 

232 

233 # Enable binary bitstream as well 

234 tcl_block = "set_property STEPS.WRITE_BITSTREAM.ARGS.BIN_FILE true ${run}" 

235 tcl += self._tcl_for_each_run("impl_*", tcl_block) 

236 

237 return tcl 

238 

239 @staticmethod 

240 def _tcl_for_each_run(run_wildcard: str, tcl_block: str) -> str: 

241 """ 

242 Apply TCL block for each defined run. Use ${run} for run variable in TCL. 

243 """ 

244 tcl = "" 

245 tcl += f"foreach run [get_runs {run_wildcard}] {{\n" 

246 tcl += tcl_block + "\n" 

247 tcl += "}\n" 

248 return tcl 

249 

250 @staticmethod 

251 def _add_generics(generics: Optional[dict[str, Any]]) -> str: 

252 """ 

253 Generics are set according to this weird format: 

254 https://www.xilinx.com/support/answers/52217.html 

255 """ 

256 if not generics: 

257 return "" 

258 

259 generic_list = [] 

260 for name, value in generics.items(): 

261 value_tcl_formatted = get_vivado_tcl_generic_value(value=value) 

262 generic_list.append(f"{name}={value_tcl_formatted}") 

263 

264 generics_string = " ".join(generic_list) 

265 return f"set_property generic {{{generics_string}}} [current_fileset]\n" 

266 

267 @staticmethod 

268 def _iterate_constraints( 

269 modules: "ModuleList", 

270 constraints: Optional[list["Constraint"]], 

271 other_arguments: dict[str, Any], 

272 ) -> Iterable["Constraint"]: 

273 for module in modules: 

274 for constraint in module.get_scoped_constraints(**other_arguments): 

275 yield constraint 

276 

277 if constraints is not None: 

278 for constraint in constraints: 

279 yield constraint 

280 

281 @staticmethod 

282 def _add_constraints(constraints: Iterable["Constraint"]) -> str: 

283 tcl = "" 

284 for constraint in constraints: 

285 constraint_file = to_tcl_path(constraint.file) 

286 

287 ref_flags = "" if constraint.ref is None else (f"-ref {constraint.ref} ") 

288 managed_flags = "" if constraint_file.endswith("xdc") else "-unmanaged " 

289 tcl += f"read_xdc {ref_flags}{managed_flags}{{{constraint_file}}}\n" 

290 

291 get_file = f"[get_files {{{constraint_file}}}]" 

292 tcl += f"set_property PROCESSING_ORDER {constraint.processing_order} {get_file}\n" 

293 

294 if constraint.used_in == "impl": 

295 tcl += f"set_property used_in_synthesis false {get_file}\n" 

296 elif constraint.used_in == "synth": 

297 tcl += f"set_property used_in_implementation false {get_file}\n" 

298 

299 return tcl 

300 

301 def build( 

302 self, 

303 project_file: Path, 

304 output_path: Path, 

305 num_threads: int, 

306 run_index: int, 

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

308 synth_only: bool = False, 

309 from_impl: bool = False, 

310 impl_explore: bool = False, 

311 analyze_synthesis_timing: bool = True, 

312 ) -> str: 

313 if impl_explore: 

314 # For implementation explore, threads are divided to one each per job. 

315 # Number of jobs in parallel are the number of threads specified for build. 

316 # Clamp max threads between 1 and 32, which are allowed by Vivado 2018.3+. 

317 num_threads_general = min(max(1, num_threads // NUM_VIVADO_STRATEGIES), 32) 

318 else: 

319 # Max value in Vivado 2018.3+. set_param will give an error if higher number. 

320 num_threads_general = min(num_threads, 32) 

321 

322 num_threads_synth = min(num_threads, 8) 

323 

324 tcl = f"open_project {to_tcl_path(project_file)}\n" 

325 tcl += f"set_param general.maxThreads {num_threads_general}\n" 

326 tcl += f"set_param synth.maxThreads {num_threads_synth}\n" 

327 tcl += "\n" 

328 tcl += self._add_generics(generics) 

329 tcl += "\n" 

330 

331 if not from_impl: 

332 synth_run = f"synth_{run_index}" 

333 

334 tcl += self._synthesis(synth_run, num_threads, analyze_synthesis_timing) 

335 tcl += "\n" 

336 

337 if not synth_only: 

338 impl_run = f"impl_{run_index}" 

339 

340 if impl_explore: 

341 tcl += self._run_multiple(num_jobs=num_threads) 

342 else: 

343 tcl += self._run(impl_run, num_threads, to_step="write_bitstream") 

344 tcl += "\n" 

345 

346 tcl += self._write_hw_platform(output_path) 

347 tcl += "\n" 

348 

349 tcl += "exit\n" 

350 

351 return tcl 

352 

353 def _synthesis(self, run: str, num_threads: int, analyze_synthesis_timing: bool) -> str: 

354 tcl = self._run(run, num_threads) 

355 if analyze_synthesis_timing: 

356 # For synthesis flow we perform the timing checks by opening the design. It would have 

357 # been more efficient to use a post-synthesis hook (since the design would already be 

358 # open), if that mechanism had worked. It seems to be very bugged. So we add the 

359 # checkers to the build script. 

360 # For implementation, we use a pre-bitstream build hook which seems to work decently. 

361 tcl += """ 

362open_run ${run} 

363set run_directory [get_property DIRECTORY [get_runs ${run}]] 

364 

365# Generate report on simultaneous switching noise (SSN) for the design. 

366# It seems safe to do this after synthesis; inspecting the reports in a test build after both 

367# synthesis and implementation shows that the results are identical. 

368# Will generate a "Designutils 20-923" message if noise margins are not met. 

369# If the user would like this to fail the build, this message severity shall be raised to ERROR. 

370# At the moment we do not know how stable this mechanism is, so we do not fail the build 

371# per default. 

372# The call is very fast (< 1s) so it is fine to run always, even though not everyone will use it. 

373set current_part [get_property PART [current_project]] 

374set part_supports_ssn [get_parts ${current_part} -filter {ssn_report == 1}] 

375if {${part_supports_ssn} != ""} { 

376 set output_file [file join ${run_directory} "report_ssn.html"] 

377 report_ssn -phase -format html -file ${output_file} 

378} 

379 

380# This call is duplicated in report_utilization.tcl for implementation. 

381set output_file [file join ${run_directory} "hierarchical_utilization.rpt"] 

382report_utilization -hierarchical -hierarchical_depth 4 -file ${output_file} 

383 

384 

385# After synthesis we check for unhandled clock crossings and abort the build based on the result. 

386# Other timing checks, e.g. setup/hold/pulse width violations, are not reliable after synthesis, 

387# and should not abort the build. These need to be checked after implementation. 

388""" 

389 

390 tcl += """ 

391# This code is duplicated in check_timing.tcl for implementation. 

392if {[regexp {\\(unsafe\\)} [report_clock_interaction -delay_type min_max -return_string]]} { 

393 puts "ERROR: Unhandled clock crossing in ${run} run. See clock_interaction.rpt and \ 

394timing_summary.rpt in ${run_directory}." 

395 

396 set output_file [file join ${run_directory} "clock_interaction.rpt"] 

397 report_clock_interaction -delay_type min_max -file ${output_file} 

398 

399 set output_file [file join ${run_directory} "timing_summary.rpt"] 

400 report_timing_summary -file ${output_file} 

401 

402 exit 1 

403} 

404""" 

405 return tcl 

406 

407 @staticmethod 

408 def _run(run: str, num_threads: int, to_step: Optional[str] = None) -> str: 

409 to_step = "" if to_step is None else f" -to_step {to_step}" 

410 

411 tcl = f""" 

412set run {run} 

413reset_run ${{run}} 

414launch_runs ${{run}} -jobs {num_threads}{to_step} 

415""" 

416 

417 tcl += """ 

418wait_on_run ${run} 

419 

420if {[get_property PROGRESS [get_runs ${run}]] != "100%"} { 

421 puts "ERROR: Run ${run} failed." 

422 exit 1 

423} 

424""" 

425 return tcl 

426 

427 def _run_multiple(self, num_jobs: int = 4, base_name: str = "impl_explore_") -> str: 

428 # Currently, this creates .tcl that waits for all active runs to complete 

429 

430 tcl = "set build_succeeded 0\n" 

431 tcl += f"reset_runs [get_runs {base_name}*]\n" 

432 tcl += f"launch_runs -jobs {num_jobs} [get_runs {base_name}*] -to_step write_bitstream\n" 

433 tcl += "\n" 

434 

435 tcl += f"wait_on_runs -exit_condition ANY_ONE_MET_TIMING [get_runs {base_name}*]\n" 

436 tcl += "\n" 

437 

438 tcl += 'reset_runs [get_runs -filter {STATUS == "Queued..."}]\n' 

439 

440 # Wait on runs that are still going, since Vivado can't kill runs in progress reliably. 

441 # Killing runs in progress causes a zombie process which will lock up VUnit's Process class. 

442 tcl += f'wait_on_runs [get_runs -filter {{STATUS != "Not started"}} {base_name}*]\n' 

443 tcl += "\n" 

444 

445 tcl_block = """ 

446 set build_succeeded 1 

447 puts "Run $run met timing" 

448""" 

449 tcl += self._tcl_for_each_run( 

450 f'-filter {{PROGRESS == "100%"}} {base_name}*', tcl_block=tcl_block 

451 ) 

452 

453 tcl += """ 

454if {${build_succeeded} eq 0} { 

455 puts "No build met timing, exiting." 

456 exit 1 

457} 

458""" 

459 

460 return tcl 

461 

462 def _write_hw_platform(self, output_path: Path) -> str: 

463 """ 

464 TCL command to create a Xilinx support archive (.xsa) file, for use as a 

465 hardware platform. 

466 Used to be known as a "hdf" or "hwdef" file. 

467 

468 This is mainly used for Zynq devices to generate code to set up the PS at boot. 

469 There is also code generated for each MicroBlaze that is present in the design. 

470 If there is neither a block design nor a MicroBlaze available, the .xsa will be empty apart 

471 from some info about which part is used, etc. 

472 

473 The '-quiet' flag is used since there was a Vivado bug observed in this very 

474 specific scenario: 

475 * Vivado 2022.1 

476 * UltraScale+ non-Zynq device (i.e. no block design) 

477 * Design contains MicroBlaze 

478 * Design contains ILA 

479 In this case the 'write_hw_platform' call would fail. 

480 This bug might be present in other Vivado versions and in other scenarios as well. 

481 Since this is a very fringe scenario, and it is unlikely that anyone would ever need the 

482 .xsa file specifically from the ILA build, we allow the command to fail quietly. 

483 """ 

484 xsa_file = to_tcl_path(output_path / f"{self.name}.xsa") 

485 

486 tcl = f""" 

487puts "Creating hardware platform {xsa_file}..." 

488write_hw_platform -fixed -force -quiet -include_bit {{{xsa_file}}} 

489""" 

490 

491 return tcl