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

188 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-29 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 

9from tsfpga.system_utils import create_file 

10from .common import to_tcl_path 

11from .generics import get_vivado_tcl_generic_value 

12 

13 

14class VivadoTcl: 

15 """ 

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

17 """ 

18 

19 def __init__( 

20 self, 

21 name, 

22 ): 

23 self.name = name 

24 

25 # pylint: disable=too-many-arguments 

26 def create( 

27 self, 

28 project_folder, 

29 modules, 

30 part, 

31 top, 

32 run_index, 

33 generics=None, 

34 constraints=None, 

35 tcl_sources=None, 

36 build_step_hooks=None, 

37 ip_cache_path=None, 

38 disable_io_buffers=True, 

39 # Add no sources other than IP cores 

40 ip_cores_only=False, 

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

42 other_arguments=None, 

43 ): 

44 generics = {} if generics is None else generics 

45 other_arguments = {} if other_arguments is None else other_arguments 

46 

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

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

49 

50 if ip_cache_path is not None: 

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

52 tcl += "\n" 

53 

54 tcl += self._add_tcl_sources(tcl_sources) 

55 tcl += "\n" 

56 

57 if not ip_cores_only: 

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

59 tcl += "\n" 

60 tcl += self._add_generics(generics) 

61 tcl += "\n" 

62 tcl += self._add_constraints( 

63 self._iterate_constraints( 

64 modules=modules, constraints=constraints, other_arguments=other_arguments 

65 ) 

66 ) 

67 tcl += "\n" 

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

69 tcl += "\n" 

70 

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

72 tcl += "\n" 

73 tcl += self._add_project_settings() 

74 tcl += "\n" 

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

76 tcl += "\n" 

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

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

79 tcl += "\n" 

80 

81 if disable_io_buffers: 

82 tcl += ( 

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

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

85 ) 

86 tcl += "\n" 

87 

88 tcl += "exit\n" 

89 return tcl 

90 

91 def _add_module_source_files(self, modules, other_arguments): 

92 tcl = "" 

93 for module in modules: 

94 vhdl_files = [] 

95 verilog_source_files = [] 

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

97 if hdl_file.is_vhdl: 

98 vhdl_files.append(hdl_file.path) 

99 elif hdl_file.is_verilog_source: 

100 verilog_source_files.append(hdl_file.path) 

101 else: 

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

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

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

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

106 # a tcl command like: 

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

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

109 

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

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

112 

113 if vhdl_files: 

114 files_string = self._to_file_list(vhdl_files) 

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

116 if verilog_source_files: 

117 files_string = self._to_file_list(verilog_source_files) 

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

119 

120 return tcl 

121 

122 @staticmethod 

123 def _to_file_list(file_paths): 

124 """ 

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

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

127 """ 

128 if len(file_paths) == 1: 

129 files_string = to_tcl_path(file_paths[0]) 

130 else: 

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

132 

133 return f"{{{files_string}}}" 

134 

135 @staticmethod 

136 def _add_tcl_sources(tcl_sources): 

137 if tcl_sources is None: 

138 return "" 

139 

140 tcl = "" 

141 for tcl_source_file in tcl_sources: 

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

143 return tcl 

144 

145 @staticmethod 

146 def _add_ip_cores(modules, other_arguments): 

147 tcl = "" 

148 for module in modules: 

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

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

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

152 

153 if ip_core_file.variables: 

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

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

156 

157 tcl += f"""\ 

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

159}} 

160{create_function_name} 

161""" 

162 

163 return tcl 

164 

165 def _add_build_step_hooks(self, build_step_hooks, project_folder): 

166 if build_step_hooks is None: 

167 return "" 

168 

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

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

171 hook_steps = {} 

172 for build_step_hook in build_step_hooks: 

173 if build_step_hook.hook_step in hook_steps: 

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

175 else: 

