build.plat: make `verbose` work like all other overrides.
[nmigen.git] / nmigen / build / plat.py
1 from collections import OrderedDict
2 from abc import ABCMeta, abstractmethod, abstractproperty
3 import os
4 import textwrap
5 import re
6 import jinja2
7
8 from .. import __version__
9 from .._toolchain import *
10 from ..hdl import *
11 from ..hdl.xfrm import SampleLowerer, DomainLowerer
12 from ..lib.cdc import ResetSynchronizer
13 from ..back import rtlil, verilog
14 from .res import *
15 from .run import *
16
17
18 __all__ = ["Platform", "TemplatedPlatform"]
19
20
21 class Platform(ResourceManager, metaclass=ABCMeta):
22 resources = abstractproperty()
23 connectors = abstractproperty()
24 default_clk = None
25 default_rst = None
26 required_tools = abstractproperty()
27
28 def __init__(self):
29 super().__init__(self.resources, self.connectors)
30
31 self.extra_files = OrderedDict()
32
33 self._prepared = False
34
35 @property
36 def default_clk_constraint(self):
37 if self.default_clk is None:
38 raise AttributeError("Platform '{}' does not define a default clock"
39 .format(type(self).__name__))
40 return self.lookup(self.default_clk).clock
41
42 @property
43 def default_clk_frequency(self):
44 constraint = self.default_clk_constraint
45 if constraint is None:
46 raise AttributeError("Platform '{}' does not constrain its default clock"
47 .format(type(self).__name__))
48 return constraint.frequency
49
50 def add_file(self, filename, content):
51 if not isinstance(filename, str):
52 raise TypeError("File name must be a string, not {!r}"
53 .format(filename))
54 if hasattr(content, "read"):
55 content = content.read()
56 elif not isinstance(content, (str, bytes)):
57 raise TypeError("File contents must be str, bytes, or a file-like object, not {!r}"
58 .format(content))
59 if filename in self.extra_files:
60 if self.extra_files[filename] != content:
61 raise ValueError("File {!r} already exists"
62 .format(filename))
63 else:
64 self.extra_files[filename] = content
65
66 def iter_files(self, *suffixes):
67 for filename in self.extra_files:
68 if filename.endswith(suffixes):
69 yield filename
70
71 @property
72 def _toolchain_env_var(self):
73 return f"NMIGEN_ENV_{self.toolchain}"
74
75 def build(self, elaboratable, name="top",
76 build_dir="build", do_build=True,
77 program_opts=None, do_program=False,
78 **kwargs):
79 # The following code performs a best-effort check for presence of required tools upfront,
80 # before performing any build actions, to provide a better diagnostic. It does not handle
81 # several corner cases:
82 # 1. `require_tool` does not source toolchain environment scripts, so if such a script
83 # is used, the check is skipped, and `execute_local()` may fail;
84 # 2. if the design is not built (do_build=False), most of the tools are not required and
85 # in fact might not be available if the design will be built manually with a different
86 # environment script specified, or on a different machine; however, Yosys is required
87 # by virtually every platform anyway, to provide debug Verilog output, and `prepare()`
88 # may fail.
89 # This is OK because even if `require_tool` succeeds, the toolchain might be broken anyway.
90 # The check only serves to catch common errors earlier.
91 if do_build and self._toolchain_env_var not in os.environ:
92 for tool in self.required_tools:
93 require_tool(tool)
94
95 plan = self.prepare(elaboratable, name, **kwargs)
96 if not do_build:
97 return plan
98
99 products = plan.execute_local(build_dir)
100 if not do_program:
101 return products
102
103 self.toolchain_program(products, name, **(program_opts or {}))
104
105 def has_required_tools(self):
106 if self._toolchain_env_var in os.environ:
107 return True
108 return all(has_tool(name) for name in self.required_tools)
109
110 def create_missing_domain(self, name):
111 # Simple instantiation of a clock domain driven directly by the board clock and reset.
112 # This implementation uses a single ResetSynchronizer to ensure that:
113 # * an external reset is definitely synchronized to the system clock;
114 # * release of power-on reset, which is inherently asynchronous, is synchronized to
115 # the system clock.
116 # Many device families provide advanced primitives for tackling reset. If these exist,
117 # they should be used instead.
118 if name == "sync" and self.default_clk is not None:
119 clk_i = self.request(self.default_clk).i
120 if self.default_rst is not None:
121 rst_i = self.request(self.default_rst).i
122 else:
123 rst_i = Const(0)
124
125 m = Module()
126 m.domains += ClockDomain("sync")
127 m.d.comb += ClockSignal("sync").eq(clk_i)
128 m.submodules.reset_sync = ResetSynchronizer(rst_i, domain="sync")
129 return m
130
131 def prepare(self, elaboratable, name="top", **kwargs):
132 assert not self._prepared
133 self._prepared = True
134
135 fragment = Fragment.get(elaboratable, self)
136 fragment = SampleLowerer()(fragment)
137 fragment._propagate_domains(self.create_missing_domain, platform=self)
138 fragment = DomainLowerer()(fragment)
139
140 def add_pin_fragment(pin, pin_fragment):
141 pin_fragment = Fragment.get(pin_fragment, self)
142 if not isinstance(pin_fragment, Instance):
143 pin_fragment.flatten = True
144 fragment.add_subfragment(pin_fragment, name="pin_{}".format(pin.name))
145
146 for pin, port, attrs, invert in self.iter_single_ended_pins():
147 if pin.dir == "i":
148 add_pin_fragment(pin, self.get_input(pin, port, attrs, invert))
149 if pin.dir == "o":
150 add_pin_fragment(pin, self.get_output(pin, port, attrs, invert))
151 if pin.dir == "oe":
152 add_pin_fragment(pin, self.get_tristate(pin, port, attrs, invert))
153 if pin.dir == "io":
154 add_pin_fragment(pin, self.get_input_output(pin, port, attrs, invert))
155
156 for pin, port, attrs, invert in self.iter_differential_pins():
157 if pin.dir == "i":
158 add_pin_fragment(pin, self.get_diff_input(pin, port, attrs, invert))
159 if pin.dir == "o":
160 add_pin_fragment(pin, self.get_diff_output(pin, port, attrs, invert))
161 if pin.dir == "oe":
162 add_pin_fragment(pin, self.get_diff_tristate(pin, port, attrs, invert))
163 if pin.dir == "io":
164 add_pin_fragment(pin, self.get_diff_input_output(pin, port, attrs, invert))
165
166 fragment._propagate_ports(ports=self.iter_ports(), all_undef_as_ports=False)
167 return self.toolchain_prepare(fragment, name, **kwargs)
168
169 @abstractmethod
170 def toolchain_prepare(self, fragment, name, **kwargs):
171 """
172 Convert the ``fragment`` and constraints recorded in this :class:`Platform` into
173 a :class:`BuildPlan`.
174 """
175 raise NotImplementedError # :nocov:
176
177 def toolchain_program(self, products, name, **kwargs):
178 """
179 Extract bitstream for fragment ``name`` from ``products`` and download it to a target.
180 """
181 raise NotImplementedError("Platform '{}' does not support programming"
182 .format(type(self).__name__))
183
184 def _check_feature(self, feature, pin, attrs, valid_xdrs, valid_attrs):
185 if len(valid_xdrs) == 0:
186 raise NotImplementedError("Platform '{}' does not support {}"
187 .format(type(self).__name__, feature))
188 elif pin.xdr not in valid_xdrs:
189 raise NotImplementedError("Platform '{}' does not support {} for XDR {}"
190 .format(type(self).__name__, feature, pin.xdr))
191
192 if not valid_attrs and attrs:
193 raise NotImplementedError("Platform '{}' does not support attributes for {}"
194 .format(type(self).__name__, feature))
195
196 @staticmethod
197 def _invert_if(invert, value):
198 if invert:
199 return ~value
200 else:
201 return value
202
203 def get_input(self, pin, port, attrs, invert):
204 self._check_feature("single-ended input", pin, attrs,
205 valid_xdrs=(0,), valid_attrs=None)
206
207 m = Module()
208 m.d.comb += pin.i.eq(self._invert_if(invert, port))
209 return m
210
211 def get_output(self, pin, port, attrs, invert):
212 self._check_feature("single-ended output", pin, attrs,
213 valid_xdrs=(0,), valid_attrs=None)
214
215 m = Module()
216 m.d.comb += port.eq(self._invert_if(invert, pin.o))
217 return m
218
219 def get_tristate(self, pin, port, attrs, invert):
220 self._check_feature("single-ended tristate", pin, attrs,
221 valid_xdrs=(0,), valid_attrs=None)
222
223 m = Module()
224 m.submodules += Instance("$tribuf",
225 p_WIDTH=pin.width,
226 i_EN=pin.oe,
227 i_A=self._invert_if(invert, pin.o),
228 o_Y=port,
229 )
230 return m
231
232 def get_input_output(self, pin, port, attrs, invert):
233 self._check_feature("single-ended input/output", pin, attrs,
234 valid_xdrs=(0,), valid_attrs=None)
235
236 m = Module()
237 m.submodules += Instance("$tribuf",
238 p_WIDTH=pin.width,
239 i_EN=pin.oe,
240 i_A=self._invert_if(invert, pin.o),
241 o_Y=port,
242 )
243 m.d.comb += pin.i.eq(self._invert_if(invert, port))
244 return m
245
246 def get_diff_input(self, pin, port, attrs, invert):
247 self._check_feature("differential input", pin, attrs,
248 valid_xdrs=(), valid_attrs=None)
249
250 def get_diff_output(self, pin, port, attrs, invert):
251 self._check_feature("differential output", pin, attrs,
252 valid_xdrs=(), valid_attrs=None)
253
254 def get_diff_tristate(self, pin, port, attrs, invert):
255 self._check_feature("differential tristate", pin, attrs,
256 valid_xdrs=(), valid_attrs=None)
257
258 def get_diff_input_output(self, pin, port, attrs, invert):
259 self._check_feature("differential input/output", pin, attrs,
260 valid_xdrs=(), valid_attrs=None)
261
262
263 class TemplatedPlatform(Platform):
264 toolchain = abstractproperty()
265 file_templates = abstractproperty()
266 command_templates = abstractproperty()
267
268 build_script_templates = {
269 "build_{{name}}.sh": """
270 # {{autogenerated}}
271 set -e{{verbose("x")}}
272 [ -n "${{platform._toolchain_env_var}}" ] && . "${{platform._toolchain_env_var}}"
273 {{emit_commands("sh")}}
274 """,
275 "build_{{name}}.bat": """
276 @rem {{autogenerated}}
277 {{quiet("@echo off")}}
278 if defined {{platform._toolchain_env_var}} call %{{platform._toolchain_env_var}}%
279 {{emit_commands("bat")}}
280 """,
281 }
282
283 def iter_clock_constraints(self):
284 for net_signal, port_signal, frequency in super().iter_clock_constraints():
285 # Skip any clock constraints placed on signals that are never used in the design.
286 # Otherwise, it will cause a crash in the vendor platform if it supports clock
287 # constraints on non-port nets.
288 if net_signal not in self._name_map:
289 continue
290 yield net_signal, port_signal, frequency
291
292 def toolchain_prepare(self, fragment, name, **kwargs):
293 # Restrict the name of the design to a strict alphanumeric character set. Platforms will
294 # interpolate the name of the design in many different contexts: filesystem paths, Python
295 # scripts, Tcl scripts, ad-hoc constraint files, and so on. It is not practical to add
296 # escaping code that handles every one of their edge cases, so make sure we never hit them
297 # in the first place.
298 invalid_char = re.match(r"[^A-Za-z0-9_]", name)
299 if invalid_char:
300 raise ValueError("Design name {!r} contains invalid character {!r}; only alphanumeric "
301 "characters are valid in design names"
302 .format(name, invalid_char.group(0)))
303
304 # This notice serves a dual purpose: to explain that the file is autogenerated,
305 # and to incorporate the nMigen version into generated code.
306 autogenerated = "Automatically generated by nMigen {}. Do not edit.".format(__version__)
307
308 rtlil_text, self._name_map = rtlil.convert_fragment(fragment, name=name)
309
310 def emit_rtlil():
311 return rtlil_text
312
313 def emit_verilog(opts=()):
314 return verilog._convert_rtlil_text(rtlil_text,
315 strip_internal_attrs=True, write_verilog_opts=opts)
316
317 def emit_debug_verilog(opts=()):
318 return verilog._convert_rtlil_text(rtlil_text,
319 strip_internal_attrs=False, write_verilog_opts=opts)
320
321 def emit_commands(syntax):
322 commands = []
323
324 for name in self.required_tools:
325 env_var = tool_env_var(name)
326 if syntax == "sh":
327 template = ": ${{{env_var}:={name}}}"
328 elif syntax == "bat":
329 template = \
330 "if [%{env_var}%] equ [\"\"] set {env_var}=\n" \
331 "if [%{env_var}%] equ [] set {env_var}={name}"
332 else:
333 assert False
334 commands.append(template.format(env_var=env_var, name=name))
335
336 for index, command_tpl in enumerate(self.command_templates):
337 command = render(command_tpl, origin="<command#{}>".format(index + 1),
338 syntax=syntax)
339 command = re.sub(r"\s+", " ", command)
340 if syntax == "sh":
341 commands.append(command)
342 elif syntax == "bat":
343 commands.append(command + " || exit /b")
344 else:
345 assert False
346
347 return "\n".join(commands)
348
349 def get_override(var):
350 var_env = "NMIGEN_{}".format(var)
351 if var_env in os.environ:
352 # On Windows, there is no way to define an "empty but set" variable; it is tempting
353 # to use a quoted empty string, but it doesn't do what one would expect. Recognize
354 # this as a useful pattern anyway, and treat `set VAR=""` on Windows the same way
355 # `export VAR=` is treated on Linux.
356 return re.sub(r'^\"\"$', "", os.environ[var_env])
357 elif var in kwargs:
358 if isinstance(kwargs[var], str):
359 return textwrap.dedent(kwargs[var]).strip()
360 else:
361 return kwargs[var]
362 else:
363 return jinja2.Undefined(name=var)
364
365 @jinja2.contextfunction
366 def invoke_tool(context, name):
367 env_var = tool_env_var(name)
368 if context.parent["syntax"] == "sh":
369 return "\"${}\"".format(env_var)
370 elif context.parent["syntax"] == "bat":
371 return "%{}%".format(env_var)
372 else:
373 assert False
374
375 def options(opts):
376 if isinstance(opts, str):
377 return opts
378 else:
379 return " ".join(opts)
380
381 def hierarchy(signal, separator):
382 return separator.join(self._name_map[signal][1:])
383
384 def ascii_escape(string):
385 def escape_one(match):
386 if match.group(1) is None:
387 return match.group(2)
388 else:
389 return "_{:02x}_".format(ord(match.group(1)[0]))
390 return "".join(escape_one(m) for m in re.finditer(r"([^A-Za-z0-9_])|(.)", string))
391
392 def tcl_escape(string):
393 return "{" + re.sub(r"([{}\\])", r"\\\1", string) + "}"
394
395 def tcl_quote(string):
396 return '"' + re.sub(r"([$[\\])", r"\\\1", string) + '"'
397
398 def verbose(arg):
399 if get_override("verbose"):
400 return arg
401 else:
402 return jinja2.Undefined(name="quiet")
403
404 def quiet(arg):
405 if get_override("verbose"):
406 return jinja2.Undefined(name="quiet")
407 else:
408 return arg
409
410 def render(source, origin, syntax=None):
411 try:
412 source = textwrap.dedent(source).strip()
413 compiled = jinja2.Template(source,
414 trim_blocks=True, lstrip_blocks=True, undefined=jinja2.StrictUndefined)
415 compiled.environment.filters["options"] = options
416 compiled.environment.filters["hierarchy"] = hierarchy
417 compiled.environment.filters["ascii_escape"] = ascii_escape
418 compiled.environment.filters["tcl_escape"] = tcl_escape
419 compiled.environment.filters["tcl_quote"] = tcl_quote
420 except jinja2.TemplateSyntaxError as e:
421 e.args = ("{} (at {}:{})".format(e.message, origin, e.lineno),)
422 raise
423 return compiled.render({
424 "name": name,
425 "platform": self,
426 "emit_rtlil": emit_rtlil,
427 "emit_verilog": emit_verilog,
428 "emit_debug_verilog": emit_debug_verilog,
429 "emit_commands": emit_commands,
430 "syntax": syntax,
431 "invoke_tool": invoke_tool,
432 "get_override": get_override,
433 "verbose": verbose,
434 "quiet": quiet,
435 "autogenerated": autogenerated,
436 })
437
438 plan = BuildPlan(script="build_{}".format(name))
439 for filename_tpl, content_tpl in self.file_templates.items():
440 plan.add_file(render(filename_tpl, origin=filename_tpl),
441 render(content_tpl, origin=content_tpl))
442 for filename, content in self.extra_files.items():
443 plan.add_file(filename, content)
444 return plan