mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
wip
This commit is contained in:
+271
@@ -0,0 +1,271 @@
|
|||||||
|
class CaravelConfig():
|
||||||
|
|
||||||
|
def __init__(self, projects, num_projects):
|
||||||
|
self.projects = projects
|
||||||
|
self.num_projects = num_projects
|
||||||
|
|
||||||
|
# create macro file & positions, power hooks
|
||||||
|
def create_macro_config(self):
|
||||||
|
# array size
|
||||||
|
rows = 18
|
||||||
|
cols = 14
|
||||||
|
|
||||||
|
# start point (lower left)
|
||||||
|
start_x = 50
|
||||||
|
start_y = 95
|
||||||
|
|
||||||
|
# module block sizes
|
||||||
|
scanchain_w = 30
|
||||||
|
scanchain_spc = 6
|
||||||
|
module_w = 150
|
||||||
|
module_h = 170
|
||||||
|
|
||||||
|
# how much x & y space to leave between blocks
|
||||||
|
space_x = 15
|
||||||
|
space_y = 15
|
||||||
|
|
||||||
|
# step sizes
|
||||||
|
step_x = scanchain_w + module_w + scanchain_spc + space_x
|
||||||
|
step_y = module_h + space_y
|
||||||
|
|
||||||
|
logging.info(f"start_x {start_x} start_y {start_y} step_x {step_x} step_y {step_y }")
|
||||||
|
|
||||||
|
num_macros_placed = 0
|
||||||
|
|
||||||
|
# macro.cfg: where macros are placed
|
||||||
|
logging.info("creating macro.cfg")
|
||||||
|
with open("openlane/user_project_wrapper/macro.cfg", 'w') as fh:
|
||||||
|
fh.write("scan_controller 100 100 N\n")
|
||||||
|
for row in range(rows):
|
||||||
|
if row % 2 == 0:
|
||||||
|
col_order = range(cols)
|
||||||
|
orientation = 'N'
|
||||||
|
else:
|
||||||
|
# reverse odd rows to place instances in a zig zag pattern, shortening the scan chain wires
|
||||||
|
col_order = range(cols - 1, -1, -1)
|
||||||
|
orientation = 'S'
|
||||||
|
for col in col_order:
|
||||||
|
# skip the space where the scan controller goes on the first row
|
||||||
|
if row == 0 and col <= 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if num_macros_placed < self.num_projects:
|
||||||
|
if orientation == 'N':
|
||||||
|
# scanchain first
|
||||||
|
# co-ords are bottom left corner
|
||||||
|
macro_instance = self.projects[num_macros_placed].get_scanchain_instance()
|
||||||
|
instance = "{} {:<4} {:<4} {}\n".format(
|
||||||
|
macro_instance, start_x + col * step_x, start_y + row * step_y, orientation
|
||||||
|
)
|
||||||
|
fh.write(instance)
|
||||||
|
|
||||||
|
macro_instance = self.projects[num_macros_placed].get_macro_instance()
|
||||||
|
instance = "{} {:<4} {:<4} {}\n".format(
|
||||||
|
macro_instance, start_x + scanchain_spc + scanchain_w + col * step_x, start_y + row * step_y, orientation
|
||||||
|
)
|
||||||
|
fh.write(instance)
|
||||||
|
else:
|
||||||
|
# macro first
|
||||||
|
macro_instance = self.projects[num_macros_placed].get_macro_instance()
|
||||||
|
instance = "{} {:<4} {:<4} {}\n".format(
|
||||||
|
macro_instance, start_x + col * step_x, start_y + row * step_y, orientation
|
||||||
|
)
|
||||||
|
fh.write(instance)
|
||||||
|
|
||||||
|
macro_instance = self.projects[num_macros_placed].get_scanchain_instance()
|
||||||
|
instance = "{} {:<4} {:<4} {}\n".format(
|
||||||
|
macro_instance, start_x + module_w + scanchain_spc + col * step_x, start_y + row * step_y, orientation
|
||||||
|
)
|
||||||
|
fh.write(instance)
|
||||||
|
|
||||||
|
num_macros_placed += 1
|
||||||
|
|
||||||
|
logging.info(f"total user macros placed: {num_macros_placed}")
|
||||||
|
|
||||||
|
# macro_power.tcl: extra file for macro power hooks
|
||||||
|
logging.info("creating macro_power.tcl")
|
||||||
|
with open("openlane/user_project_wrapper/macro_power.tcl", 'w') as fh:
|
||||||
|
fh.write('set ::env(FP_PDN_MACRO_HOOKS) "\\\n')
|
||||||
|
fh.write(" ")
|
||||||
|
fh.write("scan_controller")
|
||||||
|
fh.write(" vccd1 vssd1 vccd1 vssd1")
|
||||||
|
fh.write(", \\\n")
|
||||||
|
for i in range(self.num_projects):
|
||||||
|
fh.write(" ")
|
||||||
|
fh.write(self.projects[i].get_scanchain_instance())
|
||||||
|
fh.write(" vccd1 vssd1 vccd1 vssd1, \\\n")
|
||||||
|
fh.write(" ")
|
||||||
|
fh.write(self.projects[i].get_macro_instance())
|
||||||
|
fh.write(" vccd1 vssd1 vccd1 vssd1")
|
||||||
|
if i != self.num_projects - 1:
|
||||||
|
fh.write(", \\\n")
|
||||||
|
fh.write('"\n')
|
||||||
|
|
||||||
|
# extra_lef_gds.tcl
|
||||||
|
lefs = []
|
||||||
|
gdss = []
|
||||||
|
logging.info("creating extra_lef_gds.tcl")
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
lefs.append(project.get_macro_lef_filename())
|
||||||
|
gdss.append(project.get_macro_gds_filename())
|
||||||
|
|
||||||
|
with open("openlane/user_project_wrapper/extra_lef_gds.tcl", 'w') as fh:
|
||||||
|
fh.write('set ::env(EXTRA_LEFS) "\\\n')
|
||||||
|
fh.write("$script_dir/../../lef/scan_controller.lef \\\n")
|
||||||
|
fh.write("$script_dir/../../lef/scanchain.lef \\\n")
|
||||||
|
for i, lef in enumerate(lefs):
|
||||||
|
fh.write("$script_dir/../../lef/{}".format(lef))
|
||||||
|
if i != len(lefs) - 1:
|
||||||
|
fh.write(" \\\n")
|
||||||
|
else:
|
||||||
|
fh.write('"\n')
|
||||||
|
fh.write('set ::env(EXTRA_GDS_FILES) "\\\n')
|
||||||
|
fh.write("$script_dir/../../gds/scan_controller.gds \\\n")
|
||||||
|
fh.write("$script_dir/../../gds/scanchain.gds \\\n")
|
||||||
|
for i, gds in enumerate(gdss):
|
||||||
|
fh.write("$script_dir/../../gds/{}".format(gds))
|
||||||
|
if i != len(gdss) - 1:
|
||||||
|
fh.write(" \\\n")
|
||||||
|
else:
|
||||||
|
fh.write('"\n')
|
||||||
|
|
||||||
|
# instantiate inside user_project_wrapper
|
||||||
|
def instantiate(self):
|
||||||
|
logging.info("instantiating designs in user_project_wrapper.v")
|
||||||
|
|
||||||
|
# NOTE: The user project wrapper initially used vectored signals for clk,
|
||||||
|
# scan, and latch signals. However, this leads to atrocious sim
|
||||||
|
# performance, as any change within the vectored signal is
|
||||||
|
# interpreted as a trigger condition for re-evaluating logic (at
|
||||||
|
# least this is the case under Icarus and Verilator). Therefore
|
||||||
|
# single bit signals are used between stages to limit the impact
|
||||||
|
# of any wire changing.
|
||||||
|
|
||||||
|
# Instance the scan controller
|
||||||
|
body = [
|
||||||
|
"",
|
||||||
|
"wire sc_clk_out, sc_data_out, sc_latch_out, sc_scan_out;",
|
||||||
|
"wire sc_clk_in, sc_data_in;",
|
||||||
|
"",
|
||||||
|
f"scan_controller #(.NUM_DESIGNS({self.num_projects})) scan_controller (",
|
||||||
|
" .clk (wb_clk_i),",
|
||||||
|
" .reset (wb_rst_i),",
|
||||||
|
" .active_select (io_in[20:12]),",
|
||||||
|
" .inputs (io_in[28:21]),",
|
||||||
|
" .outputs (io_out[36:29]),",
|
||||||
|
" .ready (io_out[37]),",
|
||||||
|
" .slow_clk (io_out[10]),",
|
||||||
|
" .set_clk_div (io_in[11]),",
|
||||||
|
"",
|
||||||
|
" .scan_clk_out (sc_clk_out),",
|
||||||
|
" .scan_clk_in (sc_clk_in),",
|
||||||
|
" .scan_data_out (sc_data_out),",
|
||||||
|
" .scan_data_in (sc_data_in),",
|
||||||
|
" .scan_select (sc_scan_out),",
|
||||||
|
" .scan_latch_en (sc_latch_out),",
|
||||||
|
"",
|
||||||
|
" .la_scan_clk_in (la_data_in[0]),",
|
||||||
|
" .la_scan_data_in (la_data_in[1]),",
|
||||||
|
" .la_scan_data_out (la_data_out[0]),",
|
||||||
|
" .la_scan_select (la_data_in[2]),",
|
||||||
|
" .la_scan_latch_en (la_data_in[3]),",
|
||||||
|
"",
|
||||||
|
" .driver_sel (io_in[9:8]),",
|
||||||
|
"",
|
||||||
|
" .oeb (io_oeb)",
|
||||||
|
");",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Instance every design on the scan chain
|
||||||
|
for idx in range(self.num_projects):
|
||||||
|
# First design driven by scan controller, all others are chained
|
||||||
|
pfx = f"sw_{idx:03d}"
|
||||||
|
prev_pfx = f"sw_{idx-1:03d}" if idx > 0 else "sc"
|
||||||
|
# Pickup the Wokwi design ID and github URL for the project
|
||||||
|
giturl = self.projects[idx].get_git_url()
|
||||||
|
|
||||||
|
# Append the instance to the body
|
||||||
|
body += [
|
||||||
|
"",
|
||||||
|
f"// [{idx:03d}] {giturl}",
|
||||||
|
f"wire {pfx}_clk_out, {pfx}_data_out, {pfx}_scan_out, {pfx}_latch_out;",
|
||||||
|
f"wire [7:0] {pfx}_module_data_in;",
|
||||||
|
f"wire [7:0] {pfx}_module_data_out;",
|
||||||
|
f"scanchain #(.NUM_IOS(8)) {self.projects[idx].get_scanchain_instance()} (",
|
||||||
|
f" .clk_in ({prev_pfx}_clk_out),",
|
||||||
|
f" .data_in ({prev_pfx}_data_out),",
|
||||||
|
f" .scan_select_in ({prev_pfx}_scan_out),",
|
||||||
|
f" .latch_enable_in ({prev_pfx}_latch_out),",
|
||||||
|
f" .clk_out ({pfx}_clk_out),",
|
||||||
|
f" .data_out ({pfx}_data_out),",
|
||||||
|
f" .scan_select_out ({pfx}_scan_out),",
|
||||||
|
f" .latch_enable_out({pfx}_latch_out),",
|
||||||
|
f" .module_data_in ({pfx}_module_data_in),",
|
||||||
|
f" .module_data_out ({pfx}_module_data_out)",
|
||||||
|
");"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Append the user module to the body
|
||||||
|
body += [
|
||||||
|
"",
|
||||||
|
f"{self.projects[idx].get_macro_name()} {self.projects[idx].get_macro_instance()} (",
|
||||||
|
f" .io_in ({pfx}_module_data_in),",
|
||||||
|
f" .io_out ({pfx}_module_data_out)",
|
||||||
|
");"
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
# Link the final design back to the scan controller
|
||||||
|
body += [
|
||||||
|
"",
|
||||||
|
"// Connect final signals back to the scan controller",
|
||||||
|
f"assign sc_clk_in = sw_{idx:03d}_clk_out;",
|
||||||
|
f"assign sc_data_in = sw_{idx:03d}_data_out;",
|
||||||
|
"",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
|
||||||
|
# Write to file
|
||||||
|
with open('verilog/rtl/user_project_wrapper.v', 'w') as fh:
|
||||||
|
# Insert the Caravel preamble
|
||||||
|
with open("upw_pre.v", "r") as fh_pre:
|
||||||
|
fh.write(fh_pre.read())
|
||||||
|
# Indent, join, and insert the module instances
|
||||||
|
fh.write("\n".join([(" " + x).rstrip() for x in body]))
|
||||||
|
# Insert the Caravel postamble
|
||||||
|
with open("upw_post.v", "r") as fh_post:
|
||||||
|
fh.write(fh_post.read())
|
||||||
|
|
||||||
|
# build the blackbox_project_includes.v file - used for blackboxing when building the GDS
|
||||||
|
with open('verilog/blackbox_project_includes.v', 'w') as fh:
|
||||||
|
fh.write('`include "rtl/scan_controller/scan_controller.v"\n')
|
||||||
|
fh.write('`include "rtl/scanchain/scanchain.v"\n')
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
fh.write(f'`include "gl/{project.get_gl_verilog_filename()}"\n')
|
||||||
|
|
||||||
|
# build complete list of filenames for sim
|
||||||
|
with open('verilog/includes/includes.rtl.caravel_user_project', 'w') as fh:
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/rtl/user_project_wrapper.v\n')
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/rtl/scan_controller/scan_controller.v\n')
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/rtl/scanchain/scanchain.v\n')
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/rtl/cells.v\n')
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
fh.write(f'-v $(USER_PROJECT_VERILOG)/rtl/{project.get_top_verilog_filename()}\n')
|
||||||
|
|
||||||
|
# build GL includes
|
||||||
|
with open('verilog/includes/includes.gl.caravel_user_project', 'w') as fh:
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/gl/user_project_wrapper.v\n')
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/gl/scan_controller.v\n')
|
||||||
|
fh.write('-v $(USER_PROJECT_VERILOG)/gl/scanchain.v\n')
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
fh.write(f'-v $(USER_PROJECT_VERILOG)/gl/{project.get_gl_verilog_filename()}\n')
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
for project in self.projects:
|
||||||
|
logging.info(project)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
start_date: '2023-03-01'
|
||||||
|
filler_project: https://github.com/TinyTapeout/tt03-test-invert
|
||||||
|
project_dir: 'projects'
|
||||||
|
tmp_dir: '/tmp/tt'
|
||||||
|
num_projects: 250
|
||||||
Executable
+263
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re, glob, json, csv
|
||||||
|
from orders import Orders
|
||||||
|
import datetime
|
||||||
|
import yaml
|
||||||
|
import subprocess
|
||||||
|
import argparse, logging, shutil, sys, os, collections
|
||||||
|
from git_utils import install_artifacts, get_latest_action_url, unique
|
||||||
|
import git
|
||||||
|
from project import Project
|
||||||
|
|
||||||
|
|
||||||
|
# pipe handling
|
||||||
|
from signal import signal, SIGPIPE, SIG_DFL
|
||||||
|
signal(SIGPIPE, SIG_DFL)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
# if the project is a filler, then use the config from the first fill project
|
||||||
|
def gen_local_dir(self, fill_id):
|
||||||
|
if self.fill:
|
||||||
|
return os.path.join(os.path.join(self.project_dir, f'{fill_id :03}'))
|
||||||
|
else:
|
||||||
|
return os.path.join(os.path.join(self.project_dir, f'{self.index :03}'))
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class Projects():
|
||||||
|
|
||||||
|
def __init__(self, config, args):
|
||||||
|
self.args = args
|
||||||
|
self.project_dir = config['project_dir']
|
||||||
|
self.config = config
|
||||||
|
if not os.path.exists(self.project_dir):
|
||||||
|
os.makedirs(self.project_dir)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open('projects.yaml') as fh:
|
||||||
|
self.project_config = yaml.safe_load(fh)
|
||||||
|
except FileNotFoundError:
|
||||||
|
logging.error("projects.yaml not found, create it with --update-orders")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# useful for contacting people when I find problems in the repo
|
||||||
|
try:
|
||||||
|
with open('git_url_to_email.json') as fh:
|
||||||
|
self.git_url_to_email_map = json.load(fh)
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.git_url_to_email_map = {}
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.projects = []
|
||||||
|
for index in range(args.limit_num_projects):
|
||||||
|
if args.single >= 0 and args.single != index:
|
||||||
|
continue
|
||||||
|
if index < args.start_from:
|
||||||
|
continue
|
||||||
|
if args.end_at != 0 and index > args.end_at:
|
||||||
|
continue
|
||||||
|
if self.project_config[index]['status'] == 'disable':
|
||||||
|
continue
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
email = self.git_url_to_email_map[git_url]
|
||||||
|
except KeyError:
|
||||||
|
email = 'none'
|
||||||
|
"""
|
||||||
|
|
||||||
|
git_url = self.project_config[index]['url']
|
||||||
|
filler = self.project_config[index]['fill']
|
||||||
|
|
||||||
|
# fill projects don't have their own directory, reuse the first project which is always fill
|
||||||
|
if filler:
|
||||||
|
project_dir = os.path.join(os.path.join(self.project_dir, f'{0 :03}'))
|
||||||
|
else:
|
||||||
|
project_dir = os.path.join(os.path.join(self.project_dir, f'{index :03}'))
|
||||||
|
|
||||||
|
project = Project(index, git_url, project_dir, args, fill=filler)
|
||||||
|
|
||||||
|
# clone git repos locally & gds artifacts from action build
|
||||||
|
if args.clone_all:
|
||||||
|
if filler is False:
|
||||||
|
logging.info(f"cloning {project}")
|
||||||
|
project.clone()
|
||||||
|
|
||||||
|
if args.fetch_gds:
|
||||||
|
if filler is False:
|
||||||
|
logging.info(f"fetching gds for {project}")
|
||||||
|
project.fetch_gds()
|
||||||
|
|
||||||
|
if args.update_all:
|
||||||
|
if filler is False:
|
||||||
|
# only updates code, not gds artifacts
|
||||||
|
logging.info(f"git pull for {project}")
|
||||||
|
project.pull()
|
||||||
|
|
||||||
|
# projects should now be installed, so load all the data from the yaml files
|
||||||
|
logging.debug("post clone setup")
|
||||||
|
# fill projects will load from the fill project's directory
|
||||||
|
project.post_clone_setup()
|
||||||
|
logging.debug(project)
|
||||||
|
|
||||||
|
if args.harden:
|
||||||
|
if filler is False:
|
||||||
|
project.harden()
|
||||||
|
|
||||||
|
if args.update_caravel:
|
||||||
|
logging.debug("copying files to caravel")
|
||||||
|
if filler is False:
|
||||||
|
project.copy_files_to_caravel()
|
||||||
|
|
||||||
|
# check all top level module ports are correct
|
||||||
|
project.check_ports()
|
||||||
|
project.check_num_cells()
|
||||||
|
|
||||||
|
self.projects.append(project)
|
||||||
|
|
||||||
|
# now do some sanity checks
|
||||||
|
all_macro_instances = [project.get_macro_instance() for project in self.projects]
|
||||||
|
self.assert_unique(all_macro_instances)
|
||||||
|
|
||||||
|
all_top_files = [project.get_top_verilog_filename() for project in self.projects if not project.is_fill()]
|
||||||
|
self.assert_unique(all_top_files)
|
||||||
|
|
||||||
|
all_gds_files = [project.get_macro_gds_filename() for project in self.projects if not project.is_fill()]
|
||||||
|
self.assert_unique(all_gds_files)
|
||||||
|
|
||||||
|
|
||||||
|
def assert_unique(self, check):
|
||||||
|
duplicates = [item for item, count in collections.Counter(check).items() if count > 1]
|
||||||
|
if duplicates:
|
||||||
|
logging.error("duplicate projects: {}".format(duplicates))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
def build_metrics(self):
|
||||||
|
total_seconds = 0
|
||||||
|
total_wire_length = 0
|
||||||
|
total_wires_count = 0
|
||||||
|
total_physical_cells = 0
|
||||||
|
max_cells = 0
|
||||||
|
min_cells = 1000
|
||||||
|
max_util = 0
|
||||||
|
min_util = 100
|
||||||
|
languages = {}
|
||||||
|
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
dt = datetime.datetime.strptime(project.metrics['total_runtime'][:-3], '%Hh%Mm%Ss')
|
||||||
|
delt = datetime.timedelta(hours=dt.hour, minutes=dt.minute, seconds=dt.second)
|
||||||
|
total_seconds += delt.total_seconds()
|
||||||
|
|
||||||
|
total_wire_length += int(project.metrics['wire_length'])
|
||||||
|
total_wires_count += int(project.metrics['wires_count'])
|
||||||
|
util = float(project.metrics['OpenDP_Util'])
|
||||||
|
num_cells = project.check_num_cells()
|
||||||
|
total_physical_cells += num_cells
|
||||||
|
|
||||||
|
yaml_data = project.get_project_doc_yaml()
|
||||||
|
lang = yaml_data['language'].lower()
|
||||||
|
if lang in languages:
|
||||||
|
languages[lang] += 1
|
||||||
|
else:
|
||||||
|
languages[lang] = 1
|
||||||
|
|
||||||
|
if num_cells > max_cells:
|
||||||
|
max_cells = num_cells
|
||||||
|
max_cell_project = project
|
||||||
|
if num_cells < min_cells:
|
||||||
|
min_cells = num_cells
|
||||||
|
|
||||||
|
if util > max_util:
|
||||||
|
max_util = util
|
||||||
|
max_util_project = project
|
||||||
|
if util < min_util:
|
||||||
|
min_util = util
|
||||||
|
|
||||||
|
|
||||||
|
logging.info(f"build time for all projects {total_seconds / 3600} hrs")
|
||||||
|
logging.info(f"total wire length {total_wire_length} um")
|
||||||
|
logging.info(f"total cells {total_physical_cells}")
|
||||||
|
logging.info(f"max cells {max_cells} for project {max_cell_project}")
|
||||||
|
logging.info(f"min cells {min_cells}")
|
||||||
|
logging.info(f"max util {max_util} for project {max_util_project}")
|
||||||
|
logging.info(f"min util {min_util}")
|
||||||
|
logging.info(f"languages {languages}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description="TinyTapeout configuration and docs")
|
||||||
|
|
||||||
|
with open('config.yaml') as fh:
|
||||||
|
config = yaml.safe_load(fh)
|
||||||
|
|
||||||
|
parser.add_argument('--update-orders', help="update the order config file", action="store_const", const=True)
|
||||||
|
parser.add_argument('--list', help="list projects", action='store_const', const=True)
|
||||||
|
parser.add_argument('--clone-all', help="clone all projects", action="store_const", const=True)
|
||||||
|
parser.add_argument('--update-all', help="git pull all projects", action="store_const", const=True)
|
||||||
|
parser.add_argument('--fetch-gds', help="git fetch latest gds", action="store_const", const=True)
|
||||||
|
parser.add_argument('--single', help="do action on single project", type=int, default=-1)
|
||||||
|
parser.add_argument('--start-from', help="do action on projects after this index", type=int, default=0)
|
||||||
|
parser.add_argument('--end-at', help="do action on projects before this index", type=int, default=0)
|
||||||
|
parser.add_argument('--update-caravel', help='configure caravel for build', action='store_const', const=True)
|
||||||
|
parser.add_argument('--harden', help="harden project", action="store_const", const=True)
|
||||||
|
parser.add_argument('--limit-num-projects', help='only configure for the first n projects', type=int, default=int(config['num_projects']))
|
||||||
|
parser.add_argument('--test', help='use test projects', action='store_const', const=True)
|
||||||
|
parser.add_argument('--debug', help="debug logging", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
|
||||||
|
parser.add_argument('--log-email', help="print persons email in messages", action="store_const", const=True)
|
||||||
|
parser.add_argument('--update-image', help="update the image", action="store_const", const=True)
|
||||||
|
parser.add_argument('--dump-json', help="dump json of all project data to given file")
|
||||||
|
parser.add_argument('--dump-markdown', help="dump markdown of all project data to given file")
|
||||||
|
parser.add_argument('--dump-pdf', help="create pdf from the markdown")
|
||||||
|
parser.add_argument('--build-hugo-content', help="directory to where to build hugo content")
|
||||||
|
parser.add_argument('--metrics', help="print some project metrics", action="store_const", const=True)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# setup log
|
||||||
|
log_format = logging.Formatter('%(asctime)s - %(levelname)-8s - %(message)s')
|
||||||
|
# configure the client logging
|
||||||
|
log = logging.getLogger('')
|
||||||
|
# has to be set to debug as is the root logger
|
||||||
|
log.setLevel(args.loglevel)
|
||||||
|
|
||||||
|
# create console handler and set level to info
|
||||||
|
ch = logging.StreamHandler(sys.stdout)
|
||||||
|
# create formatter for console
|
||||||
|
ch.setFormatter(log_format)
|
||||||
|
log.addHandler(ch)
|
||||||
|
|
||||||
|
projects = Projects(config, args)
|
||||||
|
|
||||||
|
# docs = Docs(projects.projects, args=args)
|
||||||
|
# caravel = CaravelConfig(projects.projects, num_projects=args.limit_num_projects)
|
||||||
|
|
||||||
|
if args.list:
|
||||||
|
caravel.list()
|
||||||
|
|
||||||
|
if args.metrics:
|
||||||
|
projects.build_metrics()
|
||||||
|
|
||||||
|
if args.update_orders:
|
||||||
|
orders = Orders(config)
|
||||||
|
orders.fetch_orders()
|
||||||
|
orders.update_project_list()
|
||||||
|
|
||||||
|
if args.update_caravel:
|
||||||
|
caravel.create_macro_config()
|
||||||
|
caravel.instantiate()
|
||||||
|
if not args.test:
|
||||||
|
docs.build_index()
|
||||||
|
|
||||||
|
if args.update_image:
|
||||||
|
docs.update_image()
|
||||||
|
|
||||||
|
if args.dump_json:
|
||||||
|
docs.dump_json()
|
||||||
|
|
||||||
|
if args.dump_markdown:
|
||||||
|
docs.dump_markdown()
|
||||||
|
|
||||||
|
if args.build_hugo_content:
|
||||||
|
docs.build_hugo_content()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Sponsored by
|
||||||
|
|
||||||
|
[](https://efabless.com/)
|
||||||
|
|
||||||
|
# Team
|
||||||
|
|
||||||
|
Tiny Tapeout would not be possible without a lot of people helping. We would especially like to thank:
|
||||||
|
|
||||||
|
* Uri Shaked for [wokwi](https://wokwi.com/) development and lots more
|
||||||
|
* [Sylvain Munaut](https://twitter.com/tnt) for help with scan chain improvements
|
||||||
|
* [Mike Thompson](https://www.linkedin.com/in/michael-thompson-0a581a/) for verification expertise
|
||||||
|
* [Jix](https://twitter.com/jix_) for formal verification support
|
||||||
|
* [Proppy](https://twitter.com/proppy) for help with GitHub actions
|
||||||
|
* [Maximo Balestrini](https://twitter.com/maxiborga) for all the amazing renders and the interactive GDS viewer
|
||||||
|
* James Rosenthal for coming up with digital design examples
|
||||||
|
* All the people who took part in [TinyTapeout 01](/runs/tt01) and volunteered time to improve docs and test the flow
|
||||||
|
* The team at [YosysHQ](https://www.yosyshq.com/) and all the other open source EDA tool makers
|
||||||
|
* [Efabless](https://efabless.com/) for running the shuttles and providing OpenLane and sponsorship
|
||||||
|
* [Tim Ansell and Google](https://www.youtube.com/watch?v=EczW2IWdnOM) for supporting the open source silicon movement
|
||||||
|
* [Zero to ASIC course](https://zerotoasiccourse.com/) community for all your support
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
# Technical info
|
||||||
|
|
||||||
|
## Scan chain
|
||||||
|
|
||||||
|
All 250 designs are joined together in a long chain similiar to JTAG. We provide the inputs and outputs of that chain (see pinout below) externally, to the Caravel logic analyser, and to an internal scan chain driver.
|
||||||
|
|
||||||
|
The default is to use an external driver, this is in case anything goes wrong with the Caravel logic analyser or the internal driver.
|
||||||
|
|
||||||
|
The scan chain is identical for each little project, and you can [read it here](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/main/template/scan_wrapper.v).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Updating inputs and outputs of a specified design
|
||||||
|
|
||||||
|
A good way to see how this works is to read the FSM in the [scan controller](verilog/rtl/scan_controller/scan_controller.v).
|
||||||
|
You can also run one of the simple tests and check the waveforms. See how in the [scan chain verification](verification.md) doc.
|
||||||
|
|
||||||
|
* Signal names are from the perspective of the scan chain driver.
|
||||||
|
* The desired project shall be called DUT (design under test)
|
||||||
|
|
||||||
|
Assuming you want to update DUT at position 2 (0 indexed) with inputs = 0x02 and then fetch the output.
|
||||||
|
This design connects an inverter between each input and output.
|
||||||
|
|
||||||
|
* Set scan_select low so that the data is clocked into the scan flops (rather than from the design)
|
||||||
|
* For the next 8 clocks, set scan_data_out to 0, 0, 0, 0, 0, 0, 1, 0
|
||||||
|
* Toggle scan_clk_out 16 times to deliver the data to the DUT
|
||||||
|
* Toggle scan_latch_en to deliver the data from the scan chain to the DUT
|
||||||
|
* Set scan_select high to set the scan flop's input to be from the DUT
|
||||||
|
* Toggle the scan_clk_out to capture the DUT's data into the scan chain
|
||||||
|
* Toggle the scan_clk_out another 8 x number of remaining designs to receive the data at scan_data_in
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Notes on understanding the trace*
|
||||||
|
|
||||||
|
* There are large wait times between the latch and scan signals to ensure no hold violations across the whole chain. For the internal scan controller, these can be configured (see section on wait states below).
|
||||||
|
* The input looks wrong (0x03) because the input is incremented by the test bench as soon as the scan controller captures the data. The input is actually 0x02.
|
||||||
|
* The output in the trace looks wrong (0xFE) because it's updated after a full refresh, the output is 0xFD.
|
||||||
|
|
||||||
|
## Clocking
|
||||||
|
|
||||||
|
Assuming:
|
||||||
|
|
||||||
|
* 100MHz input clock
|
||||||
|
* 8 ins & 8 outs
|
||||||
|
* 2 clock cycles to push one bit through the scan chain (scan clock is half input clock rate)
|
||||||
|
* 250 designs
|
||||||
|
* scan controller can do a read/write cycle in one refresh
|
||||||
|
|
||||||
|
So the max refresh rate is 100MHz / (8 * 2 * 250) = 25000Hz.
|
||||||
|
|
||||||
|
## Clock divider
|
||||||
|
|
||||||
|
A rising edge on the set_clk_div input will capture what is set on the input pins and use this as a divider for an internal slow clock that can be provided to the first input bit.
|
||||||
|
|
||||||
|
The slow clock is only enabled if the set_clk_div is set, and the resulting clock is connected to input0 and also output on the slow_clk pin.
|
||||||
|
|
||||||
|
The slow clock is synced with the scan rate. A divider of 0 mean it toggles the input0 every scan. Divider of 1 toggles it every 2 cycles.
|
||||||
|
So the resultant slow clock frequency is scan_rate / (2 * (N+1)).
|
||||||
|
|
||||||
|
See the test_clock_div test in the [scan chain verification](verification.md).
|
||||||
|
|
||||||
|
## Wait states
|
||||||
|
|
||||||
|
This dictates how many wait cycle we insert in various state
|
||||||
|
of the load process. We have a sane default, but also allow
|
||||||
|
override externally.
|
||||||
|
|
||||||
|
To override, set the wait amount on the inputs, set the driver_sel inputs both high, and then reset the chip.
|
||||||
|
|
||||||
|
See the test_wait_state test in the [scan chain verification](verification.md).
|
||||||
|
|
||||||
|
## Pinout
|
||||||
|
|
||||||
|
PIN NAME DESCRIPTION
|
||||||
|
20:12 active_select 9 bit input to set which design is active
|
||||||
|
28:21 inputs 8 inputs
|
||||||
|
36:29 outputs 8 outputs
|
||||||
|
37 ready goes high for one cycle everytime the scanchain is refreshed
|
||||||
|
10 slow_clk slow clock from internal clock divider
|
||||||
|
11 set_clk_div enable clock divider
|
||||||
|
9:8 driver_sel which scan chain driver: 00 = external, 01 = logic analyzer, 1x = internal
|
||||||
|
|
||||||
|
21 ext_scan_clk_out for external driver, clk input
|
||||||
|
22 ext_scan_data_out data input
|
||||||
|
23 ext_scan_select scan select
|
||||||
|
24 ext_scan_latch_en latch
|
||||||
|
29 ext_scan_clk_in clk output from end of chain
|
||||||
|
30 ext_scan_data_in data output from end of chain
|
||||||
|
|
||||||
|
## Instructions to build GDS
|
||||||
|
|
||||||
|
To run the tool locally or have a fork's GitHub action work, you need the GH_USERNAME and GH_TOKEN set in your environment.
|
||||||
|
|
||||||
|
GH_USERNAME should be set to your GitHub username.
|
||||||
|
|
||||||
|
To generate your GH_TOKEN go to https://github.com/settings/tokens/new . Set the checkboxes for repo and workflow.
|
||||||
|
|
||||||
|
To run locally, make a file like this:
|
||||||
|
|
||||||
|
export GH_USERNAME=<username>
|
||||||
|
export GH_TOKEN=<token>
|
||||||
|
|
||||||
|
And then source it before running the tool.
|
||||||
|
|
||||||
|
### Fetch all the projects
|
||||||
|
|
||||||
|
This goes through all the projects in project_urls.py, and fetches the latest artifact zip from GitHub. It takes the verilog, the GL verilog, and the GDS and copies
|
||||||
|
them to the correct place.
|
||||||
|
|
||||||
|
./configure.py --clone-all --fetch-gds
|
||||||
|
|
||||||
|
### Configure Caravel
|
||||||
|
|
||||||
|
Caravel needs the list of macros, how power is connected, instantiation of all the projects etc. This command builds these configs and also makes the README.md index.
|
||||||
|
|
||||||
|
./configure.py --update-caravel
|
||||||
|
|
||||||
|
### Build the GDS
|
||||||
|
|
||||||
|
To build the GDS and run the simulations, you will need to install the Sky130 PDK and OpenLane tool.
|
||||||
|
It takes about 5 minutes and needs about 3GB of disk space.
|
||||||
|
|
||||||
|
export PDK_ROOT=<some dir>/pdk
|
||||||
|
export OPENLANE_ROOT=<some dir>/openlane
|
||||||
|
cd <the root of this repo>
|
||||||
|
make setup
|
||||||
|
|
||||||
|
Then to create the GDS:
|
||||||
|
|
||||||
|
make user_project_wrapper
|
||||||
|
|
||||||
|
## Changing macro block size
|
||||||
|
|
||||||
|
After working out what size you want:
|
||||||
|
|
||||||
|
* adjust configure.py in `CaravelConfig.create_macro_config()`.
|
||||||
|
* adjust the PDN spacing to match in openlane/user_project_wrapper/config.tcl:
|
||||||
|
* ```set ::env(FP_PDN_HPITCH)```
|
||||||
|
* ```set ::env(FP_PDN_HOFFSET)```
|
||||||
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[](https://github.com/tinytapeout/tinytapeout-02/actions/workflows/gds.yaml)
|
||||||
|
[](https://github.com/tinytapeout/tinytapeout-02/actions/workflows/verification.yaml)
|
||||||
|
[](https://github.com/tinytapeout/tinytapeout-02/actions/workflows/tt_datasheet.yaml)
|
||||||
|
|
||||||
|
# TinyTapeout
|
||||||
|
|
||||||
|
* Visit https://tinytapeout.com for more information on the project and how to get involved.
|
||||||
|
* Read [INFO](INFO.md) for how the project is built and technical project notes.
|
||||||
|
* Read [Verification](VERIFICATION.md) for how we verify the project.
|
||||||
|
|
||||||
|
# Datasheet
|
||||||
|
|
||||||
|
[PDF Download](datasheet.pdf).
|
||||||
|
|
||||||
|
For the most recent automatic build, click the tt_datasheet badge above, then download the PDF artifact.
|
||||||
|
|
||||||
|
# GDS layout of all projects
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Project Index
|
||||||
|
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Verification
|
||||||
|
|
||||||
|
We are not trying to verify every single design. That is up to the person who makes it. What we want is to ensure that every design is accessible, even if some designs are broken.
|
||||||
|
|
||||||
|
We can split the verification effort into functional testing (simulation), static tests (formal verification), timing tests (STA) and physical tests (LVS & DRC).
|
||||||
|
|
||||||
|
See the sections below for details on each type of verification.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
You will need the GitHub tokens setup as described in [INFO](INFO.md#instructions-to-build-gds).
|
||||||
|
|
||||||
|
The default of 250 projects takes a very long time to simulate, so I advise overriding the configuration:
|
||||||
|
|
||||||
|
# fetch the test projects
|
||||||
|
./configure.py --test --clone-all
|
||||||
|
# rebuild config with only 20 projects
|
||||||
|
./configure.py --test --update-caravel --limit 20
|
||||||
|
|
||||||
|
You will also need iVerilog & cocotb. The easist way to install these are to download and install the [oss-cad-suite](https://github.com/YosysHQ/oss-cad-suite-build).
|
||||||
|
|
||||||
|
## Simulations
|
||||||
|
|
||||||
|
* Simulation of some test projects at RTL and GL level.
|
||||||
|
* Simulation of the whole chip with scan controller, external controller, logic analyser.
|
||||||
|
* Check wait state setting.
|
||||||
|
* Check clock divider setting.
|
||||||
|
|
||||||
|
### Scan controller
|
||||||
|
|
||||||
|
This test only instantiates user_project_wrapper (which contains all the small projects). It doesn't simulate the rest of the ASIC.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller
|
||||||
|
make test_scan_controller
|
||||||
|
|
||||||
|
The Gate Level simulation requires scan_controller and user_project_wrapper to be re-hardened to get the correct gate level netlists:
|
||||||
|
|
||||||
|
* Edit openlane/scan_controller/config.tcl and change NUM_DESIGNS=250 to NUM_DESIGNS=20.
|
||||||
|
* Then from the top level directory:
|
||||||
|
|
||||||
|
|
||||||
|
make scan_controller
|
||||||
|
make user_project_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
* Then run the GL test
|
||||||
|
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller
|
||||||
|
make test_scan_controller_gl
|
||||||
|
|
||||||
|
|
||||||
|
#### single
|
||||||
|
|
||||||
|
Just check one inverter module. Mainly for easy understanding of the traces.
|
||||||
|
|
||||||
|
make test_single
|
||||||
|
|
||||||
|
#### custom wait state
|
||||||
|
|
||||||
|
Just check one inverter module. Set a custom wait state value.
|
||||||
|
|
||||||
|
make test_wait_state
|
||||||
|
|
||||||
|
#### clock divider
|
||||||
|
|
||||||
|
Test one inverter module with an automatically generated clock on input 0. Sets the clock rate to 1/2 of the scan refresh rate.
|
||||||
|
|
||||||
|
make test_clock_div
|
||||||
|
|
||||||
|
## Top level tests setup
|
||||||
|
|
||||||
|
For all the top level tests, you will also need a [RISCV compiler to build the firmware](https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.1-x86_64-linux-ubuntu14.tar.gz).
|
||||||
|
|
||||||
|
You will also need to install the 'management core' for the Caravel ASIC submission wrapper. This is done automatically by following the [PDK install instructions](INFO.md#build-the-gds).
|
||||||
|
|
||||||
|
### Top level test: internal control
|
||||||
|
|
||||||
|
Uses the scan controller, instantiated inside the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_int
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
### Top level test: external control
|
||||||
|
|
||||||
|
Uses external signals to control the scan chain. Simulates the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_ext
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
### Top level test: logic analyser control
|
||||||
|
|
||||||
|
Uses the RISCV co-processor to drive the scanchain with firmware. Simulates the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_la
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
## Formal Verification
|
||||||
|
|
||||||
|
* Formal verification that each small project's scan chain is correct.
|
||||||
|
* Formal verification that the correct signals are passed through for the 3 different scan chain control modes.
|
||||||
|
|
||||||
|
### Scan chain
|
||||||
|
|
||||||
|
Each GL netlist for each small project is proven to be equivalent to the reference scan chain implementation.
|
||||||
|
The verification is done on the GL netlist, so an RTL version of the cells used needed to be created.
|
||||||
|
See [here for more info](tinytapeout_scan/README.md).
|
||||||
|
|
||||||
|
### Scan controller MUX
|
||||||
|
|
||||||
|
In case the internal scan controller doesn't work, we also have ability to control the chain from external pins or the Caravel Logic Analyser.
|
||||||
|
We implement a simple MUX to achieve this and [formally prove it is correct](verilog/rtl/scan_controller/properties.v).
|
||||||
|
|
||||||
|
## Timing constraints
|
||||||
|
|
||||||
|
Due to limitations in OpenLane - a top level timing analyis is not possible. This would allow us to detect setup and hold violations in the scan chain.
|
||||||
|
|
||||||
|
Instead, we design the chain and the timing constraints for each project and the scan controller with this in mind.
|
||||||
|
|
||||||
|
* [Each small project has a negedge flop flop at the end of the shift register to reclock the data](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/17f106db36f022536d013b960316bcc7f02c572c/template/scan_wrapper.v#L67). This gives more hold margin.
|
||||||
|
* [Each small project has SDC timing constraints](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/main/src/base.sdc)
|
||||||
|
* [Scan controller](https://github.com/mattvenn/tinytapeout-mpw7/blob/aacae16304f4a4878943a49fd479d8a284736e32/verilog/rtl/scan_controller/scan_controller.v#L334) uses a shift register clocked with the end of the chain to ensure correct data is captured.
|
||||||
|
* [Scan controller has its own SDC timing constraints](openlane/scan_controller/base.sdc)
|
||||||
|
* Scan controller can be configured to wait for a programmable time at latching data into the design and capturing it from the design.
|
||||||
|
* External pins (by default) control the scan chain.
|
||||||
|
|
||||||
|
## Physical tests
|
||||||
|
|
||||||
|
* LVS
|
||||||
|
* DRC
|
||||||
|
* CVC
|
||||||
|
|
||||||
|
### LVS
|
||||||
|
|
||||||
|
Each project is built with OpenLane, which will check LVS for each small project.
|
||||||
|
Then when we combine all the projects together we run a top level LVS & DRC for routing, power supply and macro placement.
|
||||||
|
|
||||||
|
The extracted netlist from the GDS is what is used in the formal scan chain proof.
|
||||||
|
|
||||||
|
### DRC
|
||||||
|
|
||||||
|
DRC is checked by OpenLane for each small project, and then again at the top level when we combine all the projects.
|
||||||
|
|
||||||
|
### CVC
|
||||||
|
|
||||||
|
Mitch Bailey' CVC checker is a device level static verification system for quickly and easily detecting common circuit errors in CDL (Circuit Definition Language) netlists. We ran the test on the final design and found no errors.
|
||||||
|
|
||||||
|
* See [the paper here](https://woset-workshop.github.io/PDFs/2020/a05-slides.pdf).
|
||||||
|
* Github repo for the tool: https://github.com/d-m-bailey/cvc
|
||||||
@@ -0,0 +1,709 @@
|
|||||||
|
---
|
||||||
|
title: |
|
||||||
|
{width=15cm}
|
||||||
|
Tiny Tapeout 02 Datasheet
|
||||||
|
subtitle: Project Repository [https://github.com/TinyTapeout/tinytapeout-02](https://github.com/TinyTapeout/tinytapeout-02)
|
||||||
|
documentclass: scrartcl
|
||||||
|
date: \today{}
|
||||||
|
geometry: margin=2cm
|
||||||
|
fontsize: 14pt
|
||||||
|
mainfont: Latin Modern Sans
|
||||||
|
header-includes:
|
||||||
|
- \hypersetup{colorlinks=false,
|
||||||
|
allbordercolors={0 0 0},
|
||||||
|
pdfborderstyle={/S/U/W 1}}
|
||||||
|
---
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
|
||||||
|
# Render of whole chip
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
|
||||||
|
# Projects
|
||||||
|
## 0 : Test Inverter Project
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* Author: Matt Venn
|
||||||
|
* Description: Inverts every line. This project is also used to fill any empty design spaces.
|
||||||
|
* [GitHub repository](https://github.com/TinyTapeout/tt03-test-invert)
|
||||||
|
* [Wokwi](https://wokwi.com/projects/357464855584307201) project
|
||||||
|
* [Extra docs]()
|
||||||
|
* Clock: 0 Hz
|
||||||
|
* External hardware:
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
Uses 8 inverters to invert every line.
|
||||||
|
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
Setting the input switch to on should turn the corresponding LED off.
|
||||||
|
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | a | segment a |
|
||||||
|
| 1 | b | segment b |
|
||||||
|
| 2 | c | segment c |
|
||||||
|
| 3 | d | segment d |
|
||||||
|
| 4 | e | segment e |
|
||||||
|
| 5 | f | segment f |
|
||||||
|
| 6 | g | segment g |
|
||||||
|
| 7 | dot | dot |
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
## 2 : 7 Segment Life
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* Author: icegoat9
|
||||||
|
* Description: Simple 7-segment cellular automaton
|
||||||
|
* [GitHub repository](https://github.com/icegoat9/tinytapeout03-7seglife)
|
||||||
|
* [Wokwi](https://wokwi.com/projects/357752736742764545) project
|
||||||
|
* [Extra docs](https://github.com/icegoat9/tinytapeout03-7seglife/blob/main/README.md)
|
||||||
|
* Clock: 0 Hz
|
||||||
|
* External hardware: None. Could add debounced momentary pushbuttons in parallel with dip switches 1,2,3 to make loading in new patterns and stepping through a run easier.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
See the [Wokwi gate layout and simulation](https://wokwi.com/projects/357752736742764545). At a high level...
|
||||||
|
|
||||||
|
* Seven flip-flops hold the cellular automaton's internal state, which is also displayed in the seven-segment display.
|
||||||
|
* Combinatorial logic generates the next state for each segment based on its neighbors, according to the ruleset...
|
||||||
|
* Living segments with exactly one living neighbor (another segment that touches it end to end) survive, and all others die.
|
||||||
|
* Dead segments with exactly two living neighbors come to life.
|
||||||
|
* When either the system clock or a user toggling the clock input go high, this new state is latched into the automaton's state.
|
||||||
|
* There's minor additional support logic to let the user manually shift in an initial condition and handle clock dividing.
|
||||||
|
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
For full details and a few 'exercises for the reader', see the [github README](https://github.com/icegoat9/tinytapeout03-7seglife) doc link. But at a high level, assuming the IC is mounted on the standard tinytapeout PCB which provides dip switches, clock, and a seven-segment display for output...
|
||||||
|
|
||||||
|
* Set all dip switches off and the clock slide switch to the 'manual' clock side.
|
||||||
|
* Power on the system. An arbitrary state may appear on the 7-segment display.
|
||||||
|
* Set dip switch 4 on ('run mode').
|
||||||
|
* Toggle dip switch 1 on and off to advance the automaton to the next state, you should see the 7-segment display update.
|
||||||
|
|
||||||
|
If you want to watch it run automatically (which may quickly settle on an empty state or a static pattern, depending on start state)...
|
||||||
|
|
||||||
|
* Set the PCB clock divider to the maximum clock division (255). With a system clock of 6.25kHz, the clock input should now be ~24.5Hz.
|
||||||
|
* Set dip switches 5 and 7 on to add a reasonable additional clock divider (see docs for more details on a higher or lower divider).
|
||||||
|
* Set dip switch 4 on.
|
||||||
|
* Switch the clock slide switch to the 'system clock' side. The display should advance at roughly 1.5Hz if I've done math correctly.
|
||||||
|
|
||||||
|
If you want to load an initial state...
|
||||||
|
|
||||||
|
* Set dip switch 4 off ('load mode').
|
||||||
|
* Toggle dip switches 2 and/or 3 on and off seven times total, to shift in 0 and 1 values to the automaton internal state.
|
||||||
|
* Set dip switch 4 on and run manually or automatically as above.
|
||||||
|
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | clock | 7segmentA |
|
||||||
|
| 1 | load0 | 7segmentB |
|
||||||
|
| 2 | load1 | 7segmentC |
|
||||||
|
| 3 | runmode | 7segmentD |
|
||||||
|
| 4 | clockdiv8 | 7segmentE |
|
||||||
|
| 5 | clockdiv4 | 7segmentF |
|
||||||
|
| 6 | clockdiv2 | 7segmentG |
|
||||||
|
| 7 | unused | 7segmentDP |
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
## 3 : Another Piece of Pi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* Author: Meinhard Kissich, EAS Group, Graz University of Technology
|
||||||
|
* Description: This design takes up the idea of James Ross [1], who submitted a circuit to Tiny Tapeout 02 that stores and outputs the first 1024 decimal digits of the number Pi (including the decimal point) to a 7-segment display. In contrast to his approach, a densely packed decimal encoding is used to store the data. With this approach, 1400 digits can be stored and output within the design area of 150um x 170um. However, at 1400 decimals and utilization of 38.99%, the limitation seems to be routing. Like James, I'm also interested to hear about better strategies to fit more information into the design with synthesizable Verilog code. [1] https://github.com/jar/tt02_freespeech
|
||||||
|
* [GitHub repository](https://github.com/meiniKi/tt03-another-piece-of-pi)
|
||||||
|
* HDL project
|
||||||
|
* [Extra docs]()
|
||||||
|
* Clock: 0 Hz
|
||||||
|
* External hardware: 7-segment display
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
The circuit stores each triplet of decimals in a 10-bit vector encoded as densely packed decimals. An index vector selects the current digits to be output to the 7-segment display. It consists of an upper part `index[11:2]` that selects the triplet and a lower part `index[1:0]` that specifies the digit within the triplet. First, the upper part decides on the triplet, which is then decoded into three decimals. Afterwards, the lower part selects one of the three decimals to be decoded into 7-segment display logic and applied to the outputs. The index is incremented at each primary clock edge. However, when the lower part equals three, i.e., `index[1:0]==1'b10`, two is added, as the triplet consists of three (not four) digits.
|
||||||
|
|
||||||
|
- `index == 'b0000000000|00`: triplet[0], digit 0 within triplet
|
||||||
|
- `index == 'b0000000000|01`: triplet[0], digit 1 within triplet
|
||||||
|
- `index == 'b0000000000|10`: triplet[0], digit 2 within triplet
|
||||||
|
- `index == 'b0000000001|00`: triplet[1], digit 0 within triplet
|
||||||
|
- `index == 'b0000000001|01`: triplet[1], digit 1 within triplet
|
||||||
|
- `index == 'b0000000001|10`: triplet[1], digit 2 within triplet
|
||||||
|
|
||||||
|
There is one exception to the rule above: the decimal point. Another multiplexer at the input of the 7-segment decoder can either forward a digit from the decoded tripled or a constant -- the decimal point. Once the lower part of the index counter, i.e., `index[1:0]` reaches `2'b10` for the first time, the multiplexer selects the decimal point and pauses incrementing the index for one clock cycle.
|
||||||
|
|
||||||
|
- `index == 'b0000000000|00`: triplet[0], digit 0 within triplet
|
||||||
|
- `index == 'b0000000000|01`: triplet[0], decimal point
|
||||||
|
- `index == 'b0000000000|01`: triplet[0], digit 1 within triplet
|
||||||
|
- `index == 'b0000000000|10`: triplet[0], digit 2 within triplet
|
||||||
|
- `index == 'b0000000001|00`: triplet[1], digit 0 within triplet
|
||||||
|
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
For simulation, please use the provided testbench and Makefile. It is important to run the `genmux.py` Python script first, as it generates the test vectors required by the Verilog testbench. For testing the physical chip, release the reset and compare the digits of Pi against a reference.
|
||||||
|
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | clk | segment a |
|
||||||
|
| 1 | reset | segment b |
|
||||||
|
| 2 | none | segment c |
|
||||||
|
| 3 | none | segment d |
|
||||||
|
| 4 | none | segment e |
|
||||||
|
| 5 | none | segment f |
|
||||||
|
| 6 | none | segment g |
|
||||||
|
| 7 | none | decimal LED |
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
## 4 : Wormy
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
* Author: nqbit
|
||||||
|
* Description: MC Wormy Pants squirms like a worm and grows just as fast.
|
||||||
|
* [GitHub repository](https://github.com/nqbit/wormy)
|
||||||
|
* HDL project
|
||||||
|
* [Extra docs](https://github.com/nqbit/wormy)
|
||||||
|
* Clock: 300 Hz
|
||||||
|
* External hardware:
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
Wormy is a very simple, addictive last person video game. This last person,
|
||||||
|
open-world game takes you down the path of an earthworm. Wormy's world is
|
||||||
|
made up of a 4x4 grid represented by 3x16-bit arrays: Direction[0],
|
||||||
|
Direction[1], and Occupied. The Direction[x] maps keep track of which way a
|
||||||
|
segment of worm moves, if it is on, and Occupied keeps track of if the grid
|
||||||
|
location is occupied.
|
||||||
|
|
||||||
|
Example Occupied Grid:
|
||||||
|
|
||||||
|
```
|
||||||
|
_________
|
||||||
|
| X X X X |
|
||||||
|
| X |
|
||||||
|
| X |
|
||||||
|
|_________|
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition to direction and occupied there are also pointers to the head
|
||||||
|
and the tail. The same grid would look something like the following:
|
||||||
|
|
||||||
|
Example Occupied Grid with head(H) and tail(T) highlighted:
|
||||||
|
|
||||||
|
```
|
||||||
|
_________
|
||||||
|
| T X X X |
|
||||||
|
| X |
|
||||||
|
| H |
|
||||||
|
|_________|
|
||||||
|
```
|
||||||
|
|
||||||
|
BOOM! Wormy shouldn't run into itself. If its head hits any part of its
|
||||||
|
body, that causes a collision. There is a collision if the location of the
|
||||||
|
Wormy's head is occupied by another segment of the Wormy. To determine
|
||||||
|
this, we keep track of the worm location, and specifically the current and
|
||||||
|
future locations of the worm head (H) and tail (T). If the future location
|
||||||
|
of H will occupy a location that will already be occupied, this causes a
|
||||||
|
collision.
|
||||||
|
|
||||||
|
This is made a bit trickier with growth (see below), because if the future
|
||||||
|
state of H is set to occupy the current state of T - there is only a
|
||||||
|
collision if growth is set to occur in the next cycle, so be careful if you
|
||||||
|
plan to have Wormy chase its tail! SPLAT!
|
||||||
|
|
||||||
|
NOM NOM! Wormy eats the tasty earth around it and grows. Every so often
|
||||||
|
Wormy, after having eaten all of its lunch, grows a whole segment. During a
|
||||||
|
growth cycle, the state of T simply persists (remains occupied and heading
|
||||||
|
in the same direction as it is).
|
||||||
|
|
||||||
|
Last Person Input - see user_input.v
|
||||||
|
To move Wormy along, the last player needs to push buttons to help Wormy
|
||||||
|
find more tasty earth: up, right, down or left.
|
||||||
|
|
||||||
|
Buttons are nasty little bugs in general. When pushed the button generates
|
||||||
|
an analog signal that might not look exactly like a single rising edge.
|
||||||
|
|
||||||
|
It might look something like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
xxxxxxxxxxxxxxxxx
|
||||||
|
xx
|
||||||
|
x
|
||||||
|
x
|
||||||
|
x x
|
||||||
|
x x
|
||||||
|
x x
|
||||||
|
x x
|
||||||
|
x xx x x
|
||||||
|
x xxxx x
|
||||||
|
x x xx x
|
||||||
|
x x x x
|
||||||
|
x x x x
|
||||||
|
xx x x x
|
||||||
|
xxx x x x
|
||||||
|
x xx x x x
|
||||||
|
x x x x x
|
||||||
|
x x x xx x
|
||||||
|
x xx x x
|
||||||
|
xxxxxxxxxxxxxxxxxxx x
|
||||||
|
```
|
||||||
|
|
||||||
|
In order to help protect our logic from this scary looking signal that
|
||||||
|
might introduce metastability (<- WHAT?), we can filter it with a couple
|
||||||
|
flippy-floppies and keep the metastability at bay. ARGH.
|
||||||
|
|
||||||
|
Once we have a clear pushed or not-pushed, we can suggest that Wormy move
|
||||||
|
in a specific direction. If the last player tries button mashing, Wormy
|
||||||
|
won't listen. Once a second Wormy checks what the last button press was and
|
||||||
|
tries really hard to go that way (see BOOM!).
|
||||||
|
|
||||||
|
Earthworms don't have eyes - see multiplexer.v
|
||||||
|
The game's display is made up of a 4x4 grid of LEDs controlled by a
|
||||||
|
multiplexer. Why multiplexing? With a multiplexed LED setup, we can control
|
||||||
|
more display units (LEDs), with a limited number of outputs (8 on this
|
||||||
|
TinyTapeout project).
|
||||||
|
|
||||||
|
To get multiplexing working the network of outputs is mapped to each
|
||||||
|
display unit. This allows us to manipulate assigned outputs to control the
|
||||||
|
state of each display unit, one at a time. We then cycle through each
|
||||||
|
display unit quickly enough to display a persistent image to the last
|
||||||
|
player.
|
||||||
|
|
||||||
|
Wires (A1-4, B1-4) map to each location on the game arena (4x4 grid):
|
||||||
|
|
||||||
|
```
|
||||||
|
A1| | | |
|
||||||
|
__________________
|
||||||
|
A2| | | |
|
||||||
|
__________________
|
||||||
|
A3| | | |
|
||||||
|
__________________
|
||||||
|
A4| | | |
|
||||||
|
__________________
|
||||||
|
|B1 |B2 |B3 |B4
|
||||||
|
```
|
||||||
|
|
||||||
|
When B's voltage is OFF the LED's state changes to ON if A is also ON.
|
||||||
|
- ON LED state: A(ON) — >| — B(OFF)
|
||||||
|
- OFF LED state: A(ON) — >| — B(ON)
|
||||||
|
|
||||||
|
Example: The 3 filled squares below each represent a Wormy segment in the
|
||||||
|
ON state as controlled by the multiplexer. Notice how each is lighter than
|
||||||
|
the last. This is because the multiplexer cycles through each LED to update
|
||||||
|
the state, creating one persistent image even though the LEDS are not on
|
||||||
|
over the entire period of time.
|
||||||
|
|
||||||
|
```
|
||||||
|
A1| | O | |
|
||||||
|
__________________
|
||||||
|
A2| | o | |
|
||||||
|
__________________
|
||||||
|
A3| | . | |
|
||||||
|
__________________
|
||||||
|
A4| | | |
|
||||||
|
__________________
|
||||||
|
|B1 |B2 |B3 |B4
|
||||||
|
```
|
||||||
|
|
||||||
|
Another Example: If you enter a dark cave and point a flashlight straight
|
||||||
|
ahead at one point on the wall you have a very small visual field that is
|
||||||
|
contained within the beam of light. However, you can expand your visual
|
||||||
|
field in the cave by waving the flashlight back and forth across the wall.
|
||||||
|
Despite the fact that the beam is moving over individual points on the
|
||||||
|
wall, the entire wall can be seen at once. This is similar to the concept
|
||||||
|
used in the Wormy display, since the multiplexer changes the state of the
|
||||||
|
worm occupied locations to ON one at a time, but in a cycle. The result is
|
||||||
|
a solid image, made up of LEDs cycling through ON states to produce a
|
||||||
|
persistent image of Wormy (that beautiful Lumbricina).
|
||||||
|
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
After reset, you should see a single pixel moving along the display and
|
||||||
|
it should grow every now and then.
|
||||||
|
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | clock | A0 - Multiplexer channel A to be tied to a an array of 16 multiplexed LEDs |
|
||||||
|
| 1 | reset | A1 |
|
||||||
|
| 2 | button0 | A2 |
|
||||||
|
| 3 | button1 | A3 |
|
||||||
|
| 4 | button2 | B0 - Multiplexer channel B to be tied to a an array of 16 multiplexed LEDs |
|
||||||
|
| 5 | button3 | B1 |
|
||||||
|
| 6 | none | B2 |
|
||||||
|
| 7 | none | B3 |
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
## 5 : Knight Rider Sensor Lights
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
* Author: Kolos Koblasz
|
||||||
|
* Description: The logic assertes output bits one by one, like KITT's sensor lights in Knight Rider.
|
||||||
|
* [GitHub repository](https://github.com/KolosKoblasz/tt03-knight_rider)
|
||||||
|
* HDL project
|
||||||
|
* [Extra docs]()
|
||||||
|
* Clock: 6000 Hz
|
||||||
|
* External hardware: Conect LEDs with ~1K-10K Ohm serial resistors to output pins and connect push button switches to Input[2] and Input[3] which drive the inputs with logic zeros when idle and with logic 1 when pressed. Rising edge on these inputs selects the next settings.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
Uses several counters, shiftregisters to create a moving light.
|
||||||
|
Input[2] and Input[3] can control speed and brightness respectively.
|
||||||
|
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
After reset it starts moving the switched on LED.
|
||||||
|
By creating rising edges on Input[2] and Input[3] the two config spaces can be discovered. Input[0] is clk Input[1] is reset (1=reset on, 0=Reset off).
|
||||||
|
Simulated with 6KHz clock signal.
|
||||||
|
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | clock | LED 0 |
|
||||||
|
| 1 | reset | LED 1 |
|
||||||
|
| 2 | speed control | LED 2 |
|
||||||
|
| 3 | brightness control | LED 3 |
|
||||||
|
| 4 | none | LED 4 |
|
||||||
|
| 5 | none | LED 5 |
|
||||||
|
| 6 | none | LED 6 |
|
||||||
|
| 7 | none | LED 7 |
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
# Technical info
|
||||||
|
|
||||||
|
## Scan chain
|
||||||
|
|
||||||
|
All 250 designs are joined together in a long chain similiar to JTAG. We provide the inputs and outputs of that chain (see pinout below) externally, to the Caravel logic analyser, and to an internal scan chain driver.
|
||||||
|
|
||||||
|
The default is to use an external driver, this is in case anything goes wrong with the Caravel logic analyser or the internal driver.
|
||||||
|
|
||||||
|
The scan chain is identical for each little project, and you can [read it here](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/main/template/scan_wrapper.v).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Updating inputs and outputs of a specified design
|
||||||
|
|
||||||
|
A good way to see how this works is to read the FSM in the [scan controller](verilog/rtl/scan_controller/scan_controller.v).
|
||||||
|
You can also run one of the simple tests and check the waveforms. See how in the [scan chain verification](verification.md) doc.
|
||||||
|
|
||||||
|
* Signal names are from the perspective of the scan chain driver.
|
||||||
|
* The desired project shall be called DUT (design under test)
|
||||||
|
|
||||||
|
Assuming you want to update DUT at position 2 (0 indexed) with inputs = 0x02 and then fetch the output.
|
||||||
|
This design connects an inverter between each input and output.
|
||||||
|
|
||||||
|
* Set scan_select low so that the data is clocked into the scan flops (rather than from the design)
|
||||||
|
* For the next 8 clocks, set scan_data_out to 0, 0, 0, 0, 0, 0, 1, 0
|
||||||
|
* Toggle scan_clk_out 16 times to deliver the data to the DUT
|
||||||
|
* Toggle scan_latch_en to deliver the data from the scan chain to the DUT
|
||||||
|
* Set scan_select high to set the scan flop's input to be from the DUT
|
||||||
|
* Toggle the scan_clk_out to capture the DUT's data into the scan chain
|
||||||
|
* Toggle the scan_clk_out another 8 x number of remaining designs to receive the data at scan_data_in
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Notes on understanding the trace*
|
||||||
|
|
||||||
|
* There are large wait times between the latch and scan signals to ensure no hold violations across the whole chain. For the internal scan controller, these can be configured (see section on wait states below).
|
||||||
|
* The input looks wrong (0x03) because the input is incremented by the test bench as soon as the scan controller captures the data. The input is actually 0x02.
|
||||||
|
* The output in the trace looks wrong (0xFE) because it's updated after a full refresh, the output is 0xFD.
|
||||||
|
|
||||||
|
## Clocking
|
||||||
|
|
||||||
|
Assuming:
|
||||||
|
|
||||||
|
* 100MHz input clock
|
||||||
|
* 8 ins & 8 outs
|
||||||
|
* 2 clock cycles to push one bit through the scan chain (scan clock is half input clock rate)
|
||||||
|
* 250 designs
|
||||||
|
* scan controller can do a read/write cycle in one refresh
|
||||||
|
|
||||||
|
So the max refresh rate is 100MHz / (8 * 2 * 250) = 25000Hz.
|
||||||
|
|
||||||
|
## Clock divider
|
||||||
|
|
||||||
|
A rising edge on the set_clk_div input will capture what is set on the input pins and use this as a divider for an internal slow clock that can be provided to the first input bit.
|
||||||
|
|
||||||
|
The slow clock is only enabled if the set_clk_div is set, and the resulting clock is connected to input0 and also output on the slow_clk pin.
|
||||||
|
|
||||||
|
The slow clock is synced with the scan rate. A divider of 0 mean it toggles the input0 every scan. Divider of 1 toggles it every 2 cycles.
|
||||||
|
So the resultant slow clock frequency is scan_rate / (2 * (N+1)).
|
||||||
|
|
||||||
|
See the test_clock_div test in the [scan chain verification](verification.md).
|
||||||
|
|
||||||
|
## Wait states
|
||||||
|
|
||||||
|
This dictates how many wait cycle we insert in various state
|
||||||
|
of the load process. We have a sane default, but also allow
|
||||||
|
override externally.
|
||||||
|
|
||||||
|
To override, set the wait amount on the inputs, set the driver_sel inputs both high, and then reset the chip.
|
||||||
|
|
||||||
|
See the test_wait_state test in the [scan chain verification](verification.md).
|
||||||
|
|
||||||
|
## Pinout
|
||||||
|
|
||||||
|
PIN NAME DESCRIPTION
|
||||||
|
20:12 active_select 9 bit input to set which design is active
|
||||||
|
28:21 inputs 8 inputs
|
||||||
|
36:29 outputs 8 outputs
|
||||||
|
37 ready goes high for one cycle everytime the scanchain is refreshed
|
||||||
|
10 slow_clk slow clock from internal clock divider
|
||||||
|
11 set_clk_div enable clock divider
|
||||||
|
9:8 driver_sel which scan chain driver: 00 = external, 01 = logic analyzer, 1x = internal
|
||||||
|
|
||||||
|
21 ext_scan_clk_out for external driver, clk input
|
||||||
|
22 ext_scan_data_out data input
|
||||||
|
23 ext_scan_select scan select
|
||||||
|
24 ext_scan_latch_en latch
|
||||||
|
29 ext_scan_clk_in clk output from end of chain
|
||||||
|
30 ext_scan_data_in data output from end of chain
|
||||||
|
|
||||||
|
## Instructions to build GDS
|
||||||
|
|
||||||
|
To run the tool locally or have a fork's GitHub action work, you need the GH_USERNAME and GH_TOKEN set in your environment.
|
||||||
|
|
||||||
|
GH_USERNAME should be set to your GitHub username.
|
||||||
|
|
||||||
|
To generate your GH_TOKEN go to https://github.com/settings/tokens/new . Set the checkboxes for repo and workflow.
|
||||||
|
|
||||||
|
To run locally, make a file like this:
|
||||||
|
|
||||||
|
export GH_USERNAME=<username>
|
||||||
|
export GH_TOKEN=<token>
|
||||||
|
|
||||||
|
And then source it before running the tool.
|
||||||
|
|
||||||
|
### Fetch all the projects
|
||||||
|
|
||||||
|
This goes through all the projects in project_urls.py, and fetches the latest artifact zip from GitHub. It takes the verilog, the GL verilog, and the GDS and copies
|
||||||
|
them to the correct place.
|
||||||
|
|
||||||
|
./configure.py --clone-all --fetch-gds
|
||||||
|
|
||||||
|
### Configure Caravel
|
||||||
|
|
||||||
|
Caravel needs the list of macros, how power is connected, instantiation of all the projects etc. This command builds these configs and also makes the README.md index.
|
||||||
|
|
||||||
|
./configure.py --update-caravel
|
||||||
|
|
||||||
|
### Build the GDS
|
||||||
|
|
||||||
|
To build the GDS and run the simulations, you will need to install the Sky130 PDK and OpenLane tool.
|
||||||
|
It takes about 5 minutes and needs about 3GB of disk space.
|
||||||
|
|
||||||
|
export PDK_ROOT=<some dir>/pdk
|
||||||
|
export OPENLANE_ROOT=<some dir>/openlane
|
||||||
|
cd <the root of this repo>
|
||||||
|
make setup
|
||||||
|
|
||||||
|
Then to create the GDS:
|
||||||
|
|
||||||
|
make user_project_wrapper
|
||||||
|
|
||||||
|
## Changing macro block size
|
||||||
|
|
||||||
|
After working out what size you want:
|
||||||
|
|
||||||
|
* adjust configure.py in `CaravelConfig.create_macro_config()`.
|
||||||
|
* adjust the PDN spacing to match in openlane/user_project_wrapper/config.tcl:
|
||||||
|
* ```set ::env(FP_PDN_HPITCH)```
|
||||||
|
* ```set ::env(FP_PDN_HOFFSET)```
|
||||||
|
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
# Verification
|
||||||
|
|
||||||
|
We are not trying to verify every single design. That is up to the person who makes it. What we want is to ensure that every design is accessible, even if some designs are broken.
|
||||||
|
|
||||||
|
We can split the verification effort into functional testing (simulation), static tests (formal verification), timing tests (STA) and physical tests (LVS & DRC).
|
||||||
|
|
||||||
|
See the sections below for details on each type of verification.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
You will need the GitHub tokens setup as described in [INFO](INFO.md#instructions-to-build-gds).
|
||||||
|
|
||||||
|
The default of 250 projects takes a very long time to simulate, so I advise overriding the configuration:
|
||||||
|
|
||||||
|
# fetch the test projects
|
||||||
|
./configure.py --test --clone-all
|
||||||
|
# rebuild config with only 20 projects
|
||||||
|
./configure.py --test --update-caravel --limit 20
|
||||||
|
|
||||||
|
You will also need iVerilog & cocotb. The easist way to install these are to download and install the [oss-cad-suite](https://github.com/YosysHQ/oss-cad-suite-build).
|
||||||
|
|
||||||
|
## Simulations
|
||||||
|
|
||||||
|
* Simulation of some test projects at RTL and GL level.
|
||||||
|
* Simulation of the whole chip with scan controller, external controller, logic analyser.
|
||||||
|
* Check wait state setting.
|
||||||
|
* Check clock divider setting.
|
||||||
|
|
||||||
|
### Scan controller
|
||||||
|
|
||||||
|
This test only instantiates user_project_wrapper (which contains all the small projects). It doesn't simulate the rest of the ASIC.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller
|
||||||
|
make test_scan_controller
|
||||||
|
|
||||||
|
The Gate Level simulation requires scan_controller and user_project_wrapper to be re-hardened to get the correct gate level netlists:
|
||||||
|
|
||||||
|
* Edit openlane/scan_controller/config.tcl and change NUM_DESIGNS=250 to NUM_DESIGNS=20.
|
||||||
|
* Then from the top level directory:
|
||||||
|
|
||||||
|
|
||||||
|
make scan_controller
|
||||||
|
make user_project_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
* Then run the GL test
|
||||||
|
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller
|
||||||
|
make test_scan_controller_gl
|
||||||
|
|
||||||
|
|
||||||
|
#### single
|
||||||
|
|
||||||
|
Just check one inverter module. Mainly for easy understanding of the traces.
|
||||||
|
|
||||||
|
make test_single
|
||||||
|
|
||||||
|
#### custom wait state
|
||||||
|
|
||||||
|
Just check one inverter module. Set a custom wait state value.
|
||||||
|
|
||||||
|
make test_wait_state
|
||||||
|
|
||||||
|
#### clock divider
|
||||||
|
|
||||||
|
Test one inverter module with an automatically generated clock on input 0. Sets the clock rate to 1/2 of the scan refresh rate.
|
||||||
|
|
||||||
|
make test_clock_div
|
||||||
|
|
||||||
|
## Top level tests setup
|
||||||
|
|
||||||
|
For all the top level tests, you will also need a [RISCV compiler to build the firmware](https://static.dev.sifive.com/dev-tools/riscv64-unknown-elf-gcc-8.3.0-2020.04.1-x86_64-linux-ubuntu14.tar.gz).
|
||||||
|
|
||||||
|
You will also need to install the 'management core' for the Caravel ASIC submission wrapper. This is done automatically by following the [PDK install instructions](INFO.md#build-the-gds).
|
||||||
|
|
||||||
|
### Top level test: internal control
|
||||||
|
|
||||||
|
Uses the scan controller, instantiated inside the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_int
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
### Top level test: external control
|
||||||
|
|
||||||
|
Uses external signals to control the scan chain. Simulates the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_ext
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
### Top level test: logic analyser control
|
||||||
|
|
||||||
|
Uses the RISCV co-processor to drive the scanchain with firmware. Simulates the whole chip.
|
||||||
|
|
||||||
|
cd verilog/dv/scan_controller_la
|
||||||
|
make coco_test
|
||||||
|
|
||||||
|
## Formal Verification
|
||||||
|
|
||||||
|
* Formal verification that each small project's scan chain is correct.
|
||||||
|
* Formal verification that the correct signals are passed through for the 3 different scan chain control modes.
|
||||||
|
|
||||||
|
### Scan chain
|
||||||
|
|
||||||
|
Each GL netlist for each small project is proven to be equivalent to the reference scan chain implementation.
|
||||||
|
The verification is done on the GL netlist, so an RTL version of the cells used needed to be created.
|
||||||
|
See [here for more info](tinytapeout_scan/README.md).
|
||||||
|
|
||||||
|
### Scan controller MUX
|
||||||
|
|
||||||
|
In case the internal scan controller doesn't work, we also have ability to control the chain from external pins or the Caravel Logic Analyser.
|
||||||
|
We implement a simple MUX to achieve this and [formally prove it is correct](verilog/rtl/scan_controller/properties.v).
|
||||||
|
|
||||||
|
## Timing constraints
|
||||||
|
|
||||||
|
Due to limitations in OpenLane - a top level timing analyis is not possible. This would allow us to detect setup and hold violations in the scan chain.
|
||||||
|
|
||||||
|
Instead, we design the chain and the timing constraints for each project and the scan controller with this in mind.
|
||||||
|
|
||||||
|
* [Each small project has a negedge flop flop at the end of the shift register to reclock the data](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/17f106db36f022536d013b960316bcc7f02c572c/template/scan_wrapper.v#L67). This gives more hold margin.
|
||||||
|
* [Each small project has SDC timing constraints](https://github.com/mattvenn/wokwi-verilog-gds-test/blob/main/src/base.sdc)
|
||||||
|
* [Scan controller](https://github.com/mattvenn/tinytapeout-mpw7/blob/aacae16304f4a4878943a49fd479d8a284736e32/verilog/rtl/scan_controller/scan_controller.v#L334) uses a shift register clocked with the end of the chain to ensure correct data is captured.
|
||||||
|
* [Scan controller has its own SDC timing constraints](openlane/scan_controller/base.sdc)
|
||||||
|
* Scan controller can be configured to wait for a programmable time at latching data into the design and capturing it from the design.
|
||||||
|
* External pins (by default) control the scan chain.
|
||||||
|
|
||||||
|
## Physical tests
|
||||||
|
|
||||||
|
* LVS
|
||||||
|
* DRC
|
||||||
|
* CVC
|
||||||
|
|
||||||
|
### LVS
|
||||||
|
|
||||||
|
Each project is built with OpenLane, which will check LVS for each small project.
|
||||||
|
Then when we combine all the projects together we run a top level LVS & DRC for routing, power supply and macro placement.
|
||||||
|
|
||||||
|
The extracted netlist from the GDS is what is used in the formal scan chain proof.
|
||||||
|
|
||||||
|
### DRC
|
||||||
|
|
||||||
|
DRC is checked by OpenLane for each small project, and then again at the top level when we combine all the projects.
|
||||||
|
|
||||||
|
### CVC
|
||||||
|
|
||||||
|
Mitch Bailey' CVC checker is a device level static verification system for quickly and easily detecting common circuit errors in CDL (Circuit Definition Language) netlists. We ran the test on the final design and found no errors.
|
||||||
|
|
||||||
|
* See [the paper here](https://woset-workshop.github.io/PDFs/2020/a05-slides.pdf).
|
||||||
|
* Github repo for the tool: https://github.com/d-m-bailey/cvc
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
# Sponsored by
|
||||||
|
|
||||||
|
[](https://efabless.com/)
|
||||||
|
|
||||||
|
# Team
|
||||||
|
|
||||||
|
Tiny Tapeout would not be possible without a lot of people helping. We would especially like to thank:
|
||||||
|
|
||||||
|
* Uri Shaked for [wokwi](https://wokwi.com/) development and lots more
|
||||||
|
* [Sylvain Munaut](https://twitter.com/tnt) for help with scan chain improvements
|
||||||
|
* [Mike Thompson](https://www.linkedin.com/in/michael-thompson-0a581a/) for verification expertise
|
||||||
|
* [Jix](https://twitter.com/jix_) for formal verification support
|
||||||
|
* [Proppy](https://twitter.com/proppy) for help with GitHub actions
|
||||||
|
* [Maximo Balestrini](https://twitter.com/maxiborga) for all the amazing renders and the interactive GDS viewer
|
||||||
|
* James Rosenthal for coming up with digital design examples
|
||||||
|
* All the people who took part in [TinyTapeout 01](/runs/tt01) and volunteered time to improve docs and test the flow
|
||||||
|
* The team at [YosysHQ](https://www.yosyshq.com/) and all the other open source EDA tool makers
|
||||||
|
* [Efabless](https://efabless.com/) for running the shuttles and providing OpenLane and sponsorship
|
||||||
|
* [Tim Ansell and Google](https://www.youtube.com/watch?v=EczW2IWdnOM) for supporting the open source silicon movement
|
||||||
|
* [Zero to ASIC course](https://zerotoasiccourse.com/) community for all your support
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: |
|
||||||
|
{width=15cm}
|
||||||
|
Tiny Tapeout 02 Datasheet
|
||||||
|
subtitle: Project Repository [https://github.com/TinyTapeout/tinytapeout-02](https://github.com/TinyTapeout/tinytapeout-02)
|
||||||
|
documentclass: scrartcl
|
||||||
|
date: \today{}
|
||||||
|
geometry: margin=2cm
|
||||||
|
fontsize: 14pt
|
||||||
|
mainfont: Latin Modern Sans
|
||||||
|
header-includes:
|
||||||
|
- \hypersetup{colorlinks=false,
|
||||||
|
allbordercolors={0 0 0},
|
||||||
|
pdfborderstyle={/S/U/W 1}}
|
||||||
|
---
|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
|
||||||
|
# Render of whole chip
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
\pagebreak
|
||||||
|
|
||||||
|
# Projects
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## {index} : {title}
|
||||||
|
|
||||||
|
{picture_link}
|
||||||
|
|
||||||
|
* Author: {author}
|
||||||
|
* Description: {description}
|
||||||
|
* [GitHub repository]({git_url})
|
||||||
|
* {project_type} project
|
||||||
|
* [Extra docs]({doc_link})
|
||||||
|
* Clock: {clock_hz} Hz
|
||||||
|
* External hardware: {external_hw}
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
{how_it_works}
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
{how_to_test}
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | {inputs[0]} | {outputs[0]} |
|
||||||
|
| 1 | {inputs[1]} | {outputs[1]} |
|
||||||
|
| 2 | {inputs[2]} | {outputs[2]} |
|
||||||
|
| 3 | {inputs[3]} | {outputs[3]} |
|
||||||
|
| 4 | {inputs[4]} | {outputs[4]} |
|
||||||
|
| 5 | {inputs[5]} | {outputs[5]} |
|
||||||
|
| 6 | {inputs[6]} | {outputs[6]} |
|
||||||
|
| 7 | {inputs[7]} | {outputs[7]} |
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# To rebuild all gds
|
||||||
|
|
||||||
|
Build notes are also in the tt-02-build of discord
|
||||||
|
|
||||||
|
On git tag tapeout
|
||||||
|
|
||||||
|
run
|
||||||
|
|
||||||
|
. setuptokens.sh
|
||||||
|
./configure.py --harden
|
||||||
|
|
||||||
|
Some projects will fail with golden config. Change this line in golden config
|
||||||
|
|
||||||
|
set ::env(PL_BASIC_PLACEMENT) {1}
|
||||||
|
|
||||||
|
Then reharden the fails:
|
||||||
|
|
||||||
|
for i in 000 027 047 062 064 081 087 092 094 117 118 126 129 140 141 146 153 156 163 165 ; do
|
||||||
|
echo $i
|
||||||
|
./configure.py --single $i --harden
|
||||||
|
done
|
||||||
|
|
||||||
|
Project 106 only hardens with the config.tcl as provided
|
||||||
|
|
||||||
|
After rehardening, this should return empty:
|
||||||
|
|
||||||
|
grep 'Flow failed' projects/*/runs/wokwi/errors.log
|
||||||
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
title: "Tiny Tapeout 02"
|
||||||
|
weight: 10
|
||||||
|
description: Overview
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{{% toc %}}
|
||||||
|
|
||||||
|
# Design details
|
||||||
|
|
||||||
|
* [Github repository](https://github.com/TinyTapeout/tinytapeout-02)
|
||||||
|
* [PDF datasheet](https://github.com/TinyTapeout/tinytapeout-02/raw/tt02/datasheet.pdf)
|
||||||
|
* [Verification](https://github.com/TinyTapeout/tinytapeout-02/blob/tt02/VERIFICATION.md)
|
||||||
|
* [Efabless MPW submission](https://platform.efabless.com/projects/1410)
|
||||||
|
|
||||||
|
# Launch stats
|
||||||
|
|
||||||
|
* Launched: 9 November 2023
|
||||||
|
* Submission closed: 2 December 2023
|
||||||
|
* Submitted to Efabless 2211Q chipIgnite shuttle using Skywater 130nm open source PDK
|
||||||
|
|
||||||
|
# Project statistics
|
||||||
|
|
||||||
|
* 165 projects submitted. Each project is 150 x 170um
|
||||||
|
* Youngest submitter: [4 years old](https://tinytapeout.com/runs/tt02/031)
|
||||||
|
* 64 used Wokwi graphical editor, 82 Verilog, 5 Amaranth, 1 Chisel
|
||||||
|
* 39935 standard cells used across all projects.
|
||||||
|
* Most cells used in a design was 1105, the least was 8.
|
||||||
|
* Max utilisation was 52%
|
||||||
|
* Total wire length 2024 mm.
|
||||||
|
|
||||||
|
## Whole die render
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
# Project Showcase
|
||||||
|
|
||||||
|
## Wokwi projects
|
||||||
|
* [UART-CC](https://tinytapeout.com/runs/tt02/057)
|
||||||
|
* [Nano-neuron](https://tinytapeout.com/runs/tt02/066)
|
||||||
|
* [Worm in a Maze](https://tinytapeout.com/runs/tt02/084)
|
||||||
|
* [Wolf sheep cabbage river crossing puzzle ASIC design](https://tinytapeout.com/runs/tt02/111)
|
||||||
|
|
||||||
|
## HDL projects
|
||||||
|
* [Avalon Semiconductors 'TBB1143' Programmable Sound Generator](https://tinytapeout.com/runs/tt02/024/)
|
||||||
|
* [Pi (π) to 1000+ decimal places](https://tinytapeout.com/runs/tt02/036)
|
||||||
|
* [FPGA test](https://tinytapeout.com/runs/tt02/090)
|
||||||
|
* [RV8U - 8-bit RISC-V Microcore Processor](https://tinytapeout.com/runs/tt02/107)
|
||||||
|
* [8-bit CPU](https://tinytapeout.com/runs/tt02/085/)
|
||||||
|
|
||||||
|
# Testimonials / Endorsements
|
||||||
|
|
||||||
|
An educational chip development workflow entirely in-browser,
|
||||||
|
from graphic entry to GDSII output for #sky130! Stunning work by
|
||||||
|
@matthewvenn to make an opaque area of technology accessible.
|
||||||
|
|
||||||
|
---
|
||||||
|
Last week I designed an integrated circuit for #tinytapeout, my first digital circuit design not to include any kind of cpu.
|
||||||
|
|
||||||
|
---
|
||||||
|
I still can't get over how smooth the ASIC design workflow of the #tinytapeout is
|
||||||
|
|
||||||
|
---
|
||||||
|
Thank you @matthewvenn and everyone who made #tinytapeout happen! I made a simple counter FSM based on a UTexas presentation (linked below), but I made it all on my new Steamdeck!
|
||||||
|
|
||||||
|
---
|
||||||
|
Created a 128-Bit Memory (8 x 16-Bit Blocks) custom ASIC. I hope it will be manufactured in next shuttle run.
|
||||||
|
Aside from that, It was amazing how seamless and easy was the whole process.
|
||||||
|
|
||||||
|
---
|
||||||
|
More on [Twitter](https://twitter.com/search?q=tinytapeout).
|
||||||
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
title: "{index} {title}"
|
||||||
|
weight: {weight}
|
||||||
|
---
|
||||||
|
|
||||||
|
## {index} : {title}
|
||||||
|
|
||||||
|
* Author: {author}
|
||||||
|
* Description: {description}
|
||||||
|
* [GitHub repository]({git_url})
|
||||||
|
* [Most recent GDS build]({git_action})
|
||||||
|
* {project_type} project
|
||||||
|
* [Extra docs]({doc_link})
|
||||||
|
* Clock: {clock_hz} Hz
|
||||||
|
* External hardware: {external_hw}
|
||||||
|
|
||||||
|
{picture_link}
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
{how_it_works}
|
||||||
|
|
||||||
|
### How to test
|
||||||
|
|
||||||
|
{how_to_test}
|
||||||
|
|
||||||
|
### IO
|
||||||
|
|
||||||
|
| # | Input | Output |
|
||||||
|
|---|--------------|--------------|
|
||||||
|
| 0 | {inputs[0]} | {outputs[0]} |
|
||||||
|
| 1 | {inputs[1]} | {outputs[1]} |
|
||||||
|
| 2 | {inputs[2]} | {outputs[2]} |
|
||||||
|
| 3 | {inputs[3]} | {outputs[3]} |
|
||||||
|
| 4 | {inputs[4]} | {outputs[4]} |
|
||||||
|
| 5 | {inputs[5]} | {outputs[5]} |
|
||||||
|
| 6 | {inputs[6]} | {outputs[6]} |
|
||||||
|
| 7 | {inputs[7]} | {outputs[7]} |
|
||||||
+135
-63
@@ -1,75 +1,147 @@
|
|||||||
import logging
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import os
|
|
||||||
import glob
|
|
||||||
import gdstk
|
|
||||||
import cairosvg
|
|
||||||
|
|
||||||
|
class Docs():
|
||||||
|
|
||||||
# doc check
|
def __init__(self, projects, args):
|
||||||
# makes sure that the basic info is present
|
self.projects = projects
|
||||||
def check_yaml_docs(yaml):
|
self.args = args
|
||||||
logging.info("checking docs")
|
|
||||||
for key in ['author', 'title', 'description', 'how_it_works', 'how_to_test', 'language', 'inputs', 'outputs']:
|
|
||||||
if key not in yaml['documentation']:
|
|
||||||
logging.error("missing key {} in documentation".format(key))
|
|
||||||
exit(1)
|
|
||||||
if yaml['documentation'][key] == "":
|
|
||||||
logging.error("missing value for {} in documentation".format(key))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# if provided, check discord handle is valid
|
# stuff related to docs
|
||||||
if len(yaml['documentation']['discord']):
|
def build_index(self):
|
||||||
parts = yaml['documentation']['discord'].split('#')
|
logging.info("building doc index")
|
||||||
if len(parts) != 2 or len(parts[0]) == 0 or not re.match('^[0-9]{4}$', parts[1]):
|
with open("README_init.md") as fh:
|
||||||
logging.error('Invalid format for discord username')
|
readme = fh.read()
|
||||||
exit(1)
|
with open("README.md", 'w') as fh:
|
||||||
|
fh.write(readme)
|
||||||
|
fh.write("| Index | Author | Title | Type | Git Repo |\n")
|
||||||
|
fh.write("| ----- | ------ | ------| -----| ---------|\n")
|
||||||
|
for project in self.projects:
|
||||||
|
if not project.is_fill():
|
||||||
|
fh.write(project.get_index_row())
|
||||||
|
|
||||||
|
def update_image(self):
|
||||||
|
cmd = "klayout -l caravel.lyp gds/user_project_wrapper.gds -r dump_pic.rb -c klayoutrc"
|
||||||
|
logging.info(cmd)
|
||||||
|
os.system(cmd)
|
||||||
|
|
||||||
# use pandoc to create a single page PDF preview
|
# create a json file of all the project info, this is then used by tinytapeout.com to show projects
|
||||||
def create_pdf(yaml):
|
def dump_json(self):
|
||||||
yaml = yaml['documentation']
|
designs = []
|
||||||
logging.info("creating pdf")
|
for project in self.projects:
|
||||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
design = project.get_yaml()
|
||||||
with open(os.path.join(script_dir, "docs/project_header.md")) as fh:
|
designs.append(design)
|
||||||
doc_header = fh.read()
|
|
||||||
with open(os.path.join(script_dir, "docs/project_preview.md")) as fh:
|
|
||||||
doc_template = fh.read()
|
|
||||||
|
|
||||||
with open('datasheet.md', 'w') as fh:
|
with open(args.dump_json, "w") as fh:
|
||||||
fh.write(doc_header)
|
fh.write(json.dumps(designs, indent=4))
|
||||||
# handle pictures
|
logging.info(f'wrote json to {args.dump_json}')
|
||||||
yaml['picture_link'] = ''
|
|
||||||
if yaml['picture']:
|
|
||||||
# skip SVG for now, not supported by pandoc
|
|
||||||
picture_name = yaml['picture']
|
|
||||||
if 'svg' not in picture_name:
|
|
||||||
yaml['picture_link'] = ''.format(picture_name)
|
|
||||||
else:
|
|
||||||
logging.warning("svg not supported")
|
|
||||||
|
|
||||||
# now build the doc & print it
|
def dump_markdown(self):
|
||||||
try:
|
|
||||||
doc = doc_template.format(**yaml)
|
with open("doc_header.md") as fh:
|
||||||
fh.write(doc)
|
doc_header = fh.read()
|
||||||
|
|
||||||
|
with open("doc_template.md") as fh:
|
||||||
|
doc_template = fh.read()
|
||||||
|
|
||||||
|
with open("INFO.md") as fh:
|
||||||
|
doc_info = fh.read()
|
||||||
|
|
||||||
|
with open("VERIFICATION.md") as fh:
|
||||||
|
doc_verification = fh.read()
|
||||||
|
|
||||||
|
with open("CREDITS.md") as fh:
|
||||||
|
doc_credits = fh.read()
|
||||||
|
|
||||||
|
with open(args.dump_markdown, 'w') as fh:
|
||||||
|
fh.write(doc_header)
|
||||||
|
|
||||||
|
for project in self.projects:
|
||||||
|
if project.is_fill():
|
||||||
|
continue
|
||||||
|
yaml_data = project.get_project_doc_yaml()
|
||||||
|
|
||||||
|
yaml_data['index'] = project.index
|
||||||
|
|
||||||
|
logging.info(f"building datasheet for {project}")
|
||||||
|
# handle pictures
|
||||||
|
yaml_data['picture_link'] = ''
|
||||||
|
if yaml_data['picture']:
|
||||||
|
# skip SVG for now, not supported by pandoc
|
||||||
|
picture_name = yaml_data['picture']
|
||||||
|
if 'svg' not in picture_name:
|
||||||
|
picture_filename = os.path.join(project.local_dir, picture_name)
|
||||||
|
yaml_data['picture_link'] = ''.format(picture_filename)
|
||||||
|
|
||||||
|
# now build the doc & print it
|
||||||
|
try:
|
||||||
|
doc = doc_template.format(**yaml_data)
|
||||||
|
fh.write(doc)
|
||||||
|
fh.write("\n\pagebreak\n")
|
||||||
|
except IndexError:
|
||||||
|
logging.warning("missing pins in info.yaml, skipping")
|
||||||
|
|
||||||
|
# ending
|
||||||
|
fh.write(doc_info)
|
||||||
fh.write("\n\pagebreak\n")
|
fh.write("\n\pagebreak\n")
|
||||||
except IndexError:
|
fh.write(doc_verification)
|
||||||
logging.warning("missing pins in info.yaml, skipping")
|
fh.write("\n\pagebreak\n")
|
||||||
|
fh.write(doc_credits)
|
||||||
|
|
||||||
pdf_cmd = 'pandoc --pdf-engine=xelatex -i datasheet.md -o datasheet.pdf'
|
logging.info(f'wrote markdown to {args.dump_markdown}')
|
||||||
logging.info(pdf_cmd)
|
|
||||||
p = subprocess.run(pdf_cmd, shell=True)
|
|
||||||
if p.returncode != 0:
|
|
||||||
logging.error("pdf command failed")
|
|
||||||
|
|
||||||
|
if args.dump_pdf:
|
||||||
|
pdf_cmd = f'pandoc --toc --toc-depth 2 --pdf-engine=xelatex -i {args.dump_markdown} -o {args.dump_pdf}'
|
||||||
|
logging.info(pdf_cmd)
|
||||||
|
p = subprocess.run(pdf_cmd, shell=True)
|
||||||
|
if p.returncode != 0:
|
||||||
|
logging.error("pdf command failed")
|
||||||
|
|
||||||
# SVG and PNG renders of the GDS
|
def build_hugo_content(self):
|
||||||
def create_svg(args):
|
hugo_root = args.build_hugo_content
|
||||||
gds = glob.glob(os.path.join(args.run_dir, 'results/final/gds/*gds'))
|
hugo_images = os.path.join(hugo_root, 'images')
|
||||||
library = gdstk.read_gds(gds[0])
|
shutil.rmtree(hugo_root)
|
||||||
top_cells = library.top_level()
|
os.makedirs(hugo_root)
|
||||||
top_cells[0].write_svg('gds_render.svg')
|
os.makedirs(hugo_images)
|
||||||
|
|
||||||
|
with open("hugo_template.md") as fh:
|
||||||
|
doc_template = fh.read()
|
||||||
|
|
||||||
|
with open("hugo_index_template.md") as fh:
|
||||||
|
index_template = fh.read()
|
||||||
|
|
||||||
|
# copy image
|
||||||
|
shutil.copyfile('tinytapeout.png', os.path.join(hugo_images, 'tinytapeout.png'))
|
||||||
|
|
||||||
|
# index page
|
||||||
|
logging.info("building pages - can take a minute as fetching latest GDS action URLs for all projects")
|
||||||
|
with open(os.path.join(hugo_root, '_index.md'), 'w') as fh:
|
||||||
|
fh.write(index_template)
|
||||||
|
fh.write('# All projects\n')
|
||||||
|
fh.write("| Index | Title | Author |\n")
|
||||||
|
fh.write("| ----- | ----- | -------|\n")
|
||||||
|
for project in self.projects:
|
||||||
|
logging.info(project)
|
||||||
|
if not project.is_fill():
|
||||||
|
fh.write(project.get_hugo_row())
|
||||||
|
|
||||||
|
project_dir = os.path.join(hugo_root, f'{project.get_index() :03}')
|
||||||
|
project_image_dir = os.path.join(project_dir, 'images')
|
||||||
|
os.makedirs(project_dir)
|
||||||
|
os.makedirs(project_image_dir)
|
||||||
|
yaml_data = project.get_project_doc_yaml()
|
||||||
|
yaml_data['index'] = project.index
|
||||||
|
yaml_data['weight'] = project.index + 1
|
||||||
|
yaml_data['git_action'] = project.get_latest_action_url()
|
||||||
|
yaml_data['picture_link'] = ''
|
||||||
|
if yaml_data['picture']:
|
||||||
|
picture_name = yaml_data['picture']
|
||||||
|
picture_filename = os.path.join(project.local_dir, picture_name)
|
||||||
|
picture_basename = os.path.basename(picture_filename)
|
||||||
|
try:
|
||||||
|
shutil.copyfile(picture_filename, os.path.join(project_image_dir, picture_basename))
|
||||||
|
yaml_data['picture_link'] = f''
|
||||||
|
except FileNotFoundError:
|
||||||
|
yaml_data['picture_link'] = 'Image path is broken'
|
||||||
|
doc = doc_template.format(**yaml_data)
|
||||||
|
with open(os.path.join(project_dir, '_index.md'), 'w') as pfh:
|
||||||
|
pfh.write(doc)
|
||||||
|
|
||||||
if args.create_png:
|
|
||||||
cairosvg.svg2png(url='gds_render.svg', write_to='gds_render.png')
|
|
||||||
|
|||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
import base64
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import zipfile, io
|
||||||
|
|
||||||
|
|
||||||
|
def unique(duplist):
|
||||||
|
unique_list = []
|
||||||
|
# traverse for all elements
|
||||||
|
for x in duplist:
|
||||||
|
# check if exists in unique_list or not
|
||||||
|
if x not in unique_list:
|
||||||
|
unique_list.append(x)
|
||||||
|
return unique_list
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_file(url, filename):
|
||||||
|
logging.info("trying to download {}".format(url))
|
||||||
|
r = requests.get(url)
|
||||||
|
if r.status_code != 200:
|
||||||
|
logging.warning("couldn't download {}".format(url))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
with open(filename, 'wb') as fh:
|
||||||
|
logging.info("written to {}".format(filename))
|
||||||
|
fh.write(r.content)
|
||||||
|
|
||||||
|
|
||||||
|
def check_status(r):
|
||||||
|
if r.status_code == 401:
|
||||||
|
logging.error("unauthorised, check INFO.md for information about GitHub API keys")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_file_from_git(git_url, path):
|
||||||
|
# get the basics
|
||||||
|
user_name, repo = split_git_url(git_url)
|
||||||
|
|
||||||
|
# authenticate for rate limiting
|
||||||
|
auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
|
||||||
|
encoded = base64.b64encode(auth_string.encode('ascii'))
|
||||||
|
headers = {
|
||||||
|
"authorization" : 'Basic ' + encoded.decode('ascii'),
|
||||||
|
"Accept" : "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
encoded = base64.b64encode(auth_string.encode('ascii'))
|
||||||
|
|
||||||
|
api_url = 'https://api.github.com/repos/%s/%s/contents/%s' % (user_name, repo, path)
|
||||||
|
|
||||||
|
logging.debug(api_url)
|
||||||
|
r = requests.get(api_url, headers=headers)
|
||||||
|
check_status(r)
|
||||||
|
requests_remaining = int(r.headers['X-RateLimit-Remaining'])
|
||||||
|
if requests_remaining == 0:
|
||||||
|
logging.error("no API requests remaining")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
logging.debug("API requests remaining %d" % requests_remaining)
|
||||||
|
|
||||||
|
data = r.json()
|
||||||
|
if 'content' not in data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
file_content = data['content']
|
||||||
|
|
||||||
|
file_content_encoding = data.get('encoding')
|
||||||
|
if file_content_encoding == 'base64':
|
||||||
|
file_content = base64.b64decode(file_content)
|
||||||
|
|
||||||
|
return file_content
|
||||||
|
|
||||||
|
|
||||||
|
# the latest artifact isn't necessarily the one related to the latest commit, as github
|
||||||
|
# could have taken longer to process an older commit than a newer one.
|
||||||
|
# so iterate through commits and return the artifact that matches
|
||||||
|
def get_most_recent_action_url(commits, artifacts):
|
||||||
|
release_sha_to_download_url = {artifact['workflow_run']['head_sha']: artifact['archive_download_url'] for artifact in artifacts}
|
||||||
|
for commit in commits:
|
||||||
|
if commit['sha'] in release_sha_to_download_url:
|
||||||
|
return release_sha_to_download_url[commit['sha']]
|
||||||
|
|
||||||
|
|
||||||
|
def get_most_recent_action_page(commits, runs):
|
||||||
|
release_sha_to_page_url = {run['head_sha']: run['html_url'] for run in runs if run['name'] == 'gds'}
|
||||||
|
for commit in commits:
|
||||||
|
if commit['sha'] in release_sha_to_page_url:
|
||||||
|
return release_sha_to_page_url[commit['sha']]
|
||||||
|
|
||||||
|
|
||||||
|
def split_git_url(url):
|
||||||
|
res = urlparse(url)
|
||||||
|
try:
|
||||||
|
_, user_name, repo = res.path.split('/')
|
||||||
|
except ValueError:
|
||||||
|
logging.error(f"couldn't split repo from {url}")
|
||||||
|
exit(1)
|
||||||
|
repo = repo.replace('.git', '')
|
||||||
|
return user_name, repo
|
||||||
|
|
||||||
|
|
||||||
|
# download the artifact for each project to get the gds & lef
|
||||||
|
def install_artifacts(url, directory):
|
||||||
|
logging.debug(url)
|
||||||
|
user_name, repo = split_git_url(url)
|
||||||
|
|
||||||
|
# authenticate for rate limiting
|
||||||
|
auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
|
||||||
|
encoded = base64.b64encode(auth_string.encode('ascii'))
|
||||||
|
headers = {
|
||||||
|
"authorization" : 'Basic ' + encoded.decode('ascii'),
|
||||||
|
"Accept" : "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# first fetch the git commit history
|
||||||
|
api_url = f'https://api.github.com/repos/{user_name}/{repo}/commits'
|
||||||
|
r = requests.get(api_url, headers=headers)
|
||||||
|
check_status(r)
|
||||||
|
requests_remaining = int(r.headers['X-RateLimit-Remaining'])
|
||||||
|
if requests_remaining == 0:
|
||||||
|
logging.error("no API requests remaining")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
commits = r.json()
|
||||||
|
|
||||||
|
# now get the artifacts
|
||||||
|
api_url = f'https://api.github.com/repos/{user_name}/{repo}/actions/artifacts'
|
||||||
|
r = requests.get(api_url, headers=headers, params={'per_page': 100})
|
||||||
|
check_status(r)
|
||||||
|
data = r.json()
|
||||||
|
|
||||||
|
# check there are some artifacts
|
||||||
|
if 'artifacts' not in data:
|
||||||
|
logging.error(f"no artifact found for {url}")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
# only get artifacts called GDS
|
||||||
|
artifacts = [a for a in data['artifacts'] if a['name'] == 'GDS']
|
||||||
|
logging.debug(f"found {len(artifacts)} artifacts")
|
||||||
|
|
||||||
|
if len(artifacts) == 0:
|
||||||
|
logging.error("no artifacts for this project")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
download_url = get_most_recent_action_url(commits, artifacts)
|
||||||
|
|
||||||
|
# need actions access on the token to get the artifact
|
||||||
|
# won't work on a pull request because they won't have the token
|
||||||
|
attempts = 1
|
||||||
|
max_attempts = 3
|
||||||
|
while attempts < max_attempts:
|
||||||
|
try:
|
||||||
|
logging.debug(f"download url {download_url} attempt {attempts}")
|
||||||
|
r = requests.get(download_url, headers=headers)
|
||||||
|
check_status(r)
|
||||||
|
z = zipfile.ZipFile(io.BytesIO(r.content))
|
||||||
|
z.extractall(directory)
|
||||||
|
break
|
||||||
|
except zipfile.BadZipFile:
|
||||||
|
attempts += 1
|
||||||
|
logging.warning(f"problem with zipfile, retry {attempts}")
|
||||||
|
|
||||||
|
if attempts == max_attempts:
|
||||||
|
logging.error("gave up downloading zipfile")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_latest_action_url(url, directory):
|
||||||
|
logging.debug(url)
|
||||||
|
user_name, repo = split_git_url(url)
|
||||||
|
|
||||||
|
# authenticate for rate limiting
|
||||||
|
auth_string = os.environ['GH_USERNAME'] + ':' + os.environ['GH_TOKEN']
|
||||||
|
encoded = base64.b64encode(auth_string.encode('ascii'))
|
||||||
|
headers = {
|
||||||
|
"authorization" : 'Basic ' + encoded.decode('ascii'),
|
||||||
|
"Accept" : "application/vnd.github+json",
|
||||||
|
}
|
||||||
|
|
||||||
|
# first fetch the git commit history
|
||||||
|
api_url = f'https://api.github.com/repos/{user_name}/{repo}/commits'
|
||||||
|
r = requests.get(api_url, headers=headers)
|
||||||
|
check_status(r)
|
||||||
|
requests_remaining = int(r.headers['X-RateLimit-Remaining'])
|
||||||
|
if requests_remaining == 0:
|
||||||
|
logging.error("no API requests remaining")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
commits = r.json()
|
||||||
|
|
||||||
|
# get runs
|
||||||
|
api_url = f'https://api.github.com/repos/{user_name}/{repo}/actions/runs'
|
||||||
|
r = requests.get(api_url, headers=headers, params={'per_page': 100})
|
||||||
|
check_status(r)
|
||||||
|
runs = r.json()
|
||||||
|
page_url = get_most_recent_action_page(commits, runs['workflow_runs'])
|
||||||
|
|
||||||
|
return page_url
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import stripe, os, json
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import OrderedDict
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
class Orders():
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.open_date = datetime.strptime(self.config['start_date'], '%Y-%m-%d')
|
||||||
|
stripe.api_key = os.environ['STRIPE_TOKEN']
|
||||||
|
self.orders = []
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_orders(self):
|
||||||
|
start_id = None
|
||||||
|
after_open_date = True
|
||||||
|
|
||||||
|
while after_open_date:
|
||||||
|
checkouts = stripe.checkout.Session.list(limit=10, starting_after=start_id)
|
||||||
|
|
||||||
|
for checkout in checkouts:
|
||||||
|
created = datetime.fromtimestamp(checkout['created'])
|
||||||
|
if created < self.open_date:
|
||||||
|
after_open_date = False
|
||||||
|
|
||||||
|
if checkout['payment_status'] == 'paid':
|
||||||
|
if 'github' in checkout['metadata']:
|
||||||
|
git_url = checkout['metadata']['github']
|
||||||
|
order = {
|
||||||
|
'git_url' : git_url,
|
||||||
|
'email' : checkout['customer_details']['email'],
|
||||||
|
}
|
||||||
|
self.orders.append(order)
|
||||||
|
|
||||||
|
# pagination
|
||||||
|
start_id = checkout['id']
|
||||||
|
|
||||||
|
# put in date order
|
||||||
|
self.orders.reverse()
|
||||||
|
return self.orders
|
||||||
|
|
||||||
|
|
||||||
|
def update_project_list(self):
|
||||||
|
project_list = {}
|
||||||
|
# first project is the filler test project
|
||||||
|
index = 0
|
||||||
|
project = { 'url': self.config['filler_project'], 'status': 'active', 'fill': False }
|
||||||
|
project_list[index] = project
|
||||||
|
|
||||||
|
# add all the real orders
|
||||||
|
for index, order in enumerate(self.orders):
|
||||||
|
project = { 'url': order['git_url'], 'status': 'active', 'fill': False }
|
||||||
|
project_list[index + 1] = project
|
||||||
|
|
||||||
|
# fill with the filler
|
||||||
|
for index in range(len(project_list), self.config['num_projects']):
|
||||||
|
project = { 'url': self.config['filler_project'], 'status': 'active', 'fill': True }
|
||||||
|
project_list[index] = project
|
||||||
|
|
||||||
|
with open('projects.yaml', 'w') as fh:
|
||||||
|
fh.write(yaml.dump(project_list, sort_keys=False))
|
||||||
|
|
||||||
+501
-69
@@ -3,77 +3,141 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import glob
|
||||||
|
import gdstk
|
||||||
|
import cairosvg
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
import git_utils
|
||||||
|
import git
|
||||||
|
|
||||||
|
# can't find a google version
|
||||||
|
CELL_URL = 'https://antmicro-skywater-pdk-docs.readthedocs.io/en/test-submodules-in-rtd/contents/libraries/sky130_fd_sc_ls/cells/'
|
||||||
|
|
||||||
|
|
||||||
def load_yaml(args):
|
class Project():
|
||||||
with open(args.yaml, "r") as stream:
|
|
||||||
return (yaml.safe_load(stream))
|
def __init__(self, index, git_url, local_dir, args, fill=False):
|
||||||
|
self.git_url = git_url
|
||||||
|
self.args = args
|
||||||
|
self.index = index
|
||||||
|
self.fill = fill
|
||||||
|
self.local_dir = local_dir
|
||||||
|
|
||||||
|
def post_clone_setup(self):
|
||||||
|
self.load_yaml()
|
||||||
|
self.setup_source_files()
|
||||||
|
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 print_wokwi_id(yaml):
|
def check_ports(self):
|
||||||
print(yaml['project']['wokwi_id'])
|
top = self.get_macro_name()
|
||||||
|
sources = [os.path.join(self.local_dir, 'src', src) for src in self.src_files]
|
||||||
|
source_list = " ".join(sources)
|
||||||
|
|
||||||
|
json_file = 'ports.json'
|
||||||
|
yosys_cmd = f"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']
|
||||||
|
for port in ['io_in', 'io_out']:
|
||||||
|
if port not in module_ports:
|
||||||
|
logging.error(f"{self} {port} not found in top")
|
||||||
|
exit(1)
|
||||||
|
if len(module_ports[port]['bits']) != 8:
|
||||||
|
logging.error(f"{self} {port} doesn't have 8 bits")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
def create_user_config(yaml):
|
def check_num_cells(self):
|
||||||
logging.info("creating include file")
|
num_cells = self.get_cell_count_from_gl()
|
||||||
sources = get_project_source(yaml)
|
if self.is_hdl():
|
||||||
top_module = get_top_module(yaml)
|
if num_cells < 20:
|
||||||
|
logging.warning(f"{self} only has {num_cells} cells")
|
||||||
if top_module == 'top':
|
else:
|
||||||
logging.error("top module cannot be called top - prepend your repo name to make it unique")
|
if num_cells < 11:
|
||||||
exit(1)
|
logging.warning(f"{self} only has {num_cells} cells")
|
||||||
|
|
||||||
filename = 'user_config.tcl'
|
|
||||||
with open(os.path.join('src', filename), 'w') as fh:
|
|
||||||
fh.write("set ::env(DESIGN_NAME) {}\n".format(top_module))
|
|
||||||
fh.write('set ::env(VERILOG_FILES) "\\\n')
|
|
||||||
for line, source in enumerate(sources):
|
|
||||||
fh.write(" $::env(DESIGN_DIR)/" + source)
|
|
||||||
if line != len(sources) - 1:
|
|
||||||
fh.write(' \\\n')
|
|
||||||
fh.write('"\n')
|
|
||||||
|
|
||||||
|
|
||||||
def fetch_file(url, filename):
|
def is_fill(self):
|
||||||
logging.info("trying to download {}".format(url))
|
return self.fill
|
||||||
r = requests.get(url)
|
|
||||||
if r.status_code != 200:
|
|
||||||
logging.warning("couldn't download {}".format(url))
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
with open(filename, 'wb') as fh:
|
|
||||||
logging.info("written to {}".format(filename))
|
|
||||||
fh.write(r.content)
|
|
||||||
|
|
||||||
|
|
||||||
def get_project_source(yaml):
|
def is_wokwi(self):
|
||||||
# wokwi_id must be an int or 0
|
if self.wokwi_id != 0:
|
||||||
try:
|
return True
|
||||||
wokwi_id = int(yaml['project']['wokwi_id'])
|
|
||||||
except ValueError:
|
|
||||||
logging.error("wokwi id must be an integer")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# it's a wokwi project
|
|
||||||
if wokwi_id != 0:
|
|
||||||
url = "https://wokwi.com/api/projects/{}/verilog".format(wokwi_id)
|
|
||||||
src_file = "user_module_{}.v".format(wokwi_id)
|
|
||||||
fetch_file(url, os.path.join("src", src_file))
|
|
||||||
|
|
||||||
# also fetch the wokwi diagram
|
def is_hdl(self):
|
||||||
url = "https://wokwi.com/api/projects/{}/diagram.json".format(wokwi_id)
|
return not self.is_wokwi()
|
||||||
diagram_file = "wokwi_diagram.json"
|
|
||||||
fetch_file(url, os.path.join("src", diagram_file))
|
|
||||||
|
|
||||||
|
def load_yaml(self):
|
||||||
|
try:
|
||||||
|
with open(os.path.join(self.local_dir, 'info.yaml')) as fh:
|
||||||
|
self.yaml = 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)
|
||||||
|
|
||||||
|
|
||||||
|
# find and save the location of the source files
|
||||||
|
# get name of top module and the verilog module that contains the top
|
||||||
|
def setup_source_files(self):
|
||||||
|
# wokwi_id must be an int or 0
|
||||||
|
try:
|
||||||
|
self.wokwi_id = int(self.yaml['project']['wokwi_id'])
|
||||||
|
except ValueError:
|
||||||
|
logging.error("wokwi id must be an integer")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
self.yaml['project']['git_url'] = self.git_url
|
||||||
|
|
||||||
|
if self.is_hdl():
|
||||||
|
self.top_module = self.yaml['project']['top_module']
|
||||||
|
self.src_files = self.get_hdl_source()
|
||||||
|
self.top_verilog_filename = self.find_top_verilog()
|
||||||
|
else:
|
||||||
|
self.top_module = f"user_module_{self.wokwi_id}"
|
||||||
|
self.src_files = self.get_wokwi_source()
|
||||||
|
self.top_verilog_filename = self.src_files[0]
|
||||||
|
|
||||||
|
self.macro_instance = f"{self.top_module}_{self.index :03}"
|
||||||
|
self.scanchain_instance = f"scanchain_{self.index :03}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_wokwi_source(self):
|
||||||
|
src_file = "user_module_{}.v".format(self.wokwi_id)
|
||||||
return [src_file, 'cells.v']
|
return [src_file, 'cells.v']
|
||||||
|
|
||||||
# else it's HDL, so check source files
|
|
||||||
else:
|
|
||||||
if 'source_files' not in yaml['project']:
|
def get_hdl_source(self):
|
||||||
|
if 'source_files' not in self.yaml['project']:
|
||||||
logging.error("source files must be provided if wokwi_id is set to 0")
|
logging.error("source files must be provided if wokwi_id is set to 0")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
source_files = yaml['project']['source_files']
|
source_files = self.yaml['project']['source_files']
|
||||||
if source_files is None:
|
if source_files is None:
|
||||||
logging.error("must be more than 1 source file")
|
logging.error("must be more than 1 source file")
|
||||||
exit(1)
|
exit(1)
|
||||||
@@ -82,34 +146,402 @@ def get_project_source(yaml):
|
|||||||
logging.error("must be more than 1 source file")
|
logging.error("must be more than 1 source file")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if 'top_module' not in yaml['project']:
|
if 'top_module' not in self.yaml['project']:
|
||||||
logging.error("must provide a top module name")
|
logging.error("must provide a top module name")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
|
if self.yaml['project']['top_module'] == 'top':
|
||||||
|
logging.error("top module cannot be called top - prepend your repo name to make it unique")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
for filename in source_files:
|
for filename in source_files:
|
||||||
if '*' in filename:
|
if '*' in filename:
|
||||||
logging.error("* not allowed, please specify each file")
|
logging.error("* not allowed, please specify each file")
|
||||||
exit(1)
|
exit(1)
|
||||||
if not os.path.exists(os.path.join('src', filename)):
|
if not os.path.exists(os.path.join(self.local_dir, 'src', filename)):
|
||||||
logging.error(f"{filename} doesn't exist in the repo")
|
logging.error(f"{filename} doesn't exist in the repo")
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
return source_files
|
return source_files
|
||||||
|
|
||||||
|
|
||||||
def get_top_module(yaml):
|
def get_yaml(self):
|
||||||
wokwi_id = int(yaml['project']['wokwi_id'])
|
return self.yaml
|
||||||
if wokwi_id != 0:
|
|
||||||
return "user_module_{}".format(wokwi_id)
|
def get_hugo_row(self):
|
||||||
else:
|
return f'| {self.index} | [{self.yaml["documentation"]["title"]}]({self.index :03}) | {self.yaml["documentation"]["author"]}|\n'
|
||||||
return yaml['project']['top_module']
|
|
||||||
|
# docs stuff for index on README.md
|
||||||
|
def get_index_row(self):
|
||||||
|
return f'| {self.index} | {self.yaml["documentation"]["author"]} | {self.yaml["documentation"]["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_doc_yaml(self):
|
||||||
|
# fstring dict support is limited to one level deep, so put the git url and wokwi url in the docs key
|
||||||
|
docs = self.yaml['documentation']
|
||||||
|
docs['project_type'] = self.get_project_type_string()
|
||||||
|
docs['git_url'] = self.git_url
|
||||||
|
return docs
|
||||||
|
|
||||||
|
def get_wokwi_url(self):
|
||||||
|
return f'https://wokwi.com/projects/{self.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"(?:^|[\W])module[\s]{1,}([\w]+)")
|
||||||
|
top_verilog = []
|
||||||
|
for src in self.src_files:
|
||||||
|
with open(os.path.join(self.local_dir, 'src', src)) as fh:
|
||||||
|
for line in fh.readlines():
|
||||||
|
for match in rgx_mod.finditer(line):
|
||||||
|
if match.group(1) == self.top_module:
|
||||||
|
top_verilog.append(src)
|
||||||
|
assert len(top_verilog) == 1
|
||||||
|
return top_verilog[0]
|
||||||
|
|
||||||
|
def clone(self):
|
||||||
|
if os.path.exists(self.local_dir):
|
||||||
|
git_remote = list(git.Repo(self.local_dir).remotes[0].urls)[0]
|
||||||
|
if self.git_url == git_remote:
|
||||||
|
logging.info("git repo already exists and is correct - skipping")
|
||||||
|
else:
|
||||||
|
logging.error("git repo exists and remote doesn't match - abort")
|
||||||
|
logging.error(f"{self.git_url} != {git_remote}")
|
||||||
|
exit(1)
|
||||||
|
else:
|
||||||
|
logging.info("clone")
|
||||||
|
git.Repo.clone_from(self.git_url, self.local_dir, recursive=True)
|
||||||
|
|
||||||
|
def pull(self):
|
||||||
|
repo = git.Repo(self.local_dir)
|
||||||
|
# reset
|
||||||
|
repo.git.reset('--hard')
|
||||||
|
o = repo.remotes.origin
|
||||||
|
o.pull()
|
||||||
|
|
||||||
|
|
||||||
def harden():
|
def __str__(self):
|
||||||
# requires PDK, PDK_ROOT, OPENLANE_ROOT & OPENLANE_IMAGE_NAME to be set in local environment
|
# if self.args.log_email:
|
||||||
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"'
|
# return f"[{self.index:03} : {self.email} : {self.git_url}]"
|
||||||
env = os.environ.copy()
|
# else:
|
||||||
p = subprocess.run(harden_cmd, shell=True, env=env)
|
return f"[{self.index:03} : {self.git_url}]"
|
||||||
if p.returncode != 0:
|
|
||||||
logging.error("harden failed")
|
def fetch_gds(self):
|
||||||
exit(1)
|
install_artifacts(self.git_url, self.local_dir)
|
||||||
|
|
||||||
|
def get_latest_action_url(self):
|
||||||
|
return get_latest_action_url(self.git_url, self.local_dir)
|
||||||
|
|
||||||
|
def get_macro_name(self):
|
||||||
|
return self.top_module
|
||||||
|
|
||||||
|
# instance name of the project, different for each id
|
||||||
|
def get_macro_instance(self):
|
||||||
|
return self.macro_instance
|
||||||
|
|
||||||
|
# instance name of the scanchain module, different for each id
|
||||||
|
def get_scanchain_instance(self):
|
||||||
|
return self.scanchain_instance
|
||||||
|
|
||||||
|
# unique id
|
||||||
|
def get_index(self):
|
||||||
|
return self.index
|
||||||
|
|
||||||
|
# metrics
|
||||||
|
def get_metrics_path(self):
|
||||||
|
return os.path.join(self.local_dir, 'runs/wokwi/reports/metrics.csv')
|
||||||
|
|
||||||
|
# name of the gds file
|
||||||
|
def get_macro_gds_filename(self):
|
||||||
|
return f"{self.top_module}.gds"
|
||||||
|
|
||||||
|
def get_macro_lef_filename(self):
|
||||||
|
return f"{self.top_module}.lef"
|
||||||
|
|
||||||
|
# for GL sims & blackboxing
|
||||||
|
def get_gl_verilog_filename(self):
|
||||||
|
return f"{self.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
|
||||||
|
|
||||||
|
# todo add spef
|
||||||
|
def copy_files_to_caravel(self):
|
||||||
|
files = [
|
||||||
|
(f"runs/wokwi/results/final/gds/{self.get_macro_gds_filename()}", f"gds/{self.get_macro_gds_filename()}"),
|
||||||
|
(f"runs/wokwi/results/final/lef/{self.get_macro_lef_filename()}", f"lef/{self.get_macro_lef_filename()}"),
|
||||||
|
(f"runs/wokwi/results/final/verilog/gl/{self.get_gl_verilog_filename()}", f"verilog/gl/{self.get_gl_verilog_filename()}"),
|
||||||
|
(f"src/{self.top_verilog_filename}", f"verilog/rtl/{self.get_top_verilog_filename()}"),
|
||||||
|
]
|
||||||
|
|
||||||
|
logging.debug("copying files into position")
|
||||||
|
for from_path, to_path in files:
|
||||||
|
from_path = os.path.join(self.local_dir, from_path)
|
||||||
|
logging.debug(f"copy {from_path} to {to_path}")
|
||||||
|
shutil.copyfile(from_path, to_path)
|
||||||
|
|
||||||
|
|
||||||
|
def print_wokwi_id(self):
|
||||||
|
print(self.wokwi_id)
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_wokwi_files(self):
|
||||||
|
logging.info("fetching wokwi files")
|
||||||
|
url = "https://wokwi.com/api/projects/{}/verilog".format(self.wokwi_id)
|
||||||
|
git_utils.fetch_file(url, os.path.join(self.local_dir, "src", src_file))
|
||||||
|
|
||||||
|
# also fetch the wokwi diagram
|
||||||
|
url = "https://wokwi.com/api/projects/{}/diagram.json".format(self.wokwi_id)
|
||||||
|
diagram_file = "wokwi_diagram.json"
|
||||||
|
git_utils.fetch_file(url, os.path.join(self.local_dir, "src", diagram_file))
|
||||||
|
|
||||||
|
|
||||||
|
def create_user_config(self):
|
||||||
|
self.fetch_wokwi_files()
|
||||||
|
|
||||||
|
logging.info("creating include file")
|
||||||
|
filename = 'user_config.tcl'
|
||||||
|
with open(os.path.join(self.local_dir, 'src', filename), 'w') as fh:
|
||||||
|
fh.write("set ::env(DESIGN_NAME) {}\n".format(self.top_module))
|
||||||
|
fh.write('set ::env(VERILOG_FILES) "\\\n')
|
||||||
|
for line, source in enumerate(self.src_files):
|
||||||
|
fh.write(" $::env(DESIGN_DIR)/" + source)
|
||||||
|
if line != len(self.src_files) - 1:
|
||||||
|
fh.write(' \\\n')
|
||||||
|
fh.write('"\n')
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
def harden(self):
|
||||||
|
logging.info(f"hardening {self}")
|
||||||
|
|
||||||
|
# copy golden config
|
||||||
|
shutil.copyfile('golden_config.tcl', os.path.join(self.local_dir, 'src', 'config.tcl'))
|
||||||
|
|
||||||
|
cwd = os.getcwd()
|
||||||
|
os.chdir(self.local_dir)
|
||||||
|
|
||||||
|
# setup user config, not including python fails on github action
|
||||||
|
if 'LOCAL' in os.environ:
|
||||||
|
configure_cmd = './configure.py --create-user-config'
|
||||||
|
else:
|
||||||
|
configure_cmd = 'python ./configure.py --create-user-config'
|
||||||
|
p = subprocess.run(configure_cmd, shell=True)
|
||||||
|
if p.returncode != 0:
|
||||||
|
logging.error(f"configure failed for {self}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# requires 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"'
|
||||||
|
env = os.environ.copy()
|
||||||
|
p = subprocess.run(harden_cmd, shell=True, env=env)
|
||||||
|
if p.returncode != 0:
|
||||||
|
logging.error(f"harden failed for {self}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
os.chdir(cwd)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def harden(self):
|
||||||
|
cwd = os.getcwd()
|
||||||
|
os.chdir(self.local_dir)
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
os.chdir(cwd)
|
||||||
|
|
||||||
|
# doc check
|
||||||
|
# makes sure that the basic info is present
|
||||||
|
def check_yaml_docs(self):
|
||||||
|
yaml = self.yaml
|
||||||
|
logging.info("checking docs")
|
||||||
|
for key in ['author', 'title', 'description', 'how_it_works', 'how_to_test', 'language', 'inputs', 'outputs']:
|
||||||
|
if key not in yaml['documentation']:
|
||||||
|
logging.error("missing key {} in documentation".format(key))
|
||||||
|
exit(1)
|
||||||
|
if yaml['documentation'][key] == "":
|
||||||
|
logging.error("missing value for {} in documentation".format(key))
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# if provided, check discord handle is valid
|
||||||
|
if len(yaml['documentation']['discord']):
|
||||||
|
parts = yaml['documentation']['discord'].split('#')
|
||||||
|
if len(parts) != 2 or len(parts[0]) == 0 or not re.match('^[0-9]{4}$', parts[1]):
|
||||||
|
logging.error('Invalid format for discord username')
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# use pandoc to create a single page PDF preview
|
||||||
|
def create_pdf(self):
|
||||||
|
yaml = self.yaml
|
||||||
|
yaml = yaml['documentation']
|
||||||
|
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()
|
||||||
|
|
||||||
|
with open('datasheet.md', 'w') as fh:
|
||||||
|
fh.write(doc_header)
|
||||||
|
# handle pictures
|
||||||
|
yaml['picture_link'] = ''
|
||||||
|
if yaml['picture']:
|
||||||
|
# skip SVG for now, not supported by pandoc
|
||||||
|
picture_name = yaml['picture']
|
||||||
|
if 'svg' not in picture_name:
|
||||||
|
yaml['picture_link'] = ''.format(picture_name)
|
||||||
|
else:
|
||||||
|
logging.warning("svg not supported")
|
||||||
|
|
||||||
|
# now build the doc & print it
|
||||||
|
try:
|
||||||
|
doc = doc_template.format(**yaml)
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
|
# SVG and PNG renders of the GDS
|
||||||
|
def create_svg(self):
|
||||||
|
gds = glob.glob(os.path.join(self.local_dir, 'runs/wokwi/results/final/gds/*gds'))
|
||||||
|
library = gdstk.read_gds(gds[0])
|
||||||
|
top_cells = library.top_level()
|
||||||
|
top_cells[0].write_svg('gds_render.svg')
|
||||||
|
|
||||||
|
if self.args.create_png:
|
||||||
|
cairosvg.svg2png(url='gds_render.svg', write_to='gds_render.png')
|
||||||
|
|
||||||
|
|
||||||
|
def print_warnings(self):
|
||||||
|
warnings = []
|
||||||
|
with open(os.path.join(self.local_dir, 'runs/wokwi/logs/synthesis/1-synthesis.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_count_from_gl()
|
||||||
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
|
||||||
|
with open(os.path.join(script_dir, 'categories.json')) as fh:
|
||||||
|
categories = json.load(fh)
|
||||||
|
with open(os.path.join(script_dir, 'defs.json')) as fh:
|
||||||
|
defs = json.load(fh)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
by_category = {}
|
||||||
|
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]}
|
||||||
|
|
||||||
|
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"]}|')
|
||||||
|
|
||||||
|
|
||||||
|
# Parse the lib, cell and drive strength an OpenLane gate-level Verilog file
|
||||||
|
def get_cell_count_from_gl(self):
|
||||||
|
cell_count = {}
|
||||||
|
total = 0
|
||||||
|
gl_files = glob.glob(os.path.join(self.local_dir, 'runs/wokwi/results/final/verilog/gl/*.nl.v'))
|
||||||
|
with open(gl_files[0]) 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 = {}
|
||||||
|
for json_file in json_files:
|
||||||
|
with open(json_file) as fh:
|
||||||
|
definition = json.load(fh)
|
||||||
|
definitions[definition['name']] = definition
|
||||||
|
|
||||||
|
with open('defs.json', 'w') as fh:
|
||||||
|
json.dump(definitions, fh)
|
||||||
|
|||||||
+1000
File diff suppressed because it is too large
Load Diff
-116
@@ -1,116 +0,0 @@
|
|||||||
import os
|
|
||||||
import csv
|
|
||||||
import json
|
|
||||||
import re
|
|
||||||
import glob
|
|
||||||
|
|
||||||
# can't find a google version
|
|
||||||
CELL_URL = 'https://antmicro-skywater-pdk-docs.readthedocs.io/en/test-submodules-in-rtd/contents/libraries/sky130_fd_sc_ls/cells/'
|
|
||||||
|
|
||||||
|
|
||||||
def print_warnings(args):
|
|
||||||
warnings = []
|
|
||||||
with open(os.path.join(args.run_dir, 'logs/synthesis/1-synthesis.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(args):
|
|
||||||
with open(os.path.join(args.run_dir, 'reports/metrics.csv')) as f:
|
|
||||||
report = list(csv.DictReader(f))[0]
|
|
||||||
|
|
||||||
print('# Routing stats')
|
|
||||||
print()
|
|
||||||
print('| Utilisation (%) | Wire length (um) |')
|
|
||||||
print('|-------------|------------------|')
|
|
||||||
print('| {} | {} |'.format(report['OpenDP_Util'], report['wire_length']))
|
|
||||||
|
|
||||||
|
|
||||||
# Print the summaries
|
|
||||||
def summarize(args):
|
|
||||||
cell_count = get_cell_count_from_gl(args)
|
|
||||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
|
||||||
|
|
||||||
with open(os.path.join(script_dir, 'categories.json')) as fh:
|
|
||||||
categories = json.load(fh)
|
|
||||||
with open(os.path.join(script_dir, 'defs.json')) as fh:
|
|
||||||
defs = json.load(fh)
|
|
||||||
|
|
||||||
# print all used cells, sorted by frequency
|
|
||||||
total = 0
|
|
||||||
if 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 args.print_cell_category:
|
|
||||||
by_category = {}
|
|
||||||
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]}
|
|
||||||
|
|
||||||
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"]}|')
|
|
||||||
|
|
||||||
|
|
||||||
# Parse the lib, cell and drive strength an OpenLane gate-level Verilog file
|
|
||||||
def get_cell_count_from_gl(args):
|
|
||||||
cell_count = {}
|
|
||||||
total = 0
|
|
||||||
gl_files = glob.glob(os.path.join(args.run_dir, 'results/final/verilog/gl/*.nl.v'))
|
|
||||||
with open(gl_files[0]) 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(args):
|
|
||||||
# replace this with a find
|
|
||||||
json_files = glob.glob('sky130_fd_sc_hd/latest/cells/*/definition.json')
|
|
||||||
definitions = {}
|
|
||||||
for json_file in json_files:
|
|
||||||
with open(json_file) as fh:
|
|
||||||
definition = json.load(fh)
|
|
||||||
definitions[definition['name']] = definition
|
|
||||||
|
|
||||||
with open('defs.json', 'w') as fh:
|
|
||||||
json.dump(definitions, fh)
|
|
||||||
+12
-17
@@ -2,18 +2,15 @@
|
|||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import documentation
|
from project import Project
|
||||||
import project
|
|
||||||
import reports
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser(description="TT setup")
|
parser = argparse.ArgumentParser(description="TT setup")
|
||||||
|
parser.add_argument('--project-dir', help="location of the project", default='.')
|
||||||
parser.add_argument('--yaml', help="the project's yaml configuration file", default='info.yaml')
|
parser.add_argument('--yaml', help="the project's yaml configuration file", default='info.yaml')
|
||||||
parser.add_argument('--run-dir', help="OpenLane run directory", default='runs/wokwi') # set in the github action yaml
|
|
||||||
parser.add_argument('--debug', help="debug logging", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
|
parser.add_argument('--debug', help="debug logging", action="store_const", dest="loglevel", const=logging.DEBUG, default=logging.INFO)
|
||||||
|
|
||||||
# reports & summaries
|
# reports & summaries
|
||||||
parser.add_argument('--create-cell-defs', help="create def file", action="store_const", const=True, default=False)
|
|
||||||
parser.add_argument('--print-cell-summary', help="print summary", action="store_const", const=True, default=False)
|
parser.add_argument('--print-cell-summary', help="print summary", action="store_const", const=True, default=False)
|
||||||
parser.add_argument('--print-cell-category', help="print category", action="store_const", const=True, default=False)
|
parser.add_argument('--print-cell-category', help="print category", action="store_const", const=True, default=False)
|
||||||
parser.add_argument('--print-stats', help="print some stats from the run", action="store_const", const=True)
|
parser.add_argument('--print-stats', help="print some stats from the run", action="store_const", const=True)
|
||||||
@@ -45,35 +42,33 @@ if __name__ == '__main__':
|
|||||||
ch.setFormatter(log_format)
|
ch.setFormatter(log_format)
|
||||||
log.addHandler(ch)
|
log.addHandler(ch)
|
||||||
|
|
||||||
project_yaml = project.load_yaml(args)
|
project = Project(0, 'unknown', args.project_dir, args)
|
||||||
|
project.post_clone_setup()
|
||||||
|
|
||||||
# handle the options
|
# handle the options
|
||||||
if args.check_docs:
|
if args.check_docs:
|
||||||
documentation.check_yaml_docs(project_yaml)
|
project.check_yaml_docs()
|
||||||
|
|
||||||
if args.print_cell_summary or args.print_cell_category:
|
if args.print_cell_summary or args.print_cell_category:
|
||||||
reports.summarize(args)
|
project.summarize()
|
||||||
|
|
||||||
if args.create_cell_defs:
|
|
||||||
reports.create_defs(args)
|
|
||||||
|
|
||||||
if args.print_stats:
|
if args.print_stats:
|
||||||
reports.print_stats(args)
|
project.print_stats()
|
||||||
|
|
||||||
if args.print_warnings:
|
if args.print_warnings:
|
||||||
reports.print_warnings(args)
|
project.print_warnings()
|
||||||
|
|
||||||
elif args.print_wokwi_id:
|
elif args.print_wokwi_id:
|
||||||
project.print_wokwi_id(project_yaml)
|
project.print_wokwi_id()
|
||||||
|
|
||||||
if args.create_user_config:
|
if args.create_user_config:
|
||||||
project.create_user_config(project_yaml)
|
project.create_user_config()
|
||||||
|
|
||||||
if args.harden:
|
if args.harden:
|
||||||
project.harden()
|
project.harden()
|
||||||
|
|
||||||
if args.create_pdf:
|
if args.create_pdf:
|
||||||
documentation.create_pdf(project_yaml)
|
project.create_pdf()
|
||||||
|
|
||||||
if args.create_svg or args.create_png:
|
if args.create_svg or args.create_png:
|
||||||
documentation.create_svg(args)
|
project.create_svg()
|
||||||
|
|||||||
Reference in New Issue
Block a user