mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
The tile sizes have slightly changed as we plan using the openframe caravel variant, as well as power gating all the projects on the shuttle.
810 lines
30 KiB
Python
810 lines
30 KiB
Python
import copy
|
|
import csv
|
|
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import typing
|
|
|
|
import cairosvg # type: ignore
|
|
import gdstk # type: ignore
|
|
import yaml
|
|
from git.repo import Repo
|
|
|
|
import git_utils
|
|
from cells import Cell, load_cells
|
|
from markdown_utils import limit_markdown_headings
|
|
from project_info import ProjectInfo, ProjectYamlError
|
|
|
|
CELL_URL = "https://skywater-pdk.readthedocs.io/en/main/contents/libraries/sky130_fd_sc_hd/cells/"
|
|
|
|
PINOUT_KEYS = [
|
|
"ui[0]",
|
|
"ui[1]",
|
|
"ui[2]",
|
|
"ui[3]",
|
|
"ui[4]",
|
|
"ui[5]",
|
|
"ui[6]",
|
|
"ui[7]",
|
|
"uo[0]",
|
|
"uo[1]",
|
|
"uo[2]",
|
|
"uo[3]",
|
|
"uo[4]",
|
|
"uo[5]",
|
|
"uo[6]",
|
|
"uo[7]",
|
|
"uio[0]",
|
|
"uio[1]",
|
|
"uio[2]",
|
|
"uio[3]",
|
|
"uio[4]",
|
|
"uio[5]",
|
|
"uio[6]",
|
|
"uio[7]",
|
|
]
|
|
|
|
|
|
with open(os.path.join(os.path.dirname(__file__), "tile_sizes.yaml"), "r") as stream:
|
|
tile_sizes = yaml.safe_load(stream)
|
|
|
|
|
|
class Args:
|
|
openlane2: bool
|
|
print_cell_summary: bool
|
|
print_cell_category: bool
|
|
|
|
|
|
class Project:
|
|
top_verilog_filename: str
|
|
mux_address: int
|
|
commit_id: str
|
|
sort_id: int
|
|
|
|
def __init__(
|
|
self,
|
|
index: int,
|
|
git_url: str,
|
|
local_dir: str,
|
|
args: Args,
|
|
is_user_project: bool,
|
|
power_gated: bool = False,
|
|
):
|
|
self.git_url = git_url
|
|
self.args = args
|
|
self.index = index
|
|
self.local_dir = local_dir
|
|
self.is_user_project = is_user_project
|
|
self.power_gated = power_gated
|
|
self.src_dir = (
|
|
os.path.join(self.local_dir, "src")
|
|
if self.is_user_project
|
|
else self.local_dir
|
|
)
|
|
|
|
yaml_path = os.path.join(self.local_dir, "info.yaml")
|
|
try:
|
|
with open(yaml_path) as fh:
|
|
self.info = ProjectInfo(yaml.safe_load(fh))
|
|
except FileNotFoundError:
|
|
logging.error(
|
|
f"yaml file not found for {self} - do you need to --clone the project repos?"
|
|
)
|
|
exit(1)
|
|
except ProjectYamlError as e:
|
|
logging.error(f"Error loading {yaml_path}: {e}")
|
|
exit(1)
|
|
self.unprefixed_name = re.sub("^tt_um_", "", self.info.top_module)
|
|
|
|
if self.is_user_project:
|
|
self.sources = self.info.source_files
|
|
if self.is_wokwi():
|
|
self.top_verilog_filename = self.sources[0]
|
|
else:
|
|
self.check_sources()
|
|
self.top_verilog_filename = self.find_top_verilog()
|
|
else:
|
|
self.sources = [self.get_gl_verilog_filename()]
|
|
|
|
def post_clone_setup(self):
|
|
self.load_metrics()
|
|
|
|
def load_metrics(self):
|
|
try:
|
|
with open(self.get_metrics_path()) as fh:
|
|
self.metrics = next(csv.DictReader(fh))
|
|
except FileNotFoundError:
|
|
self.metrics = {}
|
|
|
|
def is_chip_rom(self):
|
|
return self.get_macro_name() == "tt_um_chip_rom"
|
|
|
|
def is_wokwi(self) -> bool:
|
|
return hasattr(self.info, "wokwi_id")
|
|
|
|
def is_hdl(self):
|
|
return not self.is_wokwi()
|
|
|
|
def check_sources(self):
|
|
for filename in self.sources:
|
|
if "*" in filename:
|
|
logging.error("* not allowed, please specify each file")
|
|
exit(1)
|
|
if not os.path.exists(os.path.join(self.src_dir, filename)):
|
|
logging.error(f"{filename} doesn't exist in the repo")
|
|
exit(1)
|
|
|
|
def check_ports(self, include_power_ports: bool = False):
|
|
top = self.get_macro_name()
|
|
if not self.is_user_project and self.is_chip_rom():
|
|
return # Chip ROM is auto generated, so we don't have the verilog yet
|
|
sources = [os.path.join(self.src_dir, src) for src in self.info.source_files]
|
|
source_list = " ".join(sources)
|
|
|
|
json_file = "ports.json"
|
|
yosys_cmd = f"yowasp-yosys -qp 'read_verilog -lib -sv {source_list}; hierarchy -top {top} ; proc; write_json {json_file}'"
|
|
p = subprocess.run(yosys_cmd, shell=True)
|
|
if p.returncode != 0:
|
|
logging.error(f"yosys port read failed for {self}")
|
|
exit(1)
|
|
|
|
with open(json_file) as fh:
|
|
ports = json.load(fh)
|
|
os.unlink(json_file)
|
|
|
|
module_ports = ports["modules"][top]["ports"]
|
|
required_ports = [
|
|
["input", "clk", 1],
|
|
["input", "ena", 1],
|
|
["input", "rst_n", 1],
|
|
["input", "ui_in", 8],
|
|
["input", "uio_in", 8],
|
|
["output", "uio_oe", 8],
|
|
["output", "uio_out", 8],
|
|
["output", "uo_out", 8],
|
|
]
|
|
if include_power_ports:
|
|
required_ports += [
|
|
["input", "VGND", 1],
|
|
["input", "VPWR", 1],
|
|
]
|
|
for direction, port, bits in required_ports:
|
|
if port not in module_ports:
|
|
logging.error(f"{self} port '{port}' missing from top module ('{top}')")
|
|
exit(1)
|
|
actual_direction = module_ports[port]["direction"]
|
|
if actual_direction != direction:
|
|
logging.error(
|
|
f"{self} incorrect direction for port '{port}' in module '{top}': {direction} required, {actual_direction} found"
|
|
)
|
|
exit(1)
|
|
actual_bits = len(module_ports[port]["bits"])
|
|
if actual_bits != bits:
|
|
logging.error(
|
|
f"{self} incorrect width for port '{port}' in module '{top}': {bits} bits required, {actual_bits} found"
|
|
)
|
|
exit(1)
|
|
del module_ports[port]
|
|
if len(module_ports):
|
|
logging.error(
|
|
f"{self} module '{top}' has unsupported extra ports: {', '.join(module_ports.keys())}"
|
|
)
|
|
exit(1)
|
|
|
|
def check_num_cells(self):
|
|
num_cells = self.get_cell_count_from_synth()
|
|
if self.is_hdl():
|
|
if num_cells < 20:
|
|
logging.warning(f"{self} only has {num_cells} cells")
|
|
else:
|
|
if num_cells < 11:
|
|
logging.warning(f"{self} only has {num_cells} cells")
|
|
|
|
def get_hugo_row(self) -> str:
|
|
return f"| {self.mux_address} | [{self.info.title}]({self.mux_address :03}) | {self.info.author}|\n"
|
|
|
|
# docs stuff for index on README.md
|
|
def get_index_row(self):
|
|
return f"| {self.mux_address} | {self.info.author} | {self.info.title} | {self.get_project_type_string()} | {self.git_url} |\n"
|
|
|
|
def get_project_type_string(self):
|
|
if self.is_wokwi():
|
|
return f"[Wokwi]({self.get_wokwi_url()})"
|
|
else:
|
|
return "HDL"
|
|
|
|
def get_project_docs_dict(self):
|
|
docs = self.info.__dict__.copy()
|
|
docs["project_type"] = self.get_project_type_string()
|
|
docs["git_url"] = self.git_url
|
|
with open(os.path.join(self.local_dir, "docs/info.md")) as fh:
|
|
docs["user_docs"] = limit_markdown_headings(fh.read(), min_level=3)
|
|
return docs
|
|
|
|
def get_wokwi_url(self):
|
|
return f"https://wokwi.com/projects/{self.info.wokwi_id}"
|
|
|
|
# top module name is defined in one of the source files, which one?
|
|
def find_top_verilog(self):
|
|
rgx_mod = re.compile(r"(?:^|[\s])module[\s]{1,}([\w]+)")
|
|
top_verilog: typing.List[str] = []
|
|
top_module = self.info.top_module
|
|
for src in self.info.source_files:
|
|
with open(os.path.join(self.src_dir, src)) as fh:
|
|
for line in fh.readlines():
|
|
for match in rgx_mod.finditer(line):
|
|
if match.group(1) == top_module:
|
|
top_verilog.append(src)
|
|
if len(top_verilog) == 0:
|
|
logging.error(
|
|
f"Couldn't find verilog module '{top_module}' in any of the project's source files"
|
|
)
|
|
exit(1)
|
|
if len(top_verilog) > 1:
|
|
logging.error(
|
|
f"Top verilog module '{top_module}' found in multiple source files: {', '.join(top_verilog)}"
|
|
)
|
|
exit(1)
|
|
return top_verilog[0]
|
|
|
|
def get_git_remote(self):
|
|
return list(Repo(self.local_dir).remotes[0].urls)[0]
|
|
|
|
def get_git_commit_hash(self):
|
|
return Repo(self.local_dir).commit().hexsha
|
|
|
|
def get_tt_tools_version(self):
|
|
repo = Repo(os.path.join(self.local_dir, "tt"))
|
|
return f"{repo.active_branch.name} {repo.commit().hexsha[:8]}"
|
|
|
|
def get_workflow_url_when_submitted(self) -> str:
|
|
json_file = os.path.join(self.local_dir, "commit_id.json")
|
|
with open(json_file) as fh:
|
|
commit_info = json.load(fh)
|
|
return commit_info["workflow_url"]
|
|
|
|
def get_workflow_url(self):
|
|
GITHUB_SERVER_URL = os.getenv("GITHUB_SERVER_URL")
|
|
GITHUB_REPOSITORY = os.getenv("GITHUB_REPOSITORY")
|
|
GITHUB_RUN_ID = os.getenv("GITHUB_RUN_ID")
|
|
if GITHUB_SERVER_URL and GITHUB_REPOSITORY and GITHUB_RUN_ID:
|
|
return (
|
|
f"{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}/actions/runs/{GITHUB_RUN_ID}"
|
|
)
|
|
|
|
def __str__(self):
|
|
return f"[{self.index:03} : {self.git_url}]"
|
|
|
|
def get_latest_action_url(self):
|
|
return git_utils.get_latest_action_url(self.git_url)
|
|
|
|
def get_macro_name(self):
|
|
return self.info.top_module
|
|
|
|
# unique id
|
|
def get_index(self):
|
|
return self.index
|
|
|
|
# metrics
|
|
def get_metrics_path(self):
|
|
if self.is_user_project:
|
|
if self.args.openlane2:
|
|
return os.path.join(self.local_dir, "runs/wokwi/final/metrics.csv")
|
|
else:
|
|
return os.path.join(self.local_dir, "runs/wokwi/reports/metrics.csv")
|
|
else:
|
|
return os.path.join(self.local_dir, "metrics.csv")
|
|
|
|
def get_gl_path(self):
|
|
if self.is_user_project:
|
|
if self.args.openlane2:
|
|
return glob.glob(
|
|
os.path.join(self.local_dir, "runs/wokwi/final/nl/*.nl.v")
|
|
)[0]
|
|
else:
|
|
return glob.glob(
|
|
os.path.join(
|
|
self.local_dir, "runs/wokwi/results/final/verilog/gl/*.nl.v"
|
|
)
|
|
)[0]
|
|
else:
|
|
return os.path.join(self.local_dir, f"{self.info.top_module}.v")
|
|
|
|
# name of the gds file
|
|
def get_macro_gds_filename(self):
|
|
return f"{self.info.top_module}.gds"
|
|
|
|
def get_macro_info_filename(self):
|
|
return f"{self.info.top_module}.info.json"
|
|
|
|
def get_macro_lef_filename(self):
|
|
return f"{self.info.top_module}.lef"
|
|
|
|
def get_macro_spef_filename(self):
|
|
return f"{self.info.top_module}.spef"
|
|
|
|
# for GL sims & blackboxing
|
|
def get_gl_verilog_filename(self):
|
|
return f"{self.info.top_module}.v"
|
|
|
|
# for simulations
|
|
def get_top_verilog_filename(self):
|
|
if self.is_hdl():
|
|
# make sure it's unique & without leading directories
|
|
# a few people use 'top.v', which is OK as long as the top module is called something more unique
|
|
# but then it needs to be made unique so the source can be found
|
|
filename = os.path.basename(self.top_verilog_filename)
|
|
return f"{self.index :03}_{filename}"
|
|
else:
|
|
return self.top_verilog_filename
|
|
|
|
def get_git_url(self):
|
|
return self.git_url
|
|
|
|
def print_wokwi_id(self):
|
|
print(self.info.wokwi_id)
|
|
|
|
def print_top_module(self):
|
|
print(self.info.top_module)
|
|
|
|
def fetch_wokwi_files(self):
|
|
logging.info("fetching wokwi files")
|
|
src_file = self.info.source_files[0]
|
|
url = f"https://wokwi.com/api/projects/{self.info.wokwi_id}/verilog"
|
|
git_utils.fetch_file(url, os.path.join(self.src_dir, src_file))
|
|
|
|
# also fetch the wokwi diagram
|
|
url = f"https://wokwi.com/api/projects/{self.info.wokwi_id}/diagram.json"
|
|
diagram_file = "wokwi_diagram.json"
|
|
git_utils.fetch_file(url, os.path.join(self.src_dir, diagram_file))
|
|
|
|
# attempt to download the *optional* truthtable for the project
|
|
truthtable_file = "truthtable.md"
|
|
url = f"https://wokwi.com/api/projects/{self.info.wokwi_id}/{truthtable_file}"
|
|
try:
|
|
git_utils.fetch_file(url, os.path.join(self.src_dir, truthtable_file))
|
|
logging.info(
|
|
f"Wokwi project {self.info.wokwi_id} has a truthtable included, will test!"
|
|
)
|
|
self.install_wokwi_testing()
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def install_wokwi_testing(
|
|
self, destination_dir: str = "src", resource_dir: typing.Optional[str] = None
|
|
):
|
|
if resource_dir is None or not len(resource_dir):
|
|
resource_dir = os.path.join(os.path.dirname(__file__), "testing")
|
|
|
|
# directories in testing/lib to copy over
|
|
pyLibsDir = os.path.join(resource_dir, "lib")
|
|
|
|
# src template dir
|
|
srcTplDir = os.path.join(resource_dir, "src-tpl")
|
|
|
|
for libfullpath in glob.glob(os.path.join(pyLibsDir, "*")):
|
|
logging.info(f"Copying {libfullpath} to {destination_dir}")
|
|
shutil.copytree(
|
|
libfullpath,
|
|
os.path.join(destination_dir, os.path.basename(libfullpath)),
|
|
dirs_exist_ok=True,
|
|
)
|
|
|
|
for srcTemplate in glob.glob(os.path.join(srcTplDir, "*")):
|
|
with open(srcTemplate, "r") as f:
|
|
contents = f.read()
|
|
customizedContents = re.sub(
|
|
"WOKWI_ID", str(self.info.wokwi_id), contents
|
|
)
|
|
outputFname = os.path.basename(srcTemplate)
|
|
with open(os.path.join(destination_dir, outputFname), "w") as outf:
|
|
logging.info(f"writing src tpl to {outputFname}")
|
|
outf.write(customizedContents)
|
|
outf.close()
|
|
|
|
def create_user_config(self):
|
|
if self.is_wokwi():
|
|
self.fetch_wokwi_files()
|
|
self.check_ports()
|
|
logging.info("creating include file")
|
|
filename = "user_config.tcl"
|
|
with open(os.path.join(self.src_dir, filename), "w") as fh:
|
|
fh.write("set ::env(DESIGN_NAME) {}\n".format(self.info.top_module))
|
|
fh.write('set ::env(VERILOG_FILES) "\\\n')
|
|
for line, source in enumerate(self.sources):
|
|
fh.write(" $::env(DESIGN_DIR)/" + source)
|
|
if line != len(self.sources) - 1:
|
|
fh.write(" \\\n")
|
|
fh.write('"\n\n')
|
|
tiles = self.info.tiles
|
|
die_area = tile_sizes[tiles]
|
|
fh.write(f"# Project area: {tiles} tiles\n")
|
|
fh.write(f'set ::env(DIE_AREA) "{die_area}"\n')
|
|
fh.write(
|
|
f'set ::env(FP_DEF_TEMPLATE) "$::env(DESIGN_DIR)/../tt/def/tt_block_{tiles}_pg.def"\n'
|
|
)
|
|
|
|
def golden_harden(self):
|
|
logging.info(f"hardening {self}")
|
|
shutil.copyfile("golden_config.tcl", os.path.join(self.src_dir, "config.tcl"))
|
|
self.harden()
|
|
|
|
def harden(self):
|
|
cwd = os.getcwd()
|
|
os.chdir(self.local_dir)
|
|
|
|
repo = self.get_git_remote()
|
|
commit_hash = self.get_git_commit_hash()
|
|
tt_version = self.get_tt_tools_version()
|
|
workflow_url = self.get_workflow_url()
|
|
|
|
if self.args.openlane2:
|
|
if not os.path.exists("runs/wokwi"):
|
|
print(
|
|
"OpenLane 2 harden not supported yet, please run OpenLane 2 manually"
|
|
)
|
|
exit(1)
|
|
print(
|
|
"Writing commit information on top of the existing OpenLane 2 run (runs/wokwi)"
|
|
)
|
|
else:
|
|
# requires PDK, PDK_ROOT, OPENLANE_ROOT & OPENLANE_IMAGE_NAME to be set in local environment
|
|
harden_cmd = 'docker run --rm -v $OPENLANE_ROOT:/openlane -v $PDK_ROOT:$PDK_ROOT -v $(pwd):/work -e PDK=$PDK -e PDK_ROOT=$PDK_ROOT -u $(id -u $USER):$(id -g $USER) $OPENLANE_IMAGE_NAME /bin/bash -c "./flow.tcl -overwrite -design /work/src -run_path /work/runs -tag wokwi"'
|
|
logging.debug(harden_cmd)
|
|
env = os.environ.copy()
|
|
p = subprocess.run(harden_cmd, shell=True, env=env)
|
|
if p.returncode != 0:
|
|
logging.error("harden failed")
|
|
exit(1)
|
|
|
|
# Write commit information
|
|
commit_id_json_path = "runs/wokwi/results/final/commit_id.json"
|
|
if self.args.openlane2:
|
|
commit_id_json_path = "runs/wokwi/final/commit_id.json"
|
|
with open(os.path.join(self.local_dir, commit_id_json_path), "w") as f:
|
|
json.dump(
|
|
{
|
|
"app": f"Tiny Tapeout {tt_version}",
|
|
"repo": repo,
|
|
"commit": commit_hash,
|
|
"workflow_url": workflow_url,
|
|
},
|
|
f,
|
|
indent=2,
|
|
)
|
|
f.write("\n")
|
|
|
|
os.chdir(cwd)
|
|
|
|
# doc check
|
|
# makes sure that the basic info is present
|
|
def check_docs(self):
|
|
info_md = os.path.join(self.local_dir, "docs/info.md")
|
|
if not os.path.exists(info_md):
|
|
logging.error("Missing docs/info.md file")
|
|
exit(1)
|
|
|
|
with open(info_md) as fh:
|
|
info_md_content = fh.read()
|
|
|
|
if "# How it works\n\nExplain how your project works" in info_md_content:
|
|
logging.error("Missing 'How it works' section in docs/info.md")
|
|
exit(1)
|
|
|
|
if "# How to test\n\nExplain how to use your project" in info_md_content:
|
|
logging.error("Missing 'How to use' section in docs/info.md")
|
|
exit(1)
|
|
|
|
# use pandoc to create a single page PDF preview
|
|
def create_pdf(self):
|
|
template_args = copy.deepcopy(self.info.__dict__)
|
|
template_args["ui"] = self.info.pinout.ui
|
|
template_args["uo"] = self.info.pinout.uo
|
|
template_args["uio"] = self.info.pinout.uio
|
|
|
|
logging.info("Creating PDF")
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
with open(os.path.join(script_dir, "docs/project_header.md")) as fh:
|
|
doc_header = fh.read()
|
|
with open(os.path.join(script_dir, "docs/project_preview.md")) as fh:
|
|
doc_template = fh.read()
|
|
info_md = os.path.join(self.local_dir, "docs/info.md")
|
|
with open(info_md) as fh:
|
|
template_args["info"] = fh.read()
|
|
|
|
with open("datasheet.md", "w") as fh:
|
|
fh.write(doc_header)
|
|
|
|
# now build the doc & print it
|
|
try:
|
|
doc = doc_template.format(**template_args)
|
|
fh.write(doc)
|
|
fh.write("\n\\pagebreak\n")
|
|
except IndexError:
|
|
logging.warning("missing pins in info.yaml, skipping")
|
|
|
|
pdf_cmd = "pandoc --pdf-engine=xelatex -i datasheet.md -o datasheet.pdf"
|
|
logging.info(pdf_cmd)
|
|
p = subprocess.run(pdf_cmd, shell=True)
|
|
if p.returncode != 0:
|
|
logging.error("pdf command failed")
|
|
|
|
# Read and return top-level GDS data from the final GDS file, using gdstk:
|
|
def get_final_gds_top_cells(self):
|
|
gds = glob.glob(
|
|
os.path.join(self.local_dir, "runs/wokwi/results/final/gds/*gds")
|
|
)
|
|
if self.args.openlane2:
|
|
gds = glob.glob(os.path.join(self.local_dir, "runs/wokwi/final/gds/*gds"))
|
|
library = gdstk.read_gds(gds[0])
|
|
top_cells = library.top_level()
|
|
return top_cells[0]
|
|
|
|
# SVG render of the GDS.
|
|
# NOTE: This includes all standard GDS layers inc. text labels.
|
|
def create_svg(self):
|
|
top_cells = self.get_final_gds_top_cells()
|
|
top_cells.write_svg("gds_render.svg")
|
|
|
|
# Try various QUICK methods to create a more-compressed PNG render of the GDS,
|
|
# and fall back to cairosvg if it doesn't work. This is designed for speed,
|
|
# and in particular for use by the GitHub Actions.
|
|
# For more info, see:
|
|
# https://github.com/TinyTapeout/tt-gds-action/issues/8
|
|
def create_png(self):
|
|
logging.info("Loading GDS data...")
|
|
top_cells = self.get_final_gds_top_cells()
|
|
|
|
# Remove label layers (i.e. delete text), then generate SVG:
|
|
sky130_label_layers = [
|
|
(64, 59), # 64/59 - pwell.label
|
|
(64, 5), # 64/5 - nwell.label
|
|
(67, 5), # 67/5 - li1.label
|
|
(68, 5), # 68/5 - met1.label
|
|
(69, 5), # 69/5 - met2.label
|
|
(70, 5), # 70/5 - met3.label
|
|
(71, 5), # 71/5 - met4.label
|
|
]
|
|
top_cells.filter(sky130_label_layers)
|
|
svg = "gds_render_preview.svg"
|
|
logging.info("Rendering SVG without text label layers: {}".format(svg))
|
|
top_cells.write_svg(svg, pad=0)
|
|
# We should now have gds_render_preview.svg
|
|
|
|
# Try converting using 'rsvg-convert' command-line utility.
|
|
# This should create gds_render_preview.png
|
|
png = "gds_render_preview.png"
|
|
logging.info("Converting to PNG using rsvg-convert: {}".format(png))
|
|
|
|
cmd = "rsvg-convert --unlimited {} -o {} --no-keep-image-data".format(svg, png)
|
|
logging.debug(cmd)
|
|
p = subprocess.run(cmd, shell=True, capture_output=True)
|
|
|
|
if p.returncode == 127:
|
|
logging.warning(
|
|
'rsvg-convert not found; is package "librsvg2-bin" installed? Falling back to cairosvg. This might take a while...'
|
|
)
|
|
# Fall back to cairosvg:
|
|
cairosvg.svg2png(url=svg, write_to=png)
|
|
|
|
elif p.returncode != 0 and b"cannot load more than" in p.stderr:
|
|
logging.warning(
|
|
'Too many SVG elements ("{}"). Reducing complexity...'.format(
|
|
p.stderr.decode().strip()
|
|
)
|
|
)
|
|
# Remove more layers that are hardly visible anyway:
|
|
sky130_buried_layers = [
|
|
(64, 16), # 64/16 - nwell.pin
|
|
(65, 44), # 65/44 - tap.drawing
|
|
(68, 16), # 68/16 - met1.pin
|
|
(68, 44), # 68/44 - via.drawing
|
|
(81, 4), # 81/4 - areaid.standardc
|
|
(70, 20), # 70/20 - met3.drawing
|
|
# Important:
|
|
# (68,20), # 68/20 - met1.drawing
|
|
# Less important, but keep for now:
|
|
# (69,20), # 69/20 - met2.drawing
|
|
]
|
|
top_cells.filter(sky130_buried_layers)
|
|
svg_alt = "gds_render_preview_alt.svg"
|
|
logging.info("Rendering SVG with more layers removed: {}".format(svg_alt))
|
|
top_cells.write_svg(svg_alt, pad=0)
|
|
logging.info("Converting to PNG using rsvg-convert: {}".format(png))
|
|
|
|
cmd = "rsvg-convert --unlimited {} -o {} --no-keep-image-data".format(
|
|
svg_alt, png
|
|
)
|
|
logging.debug(cmd)
|
|
p = subprocess.run(cmd, shell=True, capture_output=True)
|
|
|
|
if p.returncode != 0:
|
|
logging.warning(
|
|
'Still cannot convert to SVG ("{}"). Falling back to cairosvg. This might take a while...'.format(
|
|
p.stderr.decode().strip()
|
|
)
|
|
)
|
|
# Fall back to cairosvg, and since we're doing that, might as well use the original full-detail SVG anyway:
|
|
cairosvg.svg2png(url=svg, write_to=png)
|
|
|
|
# By now we should have gds_render_preview.png
|
|
|
|
# Compress with pngquant:
|
|
final_png = "gds_render.png"
|
|
if self.info.tiles == "8x2":
|
|
quality = "0-10" # Compress more for 8x2 tiles.
|
|
else:
|
|
quality = "0-30"
|
|
logging.info("Compressing PNG further with pngquant to: {}".format(final_png))
|
|
|
|
cmd = "pngquant --quality {} --speed 1 --nofs --strip --force --output {} {}".format(
|
|
quality, final_png, png
|
|
)
|
|
logging.debug(cmd)
|
|
p = subprocess.run(cmd, shell=True, capture_output=True)
|
|
|
|
if p.returncode == 127:
|
|
logging.warning(
|
|
'pngquant not found; is package "pngquant" installed? Using intermediate (uncompressed) PNG file'
|
|
)
|
|
os.rename(png, final_png)
|
|
elif p.returncode != 0:
|
|
logging.warning(
|
|
'pngquant error {} ("{}"). Using intermediate (uncompressed) PNG file'.format(
|
|
p.returncode, p.stderr.decode().strip()
|
|
)
|
|
)
|
|
os.rename(png, final_png)
|
|
logging.info(
|
|
"Final PNG is {} ({:,} bytes)".format(final_png, os.path.getsize(final_png))
|
|
)
|
|
|
|
def print_warnings(self):
|
|
warnings: typing.List[str] = []
|
|
synth_log = "runs/wokwi/logs/synthesis/1-synthesis.log"
|
|
if self.args.openlane2:
|
|
synth_log = "runs/wokwi/05-yosys-synthesis/yosys-synthesis.log"
|
|
with open(os.path.join(self.local_dir, synth_log)) as f:
|
|
for line in f.readlines():
|
|
if line.startswith("Warning:"):
|
|
# bogus warning https://github.com/YosysHQ/yosys/commit/bfacaddca8a2e113e4bc3d6177612ccdba1555c8
|
|
if "WIDTHLABEL" not in line:
|
|
warnings.append(line.strip())
|
|
if len(warnings):
|
|
print("# Synthesis warnings")
|
|
print()
|
|
for warning in warnings:
|
|
print(f"* {warning}")
|
|
|
|
def print_stats(self):
|
|
print("# Routing stats")
|
|
print()
|
|
print("| Utilisation (%) | Wire length (um) |")
|
|
print("|-------------|------------------|")
|
|
print(
|
|
"| {} | {} |".format(
|
|
self.metrics["OpenDP_Util"], self.metrics["wire_length"]
|
|
)
|
|
)
|
|
|
|
# Print the summaries
|
|
def summarize(self):
|
|
cell_count = self.get_cell_counts_from_gl()
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
Categories = typing.TypedDict(
|
|
"Categories", {"categories": typing.List[str], "map": typing.Dict[str, int]}
|
|
)
|
|
with open(os.path.join(script_dir, "categories.json")) as fh:
|
|
categories: Categories = json.load(fh)
|
|
|
|
defs = load_cells()
|
|
|
|
# print all used cells, sorted by frequency
|
|
total = 0
|
|
if self.args.print_cell_summary:
|
|
print("# Cell usage")
|
|
print()
|
|
print("| Cell Name | Description | Count |")
|
|
print("|-----------|-------------|-------|")
|
|
for name, count in sorted(
|
|
cell_count.items(), key=lambda item: item[1], reverse=True
|
|
):
|
|
if count > 0:
|
|
total += count
|
|
cell_link = f"{CELL_URL}{name}"
|
|
print(
|
|
f'| [{name}]({cell_link}) | {defs[name]["description"]} |{count} |'
|
|
)
|
|
|
|
print(f"| | Total | {total} |")
|
|
|
|
if self.args.print_cell_category:
|
|
CategoryInfo = typing.TypedDict(
|
|
"CategoryInfo", {"count": int, "examples": typing.List[str]}
|
|
)
|
|
by_category: typing.Dict[str, CategoryInfo] = {}
|
|
total = 0
|
|
for cell_name in cell_count:
|
|
cat_index = categories["map"][cell_name]
|
|
cat_name = categories["categories"][cat_index]
|
|
if cat_name in by_category:
|
|
by_category[cat_name]["count"] += cell_count[cell_name]
|
|
by_category[cat_name]["examples"].append(cell_name)
|
|
else:
|
|
by_category[cat_name] = {
|
|
"count": cell_count[cell_name],
|
|
"examples": [cell_name],
|
|
}
|
|
|
|
if cat_name not in ["Fill", "Tap"]:
|
|
total += cell_count[cell_name]
|
|
|
|
print("# Cell usage by Category")
|
|
print()
|
|
print("| Category | Cells | Count |")
|
|
print("|---------------|----------|-------|")
|
|
for cat_name, cat_dict in sorted(
|
|
by_category.items(), key=lambda x: x[1]["count"], reverse=True
|
|
):
|
|
cell_links = [
|
|
f"[{name}]({CELL_URL}{name})" for name in cat_dict["examples"]
|
|
]
|
|
print(f'|{cat_name} | {" ".join(cell_links)} | {cat_dict["count"]}|')
|
|
|
|
print(f"## {total} total cells (excluding fill and tap cells)")
|
|
|
|
# get cell count from synth report
|
|
def get_cell_count_from_synth(self):
|
|
num_cells = 0
|
|
try:
|
|
yosys_report = f"{self.local_dir}/synthesis-stats.txt"
|
|
with open(yosys_report) as fh:
|
|
for line in fh.readlines():
|
|
m = re.search(r"Number of cells:\s+(\d+)", line)
|
|
if m is not None:
|
|
num_cells = int(m.group(1))
|
|
|
|
except (IndexError, FileNotFoundError):
|
|
logging.warning(f"couldn't open yosys cell report for cell checking {self}")
|
|
|
|
return num_cells
|
|
|
|
# Parse the lib, cell and drive strength an OpenLane gate-level Verilog file
|
|
def get_cell_counts_from_gl(self):
|
|
cell_count: typing.Dict[str, int] = {}
|
|
total = 0
|
|
with open(self.get_gl_path()) as fh:
|
|
for line in fh.readlines():
|
|
m = re.search(r"sky130_(\S+)__(\S+)_(\d+)", line)
|
|
if m is not None:
|
|
total += 1
|
|
cell_lib = m.group(1)
|
|
cell_name = m.group(2)
|
|
cell_drive = m.group(3)
|
|
assert cell_lib in ["fd_sc_hd", "ef_sc_hd"]
|
|
assert int(cell_drive) > 0
|
|
try:
|
|
cell_count[cell_name] += 1
|
|
except KeyError:
|
|
cell_count[cell_name] = 1
|
|
return cell_count
|
|
|
|
# Load all the json defs and combine into one big dict, keyed by cellname
|
|
# Needs access to the PDK
|
|
def create_defs(self):
|
|
# replace this with a find
|
|
json_files = glob.glob("sky130_fd_sc_hd/latest/cells/*/definition.json")
|
|
definitions: typing.Dict[str, Cell] = {}
|
|
for json_file in json_files:
|
|
with open(json_file) as fh:
|
|
definition: Cell = json.load(fh)
|
|
definitions[definition["name"]] = definition
|
|
|
|
with open("cells.json", "w") as fh:
|
|
json.dump(definitions, fh)
|