176 hook_steps[build_step_hook.hook_step] = [build_step_hook] 

177 

178 tcl = "" 

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

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

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

182 # and add that as the hook to Vivado. 

183 if len(hooks) == 1: 

184 tcl_file = hooks[0].tcl_file 

185 else: 

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

187 source_hooks_tcl = "".join( 

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

189 ) 

190 create_file(tcl_file, source_hooks_tcl) 

191 

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

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

194 

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

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

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

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

199 

200 return tcl 

201 

202 def _add_project_settings(self): 

203 tcl = "" 

204 

205 # Default value for when opening project in GUI. 

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

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

208 

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

210 # stop the synthesis and produce an error. 

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

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

213 

214 # Enable binary bitstream as well 

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

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

217 

218 return tcl 

219 

220 @staticmethod 

221 def _tcl_for_each_run(run_wildcard, tcl_block): 

222 """ 

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

224 """ 

225 tcl = "" 

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

227 tcl += tcl_block + "\n" 

228 tcl += "}\n" 

229 return tcl 

230 

231 @staticmethod 

232 def _add_generics(generics): 

233 """ 

234 Generics are set according to this weird format: 

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

236 """ 

237 if not generics: 

238 return "" 

239 

240 generic_list = [] 

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

242 value_tcl_formatted = get_vivado_tcl_generic_value(value=value) 

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

244 

245 generics_string = " ".join(generic_list) 

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

247 

248 @staticmethod 

249 def _iterate_constraints(modules, constraints, other_arguments): 

250 for module in modules: 

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

252 yield constraint 

253 

254 if constraints is not None: 

255 for constraint in constraints: 

256 yield constraint 

257 

258 @staticmethod 

259 def _add_constraints(constraints): 

260 tcl = "" 

261 for constraint in constraints: 

262 constraint_file = to_tcl_path(constraint.file) 

263 

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

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

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

267 

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

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

270 

271 if constraint.used_in == "impl": 

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

273 elif constraint.used_in == "synth": 

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

275 

276 return tcl 

277 

278 def build( 

279 self, 

280 project_file, 

281 output_path, 

282 num_threads, 

283 run_index, 

284 generics=None, 

285 synth_only=False, 

286 from_impl=False, 

287 analyze_synthesis_timing=True, 

288 ): 

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

290 num_threads_general = min(num_threads, 32) 

291 num_threads_synth = min(num_threads, 8) 

292 

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

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

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

296 tcl += "\n" 

297 tcl += self._add_generics(generics) 

298 tcl += "\n" 

299 

300 if not from_impl: 

301 synth_run = f"synth_{run_index}" 

302 

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

304 tcl += "\n" 

305 

306 if not synth_only: 

307 impl_run = f"impl_{run_index}" 

308 

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

310 tcl += "\n" 

311 tcl += self._write_hw_platform(output_path) 

312 tcl += "\n" 

313 

314 tcl += "exit\n" 

315 

316 return tcl 

317 

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

319 tcl = self._run(run, num_threads) 

320 if analyze_synthesis_timing: 

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

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

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

324 # checkers to the build script. 

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

326 tcl += """ 

327open_run ${run} 

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

329 

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

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

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

333 

334 

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

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

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

338""" 

339 

340 tcl += """ 

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

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

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

344timing_summary.rpt in ${run_directory}." 

345 

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

347 report_clock_interaction -delay_type min_max -file ${output_file} 

348 

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

350 report_timing_summary -file ${output_file} 

351 

352 exit 1 

353} 

354""" 

355 return tcl 

356 

357 @staticmethod 

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

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

360 

361 tcl = f""" 

362set run {run} 

363reset_run ${{run}} 

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

365""" 

366 

367 tcl += """ 

368wait_on_run ${run} 

369 

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

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

372 exit 1 

373} 

374""" 

375 return tcl 

376 

377 def _write_hw_platform(self, output_path): 

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

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

380 return tcl