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

188 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-31 20:01 +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://gitlab.com/tsfpga/tsfpga 

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

8 

9# First party libraries 

10from tsfpga.system_utils import create_file 

11 

12# Local folder libraries 

13from .common import to_tcl_path 

14from .generics import get_vivado_tcl_generic_value 

15 

16 

17class VivadoTcl: 

18 """ 

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

20 """ 

21 

22 def __init__( 

23 self, 

24 name, 

25 ): 

26 self.name = name 

27 

28 # pylint: disable=too-many-arguments 

29 def create( 

30 self, 

31 project_folder, 

32 modules, 

33 part, 

34 top, 

35 run_index, 

36 generics=None, 

37 constraints=None, 

38 tcl_sources=None, 

39 build_step_hooks=None, 

40 ip_cache_path=None, 

41 disable_io_buffers=True, 

42 # Add no sources other than IP cores 

43 ip_cores_only=False, 

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

45 other_arguments=None, 

46 ): 

47 generics = {} if generics is None else generics 

48 other_arguments = {} if other_arguments is None else other_arguments 

49 

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

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

52 

53 if ip_cache_path is not None: 

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

55 tcl += "\n" 

56 

57 tcl += self._add_tcl_sources(tcl_sources) 

58 tcl += "\n" 

59 

60 if not ip_cores_only: 

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

62 tcl += "\n" 

63 tcl += self._add_generics(generics) 

64 tcl += "\n" 

65 tcl += self._add_constraints( 

66 self._iterate_constraints( 

67 modules=modules, constraints=constraints, other_arguments=other_arguments 

68 ) 

69 ) 

70 tcl += "\n" 

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

72 tcl += "\n" 

73 

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

75 tcl += "\n" 

76 tcl += self._add_project_settings() 

77 tcl += "\n" 

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

79 tcl += "\n" 

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

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

82 tcl += "\n" 

83 

84 if disable_io_buffers: 

85 tcl += ( 

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

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

88 ) 

89 tcl += "\n" 

90 

91 tcl += "exit\n" 

92 return tcl 

93 

94 def _add_module_source_files(self, modules, other_arguments): 

95 tcl = "" 

96 for module in modules: 

97 vhdl_files = [] 

98 verilog_source_files = [] 

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

100 if hdl_file.is_vhdl: 

101 vhdl_files.append(hdl_file.path) 

102 elif hdl_file.is_verilog_source: 

103 verilog_source_files.append(hdl_file.path) 

104 else: 

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

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

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

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

109 # a tcl command like: 

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

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

112 

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

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

115 

116 if vhdl_files: 

117 files_string = self._to_file_list(vhdl_files) 

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

119 if verilog_source_files: 

120 files_string = self._to_file_list(verilog_source_files) 

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

122 

123 return tcl 

124 

125 @staticmethod 

126 def _to_file_list(file_paths): 

127 """ 

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

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

130 """ 

131 if len(file_paths) == 1: 

132 files_string = to_tcl_path(file_paths[0]) 

133 else: 

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

135 

136 return f"{{{files_string}}}" 

137 

138 @staticmethod 

139 def _add_tcl_sources(tcl_sources): 

140 if tcl_sources is None: 

141 return "" 

142 

143 tcl = "" 

144 for tcl_source_file in tcl_sources: 

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

146 return tcl 

147 

148 @staticmethod 

149 def _add_ip_cores(modules, other_arguments): 

150 tcl = "" 

151 for module in modules: 

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

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

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

155 

156 if ip_core_file.variables: 

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

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

159 

160 tcl += f"""\ 

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

162}} 

163{create_function_name} 

164""" 

165 

166 return tcl 

167 

168 def _add_build_step_hooks(self, build_step_hooks, project_folder): 

169 if build_step_hooks is None: 

170 return "" 

171 

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

173 # to the format step_name: [list of hooks] 

174 hook_steps = {} 

175 for build_step_hook in build_step_hooks: 

176 if build_step_hook.hook_step in hook_steps: 

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

178 else: 

179 hook_steps[build_step_hook.hook_step] = [build_step_hook] 

180 

181 tcl = "" 

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

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

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

