| #!/usr/bin/env python3 |
| # Copyright 2020 The Skywater PDK Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # https://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| |
| import os |
| import cairosvg |
| import re |
| import simplejson as json |
| #import split_inouts as split_io |
| import subprocess |
| from pathlib import Path |
| import argparse |
| from glob import glob |
| from common import files, lib_extract_from_path |
| |
| debug = False |
| |
| debug_print = lambda x: print(x) if debug else 0 |
| dummy_verilog = os.path.dirname(os.path.realpath(__file__)) + "/dummy_verilog.v" |
| |
| def call_with_retcode(cmd): |
| p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| stdout, stderr = p.communicate() |
| stderr = stderr.decode("utf-8") |
| |
| retcode = p.wait() |
| return retcode, stderr |
| |
| |
| def use_netlistsvg(json_file, out_file): |
| """Draw module schematics using netlistsvg. Operates on json files. returns retcode""" |
| cmd = ["netlistsvg", json_file, "-o", out_file] |
| return call_with_retcode(cmd) |
| |
| def use_yosys(command, files): |
| """Execute yosys command, returns retcode""" |
| actions = "read_verilog {}".format(" ".join(files)) + "; hierarchy -check -auto-top; " + command |
| cmd = ["yosys", "-f", "verilog", "-p", actions] |
| return call_with_retcode(cmd) |
| |
| def connect_delayed_ports(verilog_file, out_file): |
| conn = {} |
| finish = False |
| out = "" |
| with open(verilog_file, 'r') as in_f: |
| lines = in_f.read() |
| lines = lines.split("\n") |
| for line in lines: |
| if line.find("endmodule") != -1: |
| for src, dest in conn.items(): |
| out += f"assign {dest} = {src};\n" |
| out += "\n" |
| finish = True |
| if not finish: |
| if re.search(r"^\s*(\$recrem|\$setuphold)", line): |
| args = line[line.find("(") + 1: line.rfind(")")].split(",") |
| if len(args) <= 7: |
| continue |
| srcs = [x.strip().split(" ")[1] for x in args[:2]] |
| targts = [x.strip() for x in args[-2:]] |
| for x in range(len(srcs)): |
| if targts[x] not in conn: |
| conn[srcs[x]] = targts[x] |
| out += line + "\n" |
| with open(out_file, 'w') as out_f: |
| out_f.write(out) |
| |
| def remove_loose_ports(json_file, out_file = None): |
| if out_file == None: |
| out_file = json_file |
| with open(json_file, 'r') as in_f: |
| design = json.load(in_f) |
| for mod_name in design["modules"]: |
| used_ports= [] |
| mod = design["modules"][mod_name] |
| for cell in mod["cells"]: |
| for value in mod["cells"][cell]["connections"].values(): |
| used_ports += value |
| # only first module should be checked - it is always top one |
| to_del = [] |
| for port in mod["ports"]: |
| if mod["ports"][port]["bits"][0] not in used_ports: |
| to_del.append(port) |
| for x in to_del: |
| del design["modules"][mod_name]["ports"][x] |
| break |
| output = json.dumps(design, sort_keys=True, indent=2) |
| with open(out_file, 'w') as out_f: |
| out_f.write(output) |
| |
| def base_includes(verilog_file): |
| version_path_pos = re.search(r"/V[0-2]\.[0-2]\.[0-2]", verilog_file) |
| if version_path_pos is None: |
| return [] |
| base_list = verilog_file[:version_path_pos.end()] +"/.base" |
| debug_print(base_list) |
| modules_to_include = [] |
| with open(base_list, 'r') as in_f: |
| modules_to_include = in_f.read().split("\n") |
| # Guard from file duplication |
| try: |
| modules_to_include.remove(verilog_file) |
| except ValueError: |
| pass |
| return modules_to_include |
| |
| |
| def draw_schematics(verilog_file): |
| """Draw module schematics using yosys+netlistsvg. Operates on verilog files. |
| Output file is placed in same directory as input""" |
| connect_delayed_ports(verilog_file, "temp.v") |
| retcode, err = use_yosys("prep; splitinout; write_json temp.json", |
| ["temp.v" , dummy_verilog]) |
| if retcode > 0: |
| if err.rfind("is not part of the design") != -1: |
| # add missing modules |
| retcode, err = use_yosys("prep; splitinout; write_json temp.json", |
| ["temp.v" , dummy_verilog] + base_includes(verilog_file)) |
| if retcode > 0: |
| debug_print(err) |
| return retcode |
| else: |
| debug_print(err) |
| return retcode |
| lib = lib_extract_from_path(verilog_file) |
| json_correct("temp.json", lib) |
| remove_loose_ports("temp.json") |
| dest_path = verilog_file.replace(".simple.v", ".sch.png") |
| if not retcode: |
| retcode, err = use_netlistsvg("temp.json", "temp.svg") |
| if retcode != 0: |
| # inout are not split when they are not connected to anything - empty design |
| if err.find('direction"') == -1: |
| debug_print(err) |
| # check if design contains any connections |
| with open("temp.svg", 'r') as in_f: |
| is_empty = in_f.read().find('<line x1="') == -1 |
| if not is_empty: |
| cairosvg.svg2png(url="temp.svg", write_to=dest_path) |
| else: |
| return 2 |
| return retcode |
| |
| def json_correct(json_file, lib): |
| with open(json_file, 'r') as in_f: |
| file_as_text = in_f.read() |
| output = re.sub(r'"top": "00000000000000000000000000000001"', '"top": 1', file_as_text) |
| # replace floating lines with unknown |
| output = re.sub(r'\[ "z" \]', '[ "x" ]', output) |
| # remove libname from modules name |
| output = re.sub(rf"{lib}_", "", output) |
| with open(json_file, 'w') as out_f: |
| out_f.write(output) |
| |
| # def split_inouts(in_file, out_file = None): |
| # """Split inout ports into inputs and outputs. Operates on yosys json files""" |
| # if out_file == None: |
| # out_file = in_file |
| # with open(in_file, 'r') as in_f: |
| # design = json.load(in_f) |
| # top_name = split_io.find_top_module(design) |
| # port_map, net_map = split_io.find_and_split_inout_ports_and_nets(design, top_name) |
| # output = json.dumps(design, sort_keys=True, indent=2) |
| # output = re.sub(r'"00000000000000000000000000000001"', "1", output) |
| # with open(out_file, 'w') as out_f: |
| # out_f.write(output) |
| |
| if __name__ == "__main__": |
| |
| debug = False |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "input", |
| help="The path to the source directory/file. If file is passed it will run in debug mode with no output file" + |
| "If dir is passed it will find '.simple.v' files and generate '.sch.png' in the same directory", |
| type=Path) |
| args = parser.parse_args() |
| if not os.path.isdir(args.input): |
| debug = True |
| draw_schematics(args.input) |
| else: |
| files = args.input.rglob('*.simple.v') |
| count = 0 |
| empty = [] |
| failed = [] |
| for f in files: |
| f = str(f) |
| count+=1 |
| retcode = draw_schematics(f) |
| if retcode == 0: |
| print("Drawn:", f) |
| elif retcode == 2: |
| print("Design_empty:", f) |
| empty.append(f) |
| else: |
| # creating schematics failed due to lines stripped from .v.full |
| print("Design incomplete:", f) |
| failed.append(f) |
| if debug: |
| print("*"*60) |
| print("Done") |
| print("*"*60) |
| |
| print("Empty") |
| for x in empty: |
| print(f"\t{x}") |
| |
| print("*"*60) |
| print("Failed") |
| for x in failed: |
| print(f"\t{x}") |
| print("*"*60) |
| |
| print(f"Failed for { len(failed)} - {len(failed)*1.0 /count}") |
| print(f"Empty for { len(empty)} - {len(empty)*1.0 /count}") |
| |
| # remove temp files |
| os.remove("temp.json") |
| os.remove("temp.v") |
| os.remove("temp.svg") |
| |
| |
| |