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
« 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# --------------------------------------------------------------------------------------------------
9# First party libraries
10from tsfpga.system_utils import create_file
12# Local folder libraries
13from .common import to_tcl_path
14from .generics import get_vivado_tcl_generic_value
17class VivadoTcl:
18 """
19 Class with methods for translating a set of sources into Vivado TCL
20 """
22 def __init__(
23 self,
24 name,
25 ):
26 self.name = name
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
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"
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"
57 tcl += self._add_tcl_sources(tcl_sources)
58 tcl += "\n"
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"
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"
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"
91 tcl += "exit\n"
92 return tcl
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
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.
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"
123 return tcl
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])
136 return f"{{{files_string}}}"
138 @staticmethod
139 def _add_tcl_sources(tcl_sources):
140 if tcl_sources is None:
141 return ""
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
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"
156 if ip_core_file.variables:
157 for key, value in ip_core_file.variables.items():
158 tcl += f' set {key} "{value}"\n'
160 tcl += f"""\
161 source -notrace {{{to_tcl_path(ip_core_file.path)}}}
162}}
163{create_function_name}
164"""
166 return tcl
168 def _add_build_step_hooks(self, build_step_hooks, project_folder):
169 if build_step_hooks is None:
170 return ""
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]
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)
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"
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)
203 return tcl
205 def _add_project_settings(self):
206 tcl = ""
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"
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)
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)
221 return tcl
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
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 ""
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}")
248 generics_string = " ".join(generic_list)
249 return f"set_property generic {{{generics_string}}} [current_fileset]\n"
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
257 if constraints is not None:
258 for constraint in constraints:
259 yield constraint
261 @staticmethod
262 def _add_constraints(constraints):
263 tcl = ""
264 for constraint in constraints:
265 constraint_file = to_tcl_path(constraint.file)
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"
271 get_file = f"[get_files {{{constraint_file}}}]"
272 tcl += f"set_property PROCESSING_ORDER {constraint.processing_order} {get_file}\n"
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"
279 return tcl
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)
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"
303 if not from_impl:
304 synth_run = f"synth_{run_index}"
306 tcl += self._synthesis(synth_run, num_threads, analyze_synthesis_timing)
307 tcl += "\n"
309 if not synth_only:
310 impl_run = f"impl_{run_index}"
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"
317 tcl += "exit\n"
319 return tcl
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}]]
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}
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"""
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}."
349 set output_file [file join ${run_directory} "clock_interaction.rpt"]
350 report_clock_interaction -delay_type min_max -file ${output_file}
352 set output_file [file join ${run_directory} "timing_summary.rpt"]
353 report_timing_summary -file ${output_file}
355 exit 1
356}
357"""
358 return tcl
360 @staticmethod
361 def _run(run, num_threads, to_step=None):
362 to_step = "" if to_step is None else " -to_step " + to_step
364 tcl = f"""
365set run {run}
366reset_run ${{run}}
367launch_runs ${{run}} -jobs {num_threads}{to_step}
368"""
370 tcl += """
371wait_on_run ${run}
373if {[get_property PROGRESS [get_runs ${run}]] != "100%"} {
374 puts "ERROR: Run ${run} failed."
375 exit 1
376}
377"""
378 return tcl
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