diff --git a/edalize/edatool.py b/edalize/edatool.py index 85fe5f6ee..0631906bf 100644 --- a/edalize/edatool.py +++ b/edalize/edatool.py @@ -213,6 +213,15 @@ def run(self, args={}): self.run_main() self.run_post() + def check_args(self, unknown): + # If a tool is using subtools some of the argument may be + # parsed by the subtool. This function is used to check if + # all the provided arguments were correct. A tool can override + # this function to provide custom args checking logic. + + if unknown: + raise Exception(f'Unknown command line option {unknown[0]}') + def run_pre(self, args=None): if type(args) == list: parsed_args = self.parse_args(args, self.argtypes) @@ -239,6 +248,7 @@ def __init__(self, command, targets, depends): def __init__(self): self.commands = [] self.header = "#Auto generated by Edalize\n\n" + self.header += "SHELL := /bin/bash\n\n" def add(self, command, targets, depends): self.commands.append(self.Command(command, targets, depends)) @@ -321,7 +331,9 @@ def parse_args(self, args, paramtypes): help=_opt['desc']) args_dict = {} - for key, value in vars(parser.parse_args(args)).items(): + known, unknown = parser.parse_known_args(args) + self.check_args(unknown) + for key, value in vars(known).items(): if value is None: continue if type(value) == list: @@ -408,8 +420,12 @@ def _run_scripts(self, scripts, hook_name): cp = run(script['cmd'], cwd = self.work_root, env = _env, - capture_output=not self.verbose, + stdin = subprocess.PIPE, + stdout = subprocess.PIPE, + stderr = subprocess.STDOUT, + capture_output=not self.verbose, check = True) + logger.info(cp.stdout) except FileNotFoundError as e: msg = "Unable to run {} script '{}': {}" raise RuntimeError(msg.format(hook_name, script['name'], str(e))) @@ -431,17 +447,18 @@ def _run_tool(self, cmd, args=[], quiet=False): capture_output = quiet and not (self.verbose or self.stdout or self.stderr) try: cp = run([cmd] + args, - cwd = self.work_root, - stdin=subprocess.PIPE, - stdout=self.stdout, - stderr=self.stderr, - capture_output=capture_output, - check=True) + cwd = self.work_root, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + capture_output=capture_output, + check=True) + logger.info(cp.stdout) except FileNotFoundError: _s = "Command '{}' not found. Make sure it is in $PATH".format(cmd) raise RuntimeError(_s) except subprocess.CalledProcessError as e: - _s = "'{}' exited with an error: {}".format(e.cmd, e.returncode) + _s = "'{}' exited with an error: {}\n{}".format(e.cmd, e.returncode, e.output.decode()) logger.debug(_s) if e.stdout: @@ -450,7 +467,7 @@ def _run_tool(self, cmd, args=[], quiet=False): logger.error(e.stderr.decode()) logger.debug("=== STDERR ===") logger.debug(e.stderr) - + raise RuntimeError(_s) return cp.returncode, cp.stdout, cp.stderr diff --git a/edalize/icestorm.py b/edalize/icestorm.py index ad6e9ce4e..20af917cb 100644 --- a/edalize/icestorm.py +++ b/edalize/icestorm.py @@ -33,6 +33,9 @@ def get_doc(cls, api_ver): 'members' : options['members'], 'lists' : options['lists']} + def check_args(self, unknown): + Yosys.validate_args(unknown) + def configure_main(self): # Write yosys script file yosys_synth_options = self.tool_options.get('yosys_synth_options', '') @@ -42,8 +45,10 @@ def configure_main(self): {'yosys' : { 'arch' : 'ice40', 'yosys_synth_options' : yosys_synth_options, + 'yosys_read_options' : self.tool_options.get('yosys_read_options', []), 'yosys_as_subtool' : True, 'yosys_template' : self.tool_options.get('yosys_template'), + 'frontend_options' : self.tool_options.get('frontedn_options',[]) }, 'nextpnr' : { 'nextpnr_options' : self.tool_options.get('nextpnr_options', []) @@ -87,14 +92,14 @@ def configure_main(self): #Timing analysis depends = self.name+'.asc' targets = self.name+'.tim' - command = ['icetime', '-tmd', part or '', depends, targets] + command = ['icetime', '-tmd', part or '', depends, "-r", targets] commands.add(command, [targets], [depends]) commands.add([], ["timing"], [targets]) #Statistics depends = self.name+'.asc' targets = self.name+'.stat' - command = ['icebox_stat', depends, targets] + command = ['icebox_stat', depends, ">"+targets] commands.add(command, [targets], [depends]) commands.add([], ["stats"], [targets]) diff --git a/edalize/surelog.py b/edalize/surelog.py new file mode 100644 index 000000000..7775f36f1 --- /dev/null +++ b/edalize/surelog.py @@ -0,0 +1,59 @@ + +import logging +import os.path + +from edalize.edatool import Edatool +import shutil + +logger = logging.getLogger(__name__) + +class Surelog(Edatool): + + argtypes = ['vlogdefine', 'vlogparam'] + + @classmethod + def get_doc(cls, api_ver): + if api_ver == 0: + return {'description' : "Surelog", + 'members' : [ + {'name' : 'arch', + 'type' : 'String', + 'desc' : 'Target architecture. Legal values are *xilinx*, *ice40* and *ecp5*'} + ], + 'lists' : [ + {'name' : 'surelog_options', + 'type' : 'String', + 'desc' : 'List of the Surelog parameters'}, + ]} + + def configure_main(self): + (src_files, incdirs) = self._get_fileset_files() + verilog_file_list = [] + systemverilog_file_list = [] + for f in src_files: + if f.file_type.startswith('verilogSource'): + verilog_file_list.append(f.name) + if f.file_type.startswith('systemVerilogSource'): + systemverilog_file_list.append("-sv " + f.name) + + surelog_options = self.tool_options.get('surelog_options', []) + arch = self.tool_options.get('arch', None) + + pattern = len(self.vlogparam.keys()) * " -P%s=%%s" + verilog_params_command = pattern % tuple(self.vlogparam.keys()) % tuple(self.vlogparam.values()) + + verilog_defines_command = "+define" if self.vlogdefine.items() else "" + pattern = len(self.vlogdefine.keys()) * "+%s=%%s" + verilog_defines_command += pattern % tuple(self.vlogdefine.keys()) % tuple(self.vlogdefine.values()) + + pattern = len(incdirs) * " -I%s" + include_files_command = pattern % tuple(incdirs) + + commands = self.EdaCommands() + share_path = os.path.join(os.path.dirname(shutil.which("uhdm-yosys")), "../share/uhdm-yosys") + commands.add(["surelog", f"{' '.join(surelog_options)}", "-parse", f"{verilog_defines_command}", + f"{verilog_params_command}", f"-top {self.toplevel}", f"{include_files_command}", + f"{' '.join(verilog_file_list)}", f"{' '.join(systemverilog_file_list)}"], + ["slpp_all/surelog.uhdm"], []) + commands.add([f"cp slpp_all/surelog.uhdm {self.toplevel}.uhdm"] ,[self.toplevel + '.uhdm'], ["slpp_all/surelog.uhdm"]) + self.commands = commands.commands diff --git a/edalize/symbiflow.py b/edalize/symbiflow.py index 5b255d02c..f1f18cb00 100644 --- a/edalize/symbiflow.py +++ b/edalize/symbiflow.py @@ -10,6 +10,7 @@ from edalize.edatool import Edatool from edalize.yosys import Yosys +from edalize.surelog import Surelog from importlib import import_module logger = logging.getLogger(__name__) @@ -71,6 +72,31 @@ def get_doc(cls, api_ver): "type": "String", "desc": "Additional options for Nextpnr tool. If not used, default options for the tool will be used", }, + { + "name": 'fasm2bels', + "type": 'Boolean', + "desc": 'Value to state whether fasm2bels is to be used' + }, + { + "name": 'dbroot', + "type": 'String', + "desc": 'Path to the database root (needed by fasm2bels).' + }, + { + "name": 'clocks', + "type": 'dict', + "desc": 'Clocks to be added for having tools correctly handling timing based routing.' + }, + { + "name": 'seed', + "type": 'String', + "desc": 'Seed assigned to the PnR tool.' + }, + { + "name": "schema_dir", + "type": "String", + "desc": "Path if Capnp schema used by fpga_interchange", + }, ], } @@ -90,6 +116,7 @@ def configure_nextpnr(self): # Yosys configuration yosys_synth_options = self.tool_options.get("yosys_synth_options", "") + yosys_additional_commands = self.tool_options.get('yosys_additional_commands', '') yosys_template = self.tool_options.get("yosys_template") yosys_edam = { "files" : self.files, @@ -100,8 +127,11 @@ def configure_nextpnr(self): "yosys" : { "arch" : vendor, "yosys_synth_options" : yosys_synth_options, + "yosys_additional_commands" : yosys_additional_commands, + 'yosys_read_options' : self.tool_options.get('yosys_read_options', []), "yosys_template" : yosys_template, "yosys_as_subtool" : True, + 'frontend_options' : self.tool_options.get('frontend_options', []), } } } @@ -174,14 +204,16 @@ def configure_nextpnr(self): commands = self.EdaCommands() commands.commands = yosys.commands if arch == "fpga_interchange": - commands.header += """ifndef INTERCHANGE_SCHEMA_PATH + schema_dir = self.tool_options.get('schema_dir', None) + if schema_dir is None: + commands.header += """ifndef INTERCHANGE_SCHEMA_PATH $(error Environment variable INTERCHANGE_SCHEMA_PATH was not found. It should be set to /interchange) endif """ targets = self.name+'.netlist' command = ['python', '-m', 'fpga_interchange.yosys_json'] - command += ['--schema_dir', '$(INTERCHANGE_SCHEMA_PATH)'] + command += ['--schema_dir', '$(INTERCHANGE_SCHEMA_PATH)' if schema_dir is None else schema_dir] command += ['--device', device] command += ['--top', self.toplevel] command += [depends, targets] @@ -201,7 +233,7 @@ def configure_nextpnr(self): depends = self.name+'.phys' targets = self.name+'.fasm' command = ['python', '-m', 'fpga_interchange.fasm_generator'] - command += ['--schema_dir', '$(INTERCHANGE_SCHEMA_PATH)'] + command += ['--schema_dir', '$(INTERCHANGE_SCHEMA_PATH)' if schema_dir is None else schema_dir] command += ['--family', family, device, self.name+'.netlist', depends, targets] commands.add(command, [targets], [depends]) else: @@ -227,7 +259,7 @@ def configure_nextpnr(self): def configure_vpr(self): (src_files, incdirs) = self._get_fileset_files(force_slash=True) - has_vhdl = "vhdlSource" in [x.file_type for x in src_files] + has_vhdl = "vhdlSource" in [x.file_type for x in src_files] has_vhdl2008 = "vhdlSource-2008" in [x.file_type for x in src_files] if has_vhdl or has_vhdl2008: @@ -237,6 +269,10 @@ def configure_vpr(self): pins_constraints = [] placement_constraints = [] + vpr_grid = None + rr_graph = None + vpr_capnp_schema = None + for f in src_files: if f.file_type in ["verilogSource"]: file_list.append(f.name) @@ -246,6 +282,14 @@ def configure_vpr(self): pins_constraints.append(f.name) if f.file_type in ["xdc"]: placement_constraints.append(f.name) + if f.file_type in ["RRGraph"]: + rr_graph = f.name + if f.file_type in ["VPRGrid"]: + vpr_grid = f.name + if f.file_type in ['capnp']: + vpr_capnp_schema = f.name + + builddir = self.tool_options.get('builddir', 'build') part = self.tool_options.get("part") package = self.tool_options.get("package") @@ -282,13 +326,36 @@ def configure_vpr(self): sdc_opts = ['-s']+timing_constraints if timing_constraints else [] xdc_opts = ['-x']+placement_constraints if placement_constraints else [] + fasm2bels = self.tool_options.get('fasm2bels', False) + dbroot = self.tool_options.get('dbroot', None) + clocks = self.tool_options.get('clocks', None) + + if fasm2bels: + if any(v is None for v in [rr_graph, vpr_grid, dbroot]): + logger.error("When using fasm2bels, rr_graph, vpr_grid and database root must be provided") + tcl_params = { + 'top': self.toplevel, + 'part': partname, + 'xdc': ' '.join(placement_constraints), + 'clocks': clocks, + } + + self.render_template('symbiflow-fasm2bels-tcl.j2', + 'fasm2bels.tcl', + tcl_params) + self.render_template('vivado-sh.j2', + 'vivado.sh', + dict()) + + seed = self.tool_options.get('seed', None) + commands = self.EdaCommands() #Synthesis targets = self.toplevel+'.eblif' command = ['symbiflow_synth', '-t', self.toplevel] command += ['-v'] + file_list command += ['-d', bitstream_device] - command += ['-p' if vendor == 'xilinx' else '-P', partname] + command += ['-p', partname] command += xdc_opts commands.add(command, [targets], []) @@ -328,6 +395,30 @@ def configure_vpr(self): command += ['-b', targets] commands.add(command, [targets], [depends]) + if fasm2bels: + targets = self.toplevel+".bit.v" + command = ['python -m fasm2bels'] + command += ['--db_root', dbroot+f'/{bitstream_device}'] + command += ['--part', partname] + command += ['--bitread bitread'] + command += ['--bit_file', self.toplevel+'.bit'] + command += ['--fasm_file', self.toplevel+'.bit.fasm'] + command += ['--eblif', self.toplevel+'.eblif'] + command += ['--connection_database channels.db'] + command += ['--rr_graph', rr_graph] + command += ['--route_file', self.toplevel+".route"] + command += ['--vpr_grid_map', vpr_grid] + command += ['--vpr_capnp_schema_dir', vpr_capnp_schema] + if len(pins_constraints) > 0: + command += [f"--pcf {' '.join(pins_constraints)}"] + command += ['--verilog_file', self.toplevel+".bit.v"] + command += ['--xdc_file', self.toplevel+".bit.xdc"] + command += ["&& rm channels.db"] + commands.add(command, [targets], []) + depends = targets + targets = "timing_summary.rpt" + command = ["bash vivado.sh"] + commands.add(command, [targets], [depends]) commands.set_default_target(targets) commands.write(os.path.join(self.work_root, 'Makefile')) diff --git a/edalize/templates/symbiflow/symbiflow-fasm2bels-tcl.j2 b/edalize/templates/symbiflow/symbiflow-fasm2bels-tcl.j2 new file mode 100644 index 000000000..1028419c6 --- /dev/null +++ b/edalize/templates/symbiflow/symbiflow-fasm2bels-tcl.j2 @@ -0,0 +1,16 @@ +create_project -force -part {{ part }} design design + +read_verilog {{ top }}.bit.v +synth_design -top top + +read_xdc {{ top }}.bit.xdc + +write_checkpoint -force design_{{ top }}_after_source.dcp + +{% if clocks %} + {% for key, value in clocks.items() %} +create_clock -period {{ value }} [get_ports {{ key }}] + {% endfor %} +{% endif %} + +report_timing_summary -datasheet -max_paths 10 -file timing_summary.rpt diff --git a/edalize/templates/symbiflow/vivado-sh.j2 b/edalize/templates/symbiflow/vivado-sh.j2 new file mode 100644 index 000000000..3efc7973a --- /dev/null +++ b/edalize/templates/symbiflow/vivado-sh.j2 @@ -0,0 +1,7 @@ +#!/bin/bash + +VIVADO_SETTINGS=`find /opt -wholename "*Xilinx/Vivado/*/settings64.sh" | sort | head -n 1` + +source $VIVADO_SETTINGS + +vivado -mode batch -source fasm2bels.tcl diff --git a/edalize/templates/yosys/yosys-script-tcl.j2 b/edalize/templates/yosys/yosys-script-tcl.j2 index 0b9ce3e3d..d68c85158 100644 --- a/edalize/templates/yosys/yosys-script-tcl.j2 +++ b/edalize/templates/yosys/yosys-script-tcl.j2 @@ -13,6 +13,10 @@ verilog_defaults -pop synth $top +{% for command in additional_commands %} +{{ command }} +{% endfor %} + write_blif $name.blif write_json $name.json -write_edif {{ edif_opts }}$name.edif +write_edif {{ edif_opts }} $name.edif diff --git a/edalize/trellis.py b/edalize/trellis.py index 1102f693b..8df5962d3 100644 --- a/edalize/trellis.py +++ b/edalize/trellis.py @@ -32,8 +32,10 @@ def configure_main(self): {'yosys' : { 'arch' : 'ecp5', 'yosys_synth_options' : self.tool_options.get('yosys_synth_options', []), + 'yosys_read_options' : self.tool_options.get('yosys_read_options'), 'yosys_as_subtool' : True, 'yosys_template' : self.tool_options.get('yosys_template'), + 'frontend_options' : self.tool_options.get('frontend_options'), }, 'nextpnr' : { 'nextpnr_options' : self.tool_options.get('nextpnr_options', []) diff --git a/edalize/vivado.py b/edalize/vivado.py index b338f798c..05edfd301 100644 --- a/edalize/vivado.py +++ b/edalize/vivado.py @@ -54,6 +54,17 @@ def get_doc(cls, api_ver): {'name' : 'hw_target', 'type' : 'Description', 'desc' : 'A pattern matching a board identifier. Refer to the Vivado documentation for ``get_hw_targets`` for details. Example: ``*/xilinx_tcf/Digilent/123456789123A``'}, + ], + 'lists' : [ + {'name' : 'yosys_synth_options', + 'type' : 'String', + 'desc' :'Additional options for synth command'}, + {'name' : 'yosys_read_options', + 'type' : 'String', + 'desc' : 'Additional options for Yosys\' read command'}, + {'name' : 'frontend_options', + 'type' : 'String', + 'desc' : 'Additional options for the Yosys frontend'}, ]} """ Get tool version @@ -91,7 +102,9 @@ def configure_main(self): 'arch' : 'xilinx', 'output_format' : 'edif', 'yosys_synth_options' : self.tool_options.get('yosys_synth_options', []), + 'yosys_read_options' : self.tool_options.get('yosys_read_options', []), 'yosys_as_subtool' : True, + 'frontend_options' : self.tool_options.get('frontend_options', []) } yosys = Yosys(self.edam, self.work_root) diff --git a/edalize/yosys.py b/edalize/yosys.py index ec7ca7221..f7dbc12b7 100644 --- a/edalize/yosys.py +++ b/edalize/yosys.py @@ -6,6 +6,7 @@ import os.path from edalize.edatool import Edatool +from edalize.surelog import Surelog logger = logging.getLogger(__name__) @@ -38,6 +39,15 @@ def get_doc(cls, api_ver): {'name' : 'yosys_synth_options', 'type' : 'String', 'desc' : 'Additional options for the synth command'}, + {'name' : 'yosys_read_options', + 'type' : 'String', + 'desc' : 'Addtional options for the read_* command (e.g. read_verlog or read_uhdm)'}, + {'name' : 'yosys_additional_commands', + 'type' : 'String', + 'desc' : 'Additional commands for the yosys script after synth'}, + {'name' : 'frontend_options', + 'type' : 'String', + 'desc' : 'Additional options for the Yosys frontend'}, ]} def configure_main(self): @@ -49,20 +59,51 @@ def configure_main(self): file_table = [] unused_files = [] - for f in self.files: - cmd = "" - if f['file_type'].startswith('verilogSource'): - cmd = 'read_verilog' - elif f['file_type'].startswith('systemVerilogSource'): - cmd = 'read_verilog -sv' - elif f['file_type'] == 'tclSource': - cmd = 'source' - - if cmd: - if not self._add_include_dir(f, incdirs): - file_table.append(cmd + ' {' + f['name'] + '}') - else: - unused_files.append(f) + yosys_read_options = " ".join(self.tool_options.get('yosys_read_options', [])) + yosys_synth_options = self.tool_options.get('yosys_synth_options', []) + + arch = self.tool_options.get('arch', None) + if not arch: + logger.error("ERROR: arch is not defined.") + + use_surelog = False + if "frontend=surelog" in yosys_synth_options: + use_surelog = True + yosys_synth_options.remove("frontend=surelog") + + if use_surelog: + self.edam['tool_options']['surelog'] = { + 'surelog_options' : self.tool_options.get('frontend_options', []), + 'arch' : arch, + } + surelog = Surelog(self.edam, self.work_root) + surelog.configure() + self.vlogparam.clear() + self.vlogdefine.clear() + uhdm_file = os.path.abspath(self.work_root + '/' + self.toplevel + '.uhdm') + file_table.append('read_uhdm' + yosys_read_options + ' {' + uhdm_file + '}') + for f in self.files: + if f['file_type'].startswith('verilogSource') or\ + f['file_type'].startswith('systemVerilogSource') or\ + f['file_type'] == 'tclSource': + continue + else: + unused_files.append(f) + else: + for f in self.files: + cmd = "" + if f['file_type'].startswith('verilogSource'): + cmd = 'read_verilog' + elif f['file_type'].startswith('systemVerilogSource'): + cmd = 'read_verilog -sv' + elif f['file_type'] == 'tclSource': + cmd = 'source' + + if cmd: + if not self._add_include_dir(f, incdirs): + file_table.append(cmd + ' {' + f['name'] + '}') + else: + unused_files.append(f) self.edam['files'] = unused_files of = [ @@ -86,10 +127,6 @@ def configure_main(self): self.toplevel)) output_format = self.tool_options.get('output_format', 'blif') - arch = self.tool_options.get('arch', None) - - if not arch: - logger.error("ERROR: arch is not defined.") template = yosys_template or 'edalize_yosys_template.tcl' template_vars = { @@ -99,7 +136,8 @@ def configure_main(self): 'incdirs' : ' '.join(['-I'+d for d in incdirs]), 'top' : self.toplevel, 'synth_command' : "synth_" + arch, - 'synth_options' : " ".join(self.tool_options.get('yosys_synth_options', '')), + 'synth_options' : " ".join(yosys_synth_options), + 'additional_commands' : self.tool_options.get('yosys_additional_commands', []), 'write_command' : "write_" + output_format, 'default_target' : output_format, 'edif_opts' : '-pvector bra' if arch=='xilinx' else '', @@ -117,9 +155,15 @@ def configure_main(self): template_vars) commands = self.EdaCommands() - commands.add(['yosys', '-l', 'yosys.log', '-p', f'"tcl {template}"'], + dep = [] + yosys = 'yosys' + if use_surelog: + commands.commands += surelog.commands + dep = [self.toplevel + ".uhdm"] + yosys = 'uhdm-' + yosys + commands.add([yosys, '-l', 'yosys.log', '-p', f'"tcl {template}"'], [f'{self.name}.{output}' for output in ['blif', 'json','edif']], - [template]) + [template]+dep) if self.tool_options.get('yosys_as_subtool'): self.commands = commands.commands else: diff --git a/setup.py b/setup.py index 056af49c8..b9f260b41 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,8 @@ def read(fname): 'templates/quartus/quartus-pro-makefile.j2', 'templates/ascentlint/Makefile.j2', 'templates/ascentlint/run-ascentlint.tcl.j2', + 'templates/symbiflow/symbiflow-fasm2bels-tcl.j2', + 'templates/symbiflow/vivado-sh.j2', 'templates/libero/libero-project.tcl.j2', 'templates/libero/libero-run.tcl.j2', 'templates/libero/libero-syn-user.tcl.j2',