mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-02 20:26:10 +01:00
* TT FPGA tool, prelim import * config.ini manager, prelim * bitstream uploads and FPGA config.ini management Contributed by @psychogenic, with minor edits by @htfab
876 lines
32 KiB
Python
876 lines
32 KiB
Python
import copy
|
|
import csv
|
|
import glob
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import typing
|
|
|
|
import chevron
|
|
import klayout.db as pya
|
|
import yaml
|
|
from git.repo import Repo
|
|
|
|
import git_utils
|
|
from config_utils import read_config, write_config
|
|
from doc_utils import DocsHelper
|
|
from markdown_utils import limit_markdown_headings
|
|
from project_info import ProjectInfo, ProjectYamlError
|
|
from render_utils import render_png, render_svg
|
|
from tech import TechName, tech_map
|
|
|
|
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]",
|
|
]
|
|
|
|
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
|
|
|
|
|
class Project:
|
|
top_verilog_filename: str
|
|
mux_address: int
|
|
analog_pins: tuple[int, ...]
|
|
commit_id: str
|
|
sort_id: int
|
|
|
|
def __init__(
|
|
self,
|
|
index: int,
|
|
git_url: str,
|
|
local_dir: str,
|
|
pdk: TechName,
|
|
is_user_project: bool,
|
|
):
|
|
self.git_url = git_url
|
|
self.pdk = pdk
|
|
self.tech = tech_map[pdk]
|
|
self.index = index
|
|
self.local_dir = os.path.realpath(local_dir)
|
|
self.is_user_project = is_user_project
|
|
self.src_dir = (
|
|
os.path.join(self.local_dir, "src")
|
|
if self.is_user_project
|
|
else self.local_dir
|
|
)
|
|
self.test_dir = (
|
|
os.path.join(self.local_dir, "test")
|
|
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), self.get_tile_sizes())
|
|
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:
|
|
if any(s.endswith(".vhdl") for s in self.sources):
|
|
self.transpile_vhdl()
|
|
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 = dict(csv.reader(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 run_yosys(self, command: str, no_output: bool = False):
|
|
env = os.environ.copy()
|
|
env["YOSYS_CMD"] = command
|
|
yosys_cmd = 'yowasp-yosys -qp "$YOSYS_CMD"'
|
|
return subprocess.run(yosys_cmd, shell=True, env=env, capture_output=no_output)
|
|
|
|
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.sources]
|
|
source_list = " ".join(sources)
|
|
|
|
json_file = "ports.json"
|
|
|
|
# Heuristic - try reading just the first source file, if that fails, try all of them
|
|
p = self.run_yosys(
|
|
f"read_verilog -lib -sv {sources[0]}; hierarchy -top {top} ; proc; write_json {json_file}",
|
|
True,
|
|
)
|
|
if p.returncode != 0:
|
|
p = self.run_yosys(
|
|
f"read_verilog -lib -sv {source_list}; hierarchy -top {top} ; proc; write_json {json_file}"
|
|
)
|
|
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"]
|
|
if "VPWR" in module_ports:
|
|
if "VDPWR" in module_ports:
|
|
logging.error(
|
|
f"{self} top module '{top}' uses both VPWR and VDPWR ports"
|
|
)
|
|
exit(1)
|
|
else:
|
|
logging.info(
|
|
f"{self} top module '{top}' uses VPWR port, substituting VDPWR"
|
|
)
|
|
module_ports["VDPWR"] = module_ports["VPWR"]
|
|
del module_ports["VPWR"]
|
|
|
|
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 self.info.is_analog:
|
|
required_ports += [
|
|
["inout", "ua", 8],
|
|
]
|
|
if include_power_ports:
|
|
required_ports += [
|
|
[("input", "inout"), "VGND", 1],
|
|
[("input", "inout"), "VDPWR", 1],
|
|
]
|
|
if self.info.uses_3v3:
|
|
required_ports += [
|
|
[("input", "inout"), "VAPWR", 1],
|
|
]
|
|
for valid_directions, 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 type(valid_directions) is tuple:
|
|
valid_directions_tuple = valid_directions
|
|
else:
|
|
valid_directions_tuple = (valid_directions,)
|
|
if actual_direction not in valid_directions_tuple:
|
|
logging.error(
|
|
f"{self} incorrect direction for port '{port}' in module '{top}': {valid_directions_tuple} 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")
|
|
|
|
# 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()})"
|
|
elif self.info.is_analog:
|
|
return "Analog"
|
|
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.sources:
|
|
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"))
|
|
if repo.head.is_detached:
|
|
ref = next(
|
|
(ref.name for ref in repo.refs if ref.commit == repo.head.commit), # type: ignore[attr-defined] # see mypy issue #8085
|
|
"(detached)",
|
|
)
|
|
else:
|
|
ref = repo.active_branch.name
|
|
return f"{ref} {repo.commit().hexsha[:8]}"
|
|
|
|
def read_commit_info_json(self) -> typing.Dict[str, typing.Any]:
|
|
json_file = os.path.join(self.local_dir, "commit_id.json")
|
|
with open(json_file) as fh:
|
|
return json.load(fh)
|
|
|
|
def get_workflow_url_when_submitted(self) -> str:
|
|
commit_info = self.read_commit_info_json()
|
|
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:
|
|
return os.path.join(self.local_dir, "runs/wokwi/final/metrics.csv")
|
|
else:
|
|
return os.path.join(self.local_dir, "stats/metrics.csv")
|
|
|
|
def get_gl_path(self):
|
|
if self.is_user_project:
|
|
return glob.glob(
|
|
os.path.join(self.local_dir, "runs/wokwi/final/nl/*.nl.v")
|
|
)[0]
|
|
else:
|
|
return os.path.join(self.local_dir, f"{self.info.top_module}.v")
|
|
|
|
def get_tile_sizes(self):
|
|
if self.tech.is_fpga:
|
|
return {}
|
|
tile_sizes_yaml = f"tech/{self.pdk}/tile_sizes.yaml"
|
|
with open(
|
|
os.path.join(os.path.dirname(__file__), tile_sizes_yaml), "r"
|
|
) as stream:
|
|
tile_sizes = yaml.safe_load(stream)
|
|
return tile_sizes
|
|
|
|
# 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.test_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 = "test", 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 transpile_vhdl(self):
|
|
# logging.info("transpiling vhdl sources")
|
|
new_sources = []
|
|
for s in self.sources:
|
|
if s.endswith(".vhdl"):
|
|
# logging.info(f"running ghdl on {s}")
|
|
t = "generated/" + s.replace(".vhdl", ".v")
|
|
s_full = os.path.join(self.src_dir, s)
|
|
t_full = os.path.join(self.src_dir, t)
|
|
os.makedirs(os.path.dirname(t_full), exist_ok=True)
|
|
transpile_cmd = f"ghdl synth -Wno-binding --std=08 --out=verilog {s_full} -e > {t_full}"
|
|
p = subprocess.run(transpile_cmd, shell=True)
|
|
if p.returncode != 0:
|
|
logging.error("transpile failed")
|
|
exit(1)
|
|
new_sources.append(t)
|
|
else:
|
|
new_sources.append(s)
|
|
self.sources = new_sources
|
|
|
|
def create_merged_config(self):
|
|
config = read_config("src/config")
|
|
user_config = read_config("src/user_config")
|
|
config.update(user_config)
|
|
write_config(config, "src/config_merged")
|
|
|
|
def create_user_config(self):
|
|
if self.is_wokwi():
|
|
self.fetch_wokwi_files()
|
|
self.check_ports()
|
|
logging.info("creating include file")
|
|
tiles = self.info.tiles
|
|
tile_sizes = self.get_tile_sizes()
|
|
die_area = tile_sizes[tiles]
|
|
def_suffix = self.tech.def_suffix
|
|
def_template = (
|
|
f"dir::../tt/tech/{self.pdk}/def/tt_block_{tiles}_{def_suffix}.def"
|
|
)
|
|
config = {
|
|
"DESIGN_NAME": self.info.top_module,
|
|
"VERILOG_FILES": [f"dir::{src}" for src in self.sources],
|
|
"DIE_AREA": die_area,
|
|
"FP_DEF_TEMPLATE": def_template,
|
|
"VDD_PIN": "VPWR",
|
|
"GND_PIN": "VGND",
|
|
"RT_MAX_LAYER": self.tech.project_top_metal_layer,
|
|
}
|
|
config.update(self.tech.librelane_config)
|
|
write_config(config, os.path.join(self.src_dir, "user_config"), ("json",))
|
|
self.create_merged_config()
|
|
|
|
def golden_harden(self):
|
|
logging.info(f"hardening {self}")
|
|
shutil.copyfile("golden_config.json", os.path.join(self.src_dir, "config.json"))
|
|
self.harden()
|
|
|
|
def harden(self, no_docker: bool = False):
|
|
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()
|
|
|
|
self.create_merged_config()
|
|
shutil.rmtree("runs/wokwi", ignore_errors=True)
|
|
os.makedirs("runs/wokwi", exist_ok=True)
|
|
arg_progress = "--hide-progress-bar" if "CI" in os.environ else ""
|
|
arg_pdk_root = '--pdk-root "$PDK_ROOT"' if "PDK_ROOT" in os.environ else ""
|
|
arg_pdk = self.tech.librelane_pdk_args
|
|
|
|
# Nix/No-Docker support: Conditionally include docker flags
|
|
arg_docker = "" if no_docker else f"{arg_pdk_root} --docker-no-tty --dockerized"
|
|
|
|
harden_cmd = f"python -m librelane {arg_docker} {arg_pdk_root} {arg_pdk} --run-tag wokwi --force-run-dir runs/wokwi {arg_progress} src/config_merged.json"
|
|
|
|
env = os.environ.copy()
|
|
logging.debug(harden_cmd)
|
|
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/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")
|
|
run_dir = os.path.join(self.local_dir, "runs/wokwi")
|
|
with open(os.path.join(run_dir, "resolved.json"), "r") as f:
|
|
ll_config = json.load(f)
|
|
librelane_version = ll_config["meta"]["librelane_version"]
|
|
with open(os.path.join(run_dir, "pdk.json"), "w") as f:
|
|
pdk_version_info = self.tech.read_pdk_version(ll_config["PDK_ROOT"])
|
|
pdk_json = {
|
|
"FLOW_NAME": "LibreLane",
|
|
"FLOW_VERSION": librelane_version,
|
|
"PDK": ll_config["PDK"],
|
|
"PDK_SOURCE": pdk_version_info["source"],
|
|
"PDK_VERSION": pdk_version_info["version"],
|
|
}
|
|
json.dump(pdk_json, f, indent=2)
|
|
|
|
os.chdir(cwd)
|
|
|
|
def create_tt_submission(self):
|
|
top_module = self.get_macro_name()
|
|
logging.info(f"Creating TT submission for {top_module}")
|
|
tt_submission_dir = os.path.join(self.local_dir, "tt_submission")
|
|
shutil.rmtree(tt_submission_dir, ignore_errors=True)
|
|
stats_dir = os.path.join(tt_submission_dir, "stats")
|
|
os.makedirs(stats_dir, exist_ok=True)
|
|
run_dir = os.path.join(self.local_dir, "runs/wokwi")
|
|
final_dir = os.path.join(run_dir, "final")
|
|
|
|
# Convert to OAS:
|
|
# strm2oas tt_submission/${TOP_MODULE}.gds tt_submission/${TOP_MODULE}.oas
|
|
oas_file = os.path.join("tt_submission", f"{top_module}.oas")
|
|
gds_file = os.path.join(final_dir, "gds", f"{top_module}.gds")
|
|
layout = pya.Layout()
|
|
layout.read(gds_file)
|
|
layout.set_property("TT_PDK", self.pdk)
|
|
layout.write(oas_file)
|
|
|
|
files_to_copy = [
|
|
os.path.join(run_dir, "pdk.json"),
|
|
os.path.join(run_dir, "resolved.json"),
|
|
os.path.join(final_dir, "commit_id.json"),
|
|
os.path.join(final_dir, "gds", f"{top_module}.gds"),
|
|
os.path.join(final_dir, "lef", f"{top_module}.lef"),
|
|
*glob.glob(os.path.join(final_dir, "spef/*/*.spef")),
|
|
]
|
|
for file in files_to_copy:
|
|
shutil.copyfile(
|
|
file, os.path.join(tt_submission_dir, os.path.basename(file))
|
|
)
|
|
|
|
# GL netlist
|
|
gl_type = self.tech.netlist_type
|
|
gl_file = os.path.join(final_dir, gl_type, f"{top_module}.{gl_type}.v")
|
|
shutil.copyfile(gl_file, os.path.join(tt_submission_dir, f"{top_module}.v"))
|
|
|
|
# Stats:
|
|
shutil.copyfile(
|
|
os.path.join(final_dir, "metrics.csv"),
|
|
os.path.join(stats_dir, "metrics.csv"),
|
|
)
|
|
(yosys_synth_dir,) = glob.glob(os.path.join(run_dir, "*yosys-synthesis"))
|
|
shutil.copyfile(
|
|
os.path.join(yosys_synth_dir, "reports", "stat.rpt"),
|
|
os.path.join(stats_dir, "synthesis-stats.txt"),
|
|
)
|
|
|
|
def run_custom_librelane_flow(self, flow_name: str, no_docker: bool = False):
|
|
logging.info(f"Running {flow_name} for {self}")
|
|
|
|
# Nix/No-Docker support: Conditionally include docker flags
|
|
arg_docker = "" if no_docker else "--docker-no-tty --dockerized"
|
|
|
|
ll_cmd = f"python -m librelane {arg_docker} {self.tech.librelane_pdk_args} --run-tag wokwi --force-run-dir {self.local_dir}/runs/wokwi {self.local_dir}/src/config_merged.json --flow {flow_name}"
|
|
|
|
logging.debug(ll_cmd)
|
|
p = subprocess.run(ll_cmd, shell=True)
|
|
if p.returncode != 0:
|
|
logging.error("librelane failed")
|
|
exit(1)
|
|
|
|
def create_project_datasheet(self, template_version: str):
|
|
template_args = copy.deepcopy(self.info.__dict__)
|
|
template_args.update(
|
|
{
|
|
"pins": [
|
|
{
|
|
"pin_index": str(i),
|
|
"ui": self.info.pinout.ui[i],
|
|
"uo": self.info.pinout.uo[i],
|
|
"uio": self.info.pinout.uio[i],
|
|
}
|
|
for i in range(8)
|
|
],
|
|
"analog_pins": [
|
|
{
|
|
"ua_index": str(i),
|
|
"analog_index": "?",
|
|
"desc": desc,
|
|
}
|
|
for i, desc in enumerate(self.info.pinout.ua)
|
|
],
|
|
"is_analog": self.info.is_analog,
|
|
"uses_3v3": self.info.uses_3v3,
|
|
}
|
|
)
|
|
|
|
logging.info("creating project datasheet")
|
|
|
|
with open(os.path.join(SCRIPT_DIR, "docs/user_project.typ.mustache")) as f:
|
|
project_template = f.read()
|
|
|
|
result = DocsHelper.get_docs_as_typst(
|
|
os.path.join(self.local_dir, "docs/info.md")
|
|
)
|
|
|
|
content = {
|
|
"template_version": template_version,
|
|
"project_title": template_args["title"].replace('"', '\\"'),
|
|
"project_author": f"({DocsHelper.format_authors(template_args['author'])})",
|
|
"project_repo_link": self.get_git_remote(),
|
|
"project_description": template_args["description"],
|
|
"project_address": "----",
|
|
"project_clock": DocsHelper.pretty_clock(self.info.clock_hz),
|
|
"project_type": DocsHelper.get_project_type(
|
|
template_args["language"], self.is_wokwi(), template_args["is_analog"]
|
|
),
|
|
"project_doc_body": result,
|
|
"digital_pins": DocsHelper.format_digital_pins(template_args["pins"]),
|
|
}
|
|
|
|
if self.is_wokwi():
|
|
content["is_wokwi"] = True
|
|
content["project_wokwi_id"] = self.info.wokwi_id
|
|
|
|
if template_args["is_analog"]:
|
|
content["is_analog"] = True
|
|
content["analog_pins"] = DocsHelper.format_analog_pins(
|
|
template_args["analog_pins"]
|
|
)
|
|
|
|
with open(os.path.abspath(f"./docs/doc.typ"), "w") as f:
|
|
logging.info("writing datasheet to ./docs/doc.typ")
|
|
f.write(chevron.render(project_template, content))
|
|
|
|
DocsHelper.compile("./docs/doc.typ")
|
|
|
|
# Return final GDS used for create_svg and create_png
|
|
def get_final_gds(self):
|
|
if "GDS_PATH" in os.environ:
|
|
return os.environ["GDS_PATH"]
|
|
else:
|
|
return glob.glob(
|
|
os.path.join(self.local_dir, "runs/wokwi/final/gds/*.gds")
|
|
)[0]
|
|
|
|
# SVG render of the GDS
|
|
def create_svg(self):
|
|
render_svg(self.get_final_gds(), scramble_cells=self.tech.scramble_cells)
|
|
|
|
def create_png(self):
|
|
if self.info.tiles == "8x2":
|
|
quality = "0-10" # Compress more for 8x2 tiles.
|
|
else:
|
|
quality = "0-30"
|
|
render_png(
|
|
self.get_final_gds(),
|
|
scramble_cells=self.tech.scramble_cells,
|
|
buried_layers=self.tech.buried_layers,
|
|
quality=quality,
|
|
)
|
|
|
|
def print_warnings(self):
|
|
warnings: typing.List[str] = []
|
|
|
|
synth_log_glob = "runs/wokwi/*-yosys-synthesis/yosys-synthesis.log"
|
|
synth_log = glob.glob(os.path.join(self.local_dir, synth_log_glob))[0]
|
|
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())
|
|
|
|
tt_corner = self.tech.tt_corner
|
|
sta_report_glob = f"runs/wokwi/*-openroad-stapostpnr/{tt_corner}/checks.rpt"
|
|
sta_report = glob.glob(os.path.join(self.local_dir, sta_report_glob))[0]
|
|
with open(os.path.join(self.local_dir, sta_report)) as f:
|
|
for line in f.readlines():
|
|
if line.startswith("Warning:") and "clock" in line:
|
|
warnings.append(line.strip())
|
|
|
|
if len(warnings):
|
|
print("# Synthesis warnings")
|
|
print()
|
|
for warning in warnings:
|
|
print(f"* {warning}")
|
|
|
|
def print_stats(self):
|
|
util_glob = "runs/wokwi/*-openroad-globalplacement/openroad-globalplacement.log"
|
|
util_log = glob.glob(os.path.join(self.local_dir, util_glob))[0]
|
|
util_regexp = r"^\[INFO GPL-0019\] (Util|Utilization):"
|
|
util_line = next(line for line in open(util_log) if re.match(util_regexp, line))
|
|
util = re.sub(util_regexp, "", util_line).strip()
|
|
|
|
try:
|
|
wire_length = self.metrics["route__wirelength"] # OpenROAD #2047
|
|
except KeyError:
|
|
wire_length = self.metrics["detailedroute__route__wirelength"]
|
|
|
|
print("# Routing stats")
|
|
print()
|
|
print("| Utilisation (%) | Wire length (um) |")
|
|
print("|-------------|------------------|")
|
|
print("| {} | {} |".format(util, wire_length))
|
|
|
|
# Print the summaries
|
|
def summarize(self, print_cell_category: bool, print_cell_summary: bool):
|
|
cell_count = self.get_cell_counts_from_gl()
|
|
|
|
Categories = typing.TypedDict(
|
|
"Categories", {"categories": typing.List[str], "map": typing.Dict[str, int]}
|
|
)
|
|
categories_file = f"tech/{self.pdk}/categories.json"
|
|
with open(os.path.join(SCRIPT_DIR, categories_file)) as fh:
|
|
categories: Categories = json.load(fh)
|
|
|
|
cell_defs = self.tech.load_cell_definitions()
|
|
|
|
# print all used cells, sorted by frequency
|
|
total = 0
|
|
if 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_def = cell_defs.get(name)
|
|
if cell_def is not None:
|
|
cell_link = cell_def["url"]
|
|
description = cell_def["description"]
|
|
print(f"| [{name}]({cell_link}) | {description} |{count} |")
|
|
else:
|
|
print(f"| {name} | (unknown) | {count} |")
|
|
|
|
print(f"| | Total | {total} |")
|
|
|
|
if 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 = []
|
|
for name in cat_dict["examples"]:
|
|
cell_def = cell_defs.get(name)
|
|
if cell_def is not None:
|
|
cell_links.append(f"[{name}]({cell_def['url']})")
|
|
else:
|
|
cell_links.append(name)
|
|
|
|
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}/stats/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 LibreLane 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(self.tech.cell_regexp, line)
|
|
if m is not None:
|
|
total += 1
|
|
gd = m.groupdict()
|
|
cell_name = gd["cell_name"]
|
|
try:
|
|
cell_count[cell_name] += 1
|
|
except KeyError:
|
|
cell_count[cell_name] = 1
|
|
return cell_count
|