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