Coverage for tsfpga/vivado/tcl.py: 98%
188 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-28 04:01 +0000
« prev ^ index » next coverage.py v6.4, created at 2022-05-28 04:01 +0000
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# --------------------------------------------------------------------------------------------------
9from tsfpga.system_utils import create_file
10from .common import to_tcl_path
11from .generics import get_vivado_tcl_generic_value
14class VivadoTcl:
15 """
16 Class with methods for translating a set of sources into Vivado TCL
17 """
19 def __init__(
20 self,
21 name,
22 ):
23 self.name = name
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
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"
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"
54 tcl += self._add_tcl_sources(tcl_sources)
55 tcl += "\n"
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"
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"
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"
88 tcl += "exit\n"
89 return tcl
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
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.
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"
120 return tcl
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])
133 return f"{{{files_string}}}"
135 @staticmethod
136 def _add_tcl_sources(tcl_sources):
137 if tcl_sources is None:
138 return ""
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
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"
153 if ip_core_file.variables:
154 for key, value in ip_core_file.variables.items():
155 tcl += f' set {key} "{value}"\n'
157 tcl += f"""\
158 source -notrace {{{to_tcl_path(ip_core_file.path)}}}
159}}
160{create_function_name}
161"""
163 return tcl
165 def _add_build_step_hooks(self, build_step_hooks, project_folder):
166 if build_step_hooks is None:
167 return ""
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]
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)
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"
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)
200 return tcl
202 def _add_project_settings(self):
203 tcl = ""
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"
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)
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)
218 return tcl
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
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 ""
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}")
245 generics_string = " ".join(generic_list)
246 return f"set_property generic {{{generics_string}}} [current_fileset]\n"
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
254 if constraints is not None:
255 for constraint in constraints:
256 yield constraint
258 @staticmethod
259 def _add_constraints(constraints):
260 tcl = ""
261 for constraint in constraints:
262 constraint_file = to_tcl_path(constraint.file)
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"
268 get_file = f"[get_files {{{constraint_file}}}]"
269 tcl += f"set_property PROCESSING_ORDER {constraint.processing_order} {get_file}\n"
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"
276 return tcl
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)
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"
300 if not from_impl:
301 synth_run = f"synth_{run_index}"
303 tcl += self._synthesis(synth_run, num_threads, analyze_synthesis_timing)
304 tcl += "\n"
306 if not synth_only:
307 impl_run = f"impl_{run_index}"
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"
314 tcl += "exit\n"
316 return tcl
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}]]
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}
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"""
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}."
346 set output_file [file join ${run_directory} "clock_interaction.rpt"]
347 report_clock_interaction -delay_type min_max -file ${output_file}
349 set output_file [file join ${run_directory} "timing_summary.rpt"]
350 report_timing_summary -file ${output_file}
352 exit 1
353}
354"""
355 return tcl
357 @staticmethod
358 def _run(run, num_threads, to_step=None):
359 to_step = "" if to_step is None else " -to_step " + to_step
361 tcl = f"""
362set run {run}
363reset_run ${{run}}
364launch_runs ${{run}} -jobs {num_threads}{to_step}
365"""
367 tcl += """
368wait_on_run ${run}
370if {[get_property PROGRESS [get_runs ${run}]] != "100%"} {
371 puts "ERROR: Run ${run} failed."
372 exit 1
373}
374"""
375 return tcl
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