185 # and add that as the hook to Vivado. 

186 if len(hooks) == 1: 

187 tcl_file = hooks[0].tcl_file 

188 else: 

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

190 source_hooks_tcl = "".join( 

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

192 ) 

193 create_file(tcl_file, source_hooks_tcl) 

194 

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

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

197 

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

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

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

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

202 

203 return tcl 

204 

205 def _add_project_settings(self): 

206 tcl = "" 

207 

208 # Default value for when opening project in GUI. 

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

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

211 

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

213 # stop the synthesis and produce an error. 

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

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

216 

217 # Enable binary bitstream as well 

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

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

220 

221 return tcl 

222 

223 @staticmethod 

224 def _tcl_for_each_run(run_wildcard, tcl_block): 

225 """ 

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

227 """ 

228 tcl = "" 

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

230 tcl += tcl_block + "\n" 

231 tcl += "}\n" 

232 return tcl 

233 

234 @staticmethod 

235 def _add_generics(generics): 

236 """ 

237 Generics are set according to this weird format: 

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

239 """ 

240 if not generics: 

241 return "" 

242 

243 generic_list = [] 

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

245 value_tcl_formatted = get_vivado_tcl_generic_value(value=value) 

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

247 

248 generics_string = " ".join(generic_list) 

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

250 

251 @staticmethod 

252 def _iterate_constraints(modules, constraints, other_arguments): 

253 for module in modules: 

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

255 yield constraint 

256 

257 if constraints is not None: 

258 for constraint in constraints: 

259 yield constraint 

260 

261 @staticmethod 

262 def _add_constraints(constraints): 

263 tcl = "" 

264 for constraint in constraints: 

265 constraint_file = to_tcl_path(constraint.file) 

266 

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

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

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

270 

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

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

273 

274 if constraint.used_in == "impl": 

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

276 elif constraint.used_in == "synth": 

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

278 

279 return tcl 

280 

281 def build( 

282 self, 

283 project_file, 

284 output_path, 

285 num_threads, 

286 run_index, 

287 generics=None, 

288 synth_only=False, 

289 from_impl=False, 

290 analyze_synthesis_timing=True, 

291 ): 

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

293 num_threads_general = min(num_threads, 32) 

294 num_threads_synth = min(num_threads, 8) 

295 

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

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

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

299 tcl += "\n" 

300 tcl += self._add_generics(generics) 

301 tcl += "\n" 

302 

303 if not from_impl: 

304 synth_run = f"synth_{run_index}" 

305 

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

307 tcl += "\n" 

308 

309 if not synth_only: 

310 impl_run = f"impl_{run_index}" 

311 

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

313 tcl += "\n" 

314 tcl += self._write_hw_platform(output_path) 

315 tcl += "\n" 

316 

317 tcl += "exit\n" 

318 

319 return tcl 

320 

321 def _synthesis(self, run, num_threads, analyze_synthesis_timing): 

322 tcl = self._run(run, num_threads) 

323 if analyze_synthesis_timing: 

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

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

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

327 # checkers to the build script. 

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

329 tcl += """ 

330open_run ${run} 

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

332 

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

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

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

336 

337 

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

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

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

341""" 

342 

343 tcl += """ 

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

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

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

347timing_summary.rpt in ${run_directory}." 

348 

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

350 report_clock_interaction -delay_type min_max -file ${output_file} 

351 

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

353 report_timing_summary -file ${output_file} 

354 

355 exit 1 

356} 

357""" 

358 return tcl 

359 

360 @staticmethod 

361 def _run(run, num_threads, to_step=None): 

362 to_step = "" if to_step is None else " -to_step " + to_step 

363 

364 tcl = f""" 

365set run {run} 

366reset_run ${{run}} 

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

368""" 

369 

370 tcl += """ 

371wait_on_run ${run} 

372 

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

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

375 exit 1 

376} 

377""" 

378 return tcl 

379 

380 def _write_hw_platform(self, output_path): 

381 xsa_file = to_tcl_path(output_path / (self.name + ".xsa")) 

382 tcl = f"write_hw_platform -fixed -force {{{xsa_file}}}\n" 

383 return tcl