mirror of
https://github.com/TinyTapeout/tt-support-tools.git
synced 2026-11-03 04:36:09 +01:00
merge
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
*swp
|
||||
setuptokens.sh
|
||||
__pycache__
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
class CaravelConfig():
|
||||
|
||||
def __init__(self, config, projects, num_projects):
|
||||
self.config = config
|
||||
self.projects = projects
|
||||
self.num_projects = num_projects
|
||||
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# co-ords are bottom left corner
|
||||
def place_macro(self, x, y, instance, rotate, fh):
|
||||
c = self.config['layout']
|
||||
if not rotate:
|
||||
# scanchain first
|
||||
orientation = 'N'
|
||||
macro_instance = self.projects[instance].get_scanchain_instance()
|
||||
fh.write(f"{macro_instance} {x:<4} {y:<4} {orientation}\n")
|
||||
|
||||
macro_instance = self.projects[instance].get_macro_instance()
|
||||
x = x + c['scanchain_w'] + c['scanchain_spc']
|
||||
fh.write(f"{macro_instance} {x:<4} {y:<4} {orientation}\n")
|
||||
else:
|
||||
# macro first
|
||||
orientation = 'S'
|
||||
macro_instance = self.projects[instance].get_macro_instance()
|
||||
fh.write(f"{macro_instance} {x:<4} {y:<4} {orientation}\n")
|
||||
|
||||
macro_instance = self.projects[instance].get_scanchain_instance()
|
||||
x = x + c['module_w'] + c['scanchain_spc']
|
||||
y = y + c['module_h'] - c['scanchain_h']
|
||||
fh.write(f"{macro_instance} {x:<4} {y:<4} {orientation}\n")
|
||||
|
||||
# create macro file & positions, power hooks
|
||||
def create_macro_config(self):
|
||||
# array size
|
||||
c = self.config['layout']
|
||||
|
||||
pos_x = 2
|
||||
pos_y = 0
|
||||
dir_x = 1
|
||||
dir_y = 1
|
||||
|
||||
# macro.cfg: where macros are placed
|
||||
logging.info("creating macro.cfg")
|
||||
with open("openlane/user_project_wrapper/macro.cfg", 'w') as fh:
|
||||
fh.write(f"scan_controller {c['scan_control_x']} {c['scan_control_y']} N\n")
|
||||
for i in range(self.num_projects):
|
||||
logging.debug(f"Block {i:d} at ({pos_x:d}, {pos_y:d})")
|
||||
# calculate 0, 0 point
|
||||
macro_x = c['start_x'] + pos_x * (c['module_w'] + c['scanchain_w'] + c['scanchain_spc'] + c['space_x'])
|
||||
macro_y = c['start_y'] + pos_y * (c['module_h'] + c['space_y'])
|
||||
|
||||
# rotated?
|
||||
if dir_x == 1:
|
||||
rotate = False
|
||||
else:
|
||||
rotate = True
|
||||
|
||||
self.place_macro(macro_x, macro_y, i, rotate, fh)
|
||||
|
||||
# next position
|
||||
pos_x += 2 * dir_x
|
||||
|
||||
if pos_x >= c['cols']:
|
||||
pos_x = c['cols'] - 1
|
||||
pos_y += dir_y
|
||||
dir_x = - dir_x
|
||||
|
||||
elif pos_x < 0:
|
||||
pos_x = 0
|
||||
pos_y += dir_y
|
||||
dir_x = - dir_x
|
||||
|
||||
if pos_y >= c['rows']:
|
||||
pos_y = c['rows'] - 1
|
||||
dir_y = - dir_y
|
||||
|
||||
logging.info(f"total user macros placed: {self.num_projects}")
|
||||
|
||||
with open(os.path.join(self.script_dir, 'caravel_template', 'upw_config.json')) as fh:
|
||||
caravel_config = json.load(fh)
|
||||
|
||||
power_domains = "vccd1 vssd1 vccd1 vssd1"
|
||||
power_config = []
|
||||
logging.info("creating macro hooks")
|
||||
|
||||
for i in range(self.num_projects):
|
||||
power_config.append(f"{self.projects[i].get_scanchain_instance()} {power_domains},")
|
||||
power_config.append(f"{self.projects[i].get_macro_instance()} {power_domains},")
|
||||
power_config.append(f"scan_controller {power_domains}") # no trailing comma
|
||||
|
||||
caravel_config["FP_PDN_MACRO_HOOKS"] = power_config
|
||||
|
||||
logging.info("GDS and LEF")
|
||||
lef_prefix = "dir::../../lef/"
|
||||
gds_prefix = "dir::../../gds/"
|
||||
caravel_config["EXTRA_LEFS"].append(lef_prefix + "scan_controller.lef")
|
||||
caravel_config["EXTRA_LEFS"].append(lef_prefix + "scanchain.lef")
|
||||
caravel_config["EXTRA_GDS_FILES"].append(gds_prefix + "scan_controller.gds")
|
||||
caravel_config["EXTRA_GDS_FILES"].append(gds_prefix + "scanchain.gds")
|
||||
for project in self.projects:
|
||||
if not project.is_fill():
|
||||
caravel_config["EXTRA_LEFS"].append(lef_prefix + project.get_macro_lef_filename())
|
||||
caravel_config["EXTRA_GDS_FILES"].append(gds_prefix + project.get_macro_gds_filename())
|
||||
|
||||
with open("openlane/user_project_wrapper/config.json", 'w') as fh:
|
||||
json.dump(caravel_config, fh, indent=4)
|
||||
|
||||
# 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(os.path.join(self.script_dir, 'caravel_template', 'upw_pre.v')) 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(os.path.join(self.script_dir, 'caravel_template', 'upw_post.v')) 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:
|
||||
if not project.is_fill():
|
||||
logging.info(project)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
RBA::Application.instance.main_window.current_view.save_image("./pics/tinytapeout.png",2000,2000)
|
||||
@@ -0,0 +1,247 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<config>
|
||||
<absolute-units>false</absolute-units>
|
||||
<abstract-mode-enabled>false</abstract-mode-enabled>
|
||||
<abstract-mode-width>10.0</abstract-mode-width>
|
||||
<apply-text-trans>true</apply-text-trans>
|
||||
<background-color>auto</background-color>
|
||||
<bitmap-caching>true</bitmap-caching>
|
||||
<bitmap-oversampling>1</bitmap-oversampling>
|
||||
<cell-list-sorting>by-name</cell-list-sorting>
|
||||
<child-context-color>auto</child-context-color>
|
||||
<child-context-dimming>50</child-context-dimming>
|
||||
<child-context-enabled>false</child-context-enabled>
|
||||
<child-context-hollow>false</child-context-hollow>
|
||||
<cib-context-cell/>
|
||||
<cib-context-mode>any-top</cib-context-mode>
|
||||
<cib-max-inst-count>1000</cib-max-inst-count>
|
||||
<cib-window-dim>1.0</cib-window-dim>
|
||||
<cib-window-mode>fit-marker</cib-window-mode>
|
||||
<cib-window-state/>
|
||||
<circle-points>32</circle-points>
|
||||
<clear-ruler-new-cell>false</clear-ruler-new-cell>
|
||||
<color-palette/>
|
||||
<combine-mode>add</combine-mode>
|
||||
<context-color>auto</context-color>
|
||||
<context-dimming>50</context-dimming>
|
||||
<context-hollow>false</context-hollow>
|
||||
<current-lib-view/>
|
||||
<current-ruler-template>0</current-ruler-template>
|
||||
<custom-macro-paths/>
|
||||
<dbu-units>false</dbu-units>
|
||||
<def-import-spec/>
|
||||
<default-add-other-layers>false</default-add-other-layers>
|
||||
<default-font-size>0</default-font-size>
|
||||
<default-grids>0.01,0.005,0.001</default-grids>
|
||||
<default-layer-properties/>
|
||||
<default-text-size>0.1</default-text-size>
|
||||
<diff-detailed>false</diff-detailed>
|
||||
<diff-exact>false</diff-exact>
|
||||
<diff-expand-cell-arrays>false</diff-expand-cell-arrays>
|
||||
<diff-run-xor>false</diff-run-xor>
|
||||
<diff-summarize>false</diff-summarize>
|
||||
<digits-dbu>2</digits-dbu>
|
||||
<digits-micron>5</digits-micron>
|
||||
<draw-array-border-instances>false</draw-array-border-instances>
|
||||
<drawing-workers>1</drawing-workers>
|
||||
<drop-small-cells>false</drop-small-cells>
|
||||
<drop-small-cells-condition>0</drop-small-cells-condition>
|
||||
<drop-small-cells-value>10</drop-small-cells-value>
|
||||
<edit-connect-angle-mode>any</edit-connect-angle-mode>
|
||||
<edit-grid>global</edit-grid>
|
||||
<edit-hier-copy-mode>-1</edit-hier-copy-mode>
|
||||
<edit-inst-angle>0</edit-inst-angle>
|
||||
<edit-inst-array>false</edit-inst-array>
|
||||
<edit-inst-cell-name/>
|
||||
<edit-inst-column_x>0</edit-inst-column_x>
|
||||
<edit-inst-column_y>0</edit-inst-column_y>
|
||||
<edit-inst-columns>1</edit-inst-columns>
|
||||
<edit-inst-lib-name/>
|
||||
<edit-inst-mirror>false</edit-inst-mirror>
|
||||
<edit-inst-pcell-parameters/>
|
||||
<edit-inst-place-origin>false</edit-inst-place-origin>
|
||||
<edit-inst-row_x>0</edit-inst-row_x>
|
||||
<edit-inst-row_y>0</edit-inst-row_y>
|
||||
<edit-inst-rows>1</edit-inst-rows>
|
||||
<edit-inst-scale>1</edit-inst-scale>
|
||||
<edit-max-shapes-of-instances>1000</edit-max-shapes-of-instances>
|
||||
<edit-mode>false</edit-mode>
|
||||
<edit-move-angle-mode>any</edit-move-angle-mode>
|
||||
<edit-path-ext-type>flush</edit-path-ext-type>
|
||||
<edit-path-ext-var-begin>0.0</edit-path-ext-var-begin>
|
||||
<edit-path-ext-var-end>0.0</edit-path-ext-var-end>
|
||||
<edit-path-width>0.1</edit-path-width>
|
||||
<edit-show-shapes-of-instances>true</edit-show-shapes-of-instances>
|
||||
<edit-snap-to-objects>false</edit-snap-to-objects>
|
||||
<edit-text-halign>left</edit-text-halign>
|
||||
<edit-text-size>0</edit-text-size>
|
||||
<edit-text-string>ABC</edit-text-string>
|
||||
<edit-text-valign>bottom</edit-text-valign>
|
||||
<edit-top-level-selection>false</edit-top-level-selection>
|
||||
<fit-new-cell>true</fit-new-cell>
|
||||
<flat-cell-list>false</flat-cell-list>
|
||||
<full-hierarchy-new-cell>true</full-hierarchy-new-cell>
|
||||
<global-trans>r0</global-trans>
|
||||
<grid-axis-color>auto</grid-axis-color>
|
||||
<grid-color>auto</grid-color>
|
||||
<grid-grid-color>auto</grid-grid-color>
|
||||
<grid-micron>0.001</grid-micron>
|
||||
<grid-ruler-color>auto</grid-ruler-color>
|
||||
<grid-show-ruler>true</grid-show-ruler>
|
||||
<grid-style0>invisible</grid-style0>
|
||||
<grid-style1>dots</grid-style1>
|
||||
<grid-style2>tenths-dotted-lines</grid-style2>
|
||||
<grid-visible>true</grid-visible>
|
||||
<guiding-shape-color>auto</guiding-shape-color>
|
||||
<guiding-shape-line-width>1</guiding-shape-line-width>
|
||||
<guiding-shape-vertex-size>5</guiding-shape-vertex-size>
|
||||
<guiding-shape-visible>true</guiding-shape-visible>
|
||||
<hide-empty-layers>false</hide-empty-layers>
|
||||
<image-cache-size>1</image-cache-size>
|
||||
<initial-hier-depth>1</initial-hier-depth>
|
||||
<initial-technology/>
|
||||
<inst-color>auto</inst-color>
|
||||
<inst-label-font>0</inst-label-font>
|
||||
<inst-label-transform>true</inst-label-transform>
|
||||
<inst-visible>true</inst-visible>
|
||||
<key-bindings/>
|
||||
<l2ndb-export-circuit-cell-prefix>CIRCUIT_</l2ndb-export-circuit-cell-prefix>
|
||||
<l2ndb-export-device-cell-prefix>DEVICE_</l2ndb-export-device-cell-prefix>
|
||||
<l2ndb-export-net-cell-prefix>NET_</l2ndb-export-net-cell-prefix>
|
||||
<l2ndb-export-net-propname/>
|
||||
<l2ndb-export-produce-circuit-cells>false</l2ndb-export-produce-circuit-cells>
|
||||
<l2ndb-export-produce-device-cells>false</l2ndb-export-produce-device-cells>
|
||||
<l2ndb-export-start-layer-number>1000</l2ndb-export-start-layer-number>
|
||||
<l2ndb-marker-color>auto</l2ndb-marker-color>
|
||||
<l2ndb-marker-cycle-colors>255,0,0 0,255,0 0,0,255 255,255,0 255,0,255 0,255,255 160,80,255 255,160,0</l2ndb-marker-cycle-colors>
|
||||
<l2ndb-marker-cycle-colors-enabled>false</l2ndb-marker-cycle-colors-enabled>
|
||||
<l2ndb-marker-dither-pattern>-1</l2ndb-marker-dither-pattern>
|
||||
<l2ndb-marker-halo>-1</l2ndb-marker-halo>
|
||||
<l2ndb-marker-intensity>50</l2ndb-marker-intensity>
|
||||
<l2ndb-marker-line-width>-1</l2ndb-marker-line-width>
|
||||
<l2ndb-marker-use-original-colors>false</l2ndb-marker-use-original-colors>
|
||||
<l2ndb-marker-vertex-size>-1</l2ndb-marker-vertex-size>
|
||||
<l2ndb-max-shapes-highlighted>10000</l2ndb-max-shapes-highlighted>
|
||||
<l2ndb-show-all>true</l2ndb-show-all>
|
||||
<l2ndb-window-dim>1.0</l2ndb-window-dim>
|
||||
<l2ndb-window-mode>fit-net</l2ndb-window-mode>
|
||||
<l2ndb-window-state/>
|
||||
<layers-always-show-layout-index>false</layers-always-show-layout-index>
|
||||
<layers-always-show-ld>true</layers-always-show-ld>
|
||||
<layers-always-show-source>false</layers-always-show-source>
|
||||
<layout-file-watcher-enabled>true</layout-file-watcher-enabled>
|
||||
<lef-import-spec/>
|
||||
<line-style-palette/>
|
||||
<macro-editor-active-macro/>
|
||||
<macro-editor-console-interpreter/>
|
||||
<macro-editor-console-mru/>
|
||||
<macro-editor-current-macro/>
|
||||
<macro-editor-debugging-enabled>true</macro-editor-debugging-enabled>
|
||||
<macro-editor-file-watcher-enabled>true</macro-editor-file-watcher-enabled>
|
||||
<macro-editor-font-family/>
|
||||
<macro-editor-font-size/>
|
||||
<macro-editor-indent>2</macro-editor-indent>
|
||||
<macro-editor-open-macros/>
|
||||
<macro-editor-save-all-on-run>false</macro-editor-save-all-on-run>
|
||||
<macro-editor-stop-on-exception>true</macro-editor-stop-on-exception>
|
||||
<macro-editor-styles/>
|
||||
<macro-editor-tab-width>8</macro-editor-tab-width>
|
||||
<macro-editor-watch-expressions/>
|
||||
<macro-editor-window-state/>
|
||||
<markers-visible>true</markers-visible>
|
||||
<menu-items-hidden/>
|
||||
<min-inst-label-size>16</min-inst-label-size>
|
||||
<mouse-wheel-mode>0</mouse-wheel-mode>
|
||||
<mru>'/home/matt/work/asic-workshop/shuttle6/zero_to_asic_mpw6/gds/user_project_wrapper.gds.gz' '/home/matt/work/asic-workshop/shuttle7/zero_to_asic_mpw7/gds/user_project_wrapper.gds.gz' '/home/matt/work/asic-workshop/shuttle7/tinytapeout-02/gds/user_project_wrapper.gds' '/home/matt/work/asic-workshop/shuttle8/tinytapeout-02/gds/user_project_wrapper.gds' '/home/matt/work/asic-workshop/shuttle9/caravel_user_project/gds/user_project_wrapper.gds' '/home/matt/work/asic-workshop/shuttle9/tinytapeout-03/gds/user_project_wrapper.gds'</mru>
|
||||
<navigator-show-all-hier-levels>false</navigator-show-all-hier-levels>
|
||||
<navigator-show-images>true</navigator-show-images>
|
||||
<no-stipple>false</no-stipple>
|
||||
<nt-marker-color>auto</nt-marker-color>
|
||||
<nt-marker-cycle-colors>255,0,0 0,255,0 0,0,255 255,255,0 255,0,255 0,255,255 160,80,255 255,160,0</nt-marker-cycle-colors>
|
||||
<nt-marker-cycle-colors-enabled>false</nt-marker-cycle-colors-enabled>
|
||||
<nt-marker-dither-pattern>-1</nt-marker-dither-pattern>
|
||||
<nt-marker-halo>-1</nt-marker-halo>
|
||||
<nt-marker-intensity>50</nt-marker-intensity>
|
||||
<nt-marker-line-width>-1</nt-marker-line-width>
|
||||
<nt-marker-vertex-size>-1</nt-marker-vertex-size>
|
||||
<nt-max-shapes-highlighted>10000</nt-max-shapes-highlighted>
|
||||
<nt-window-dim>1.0</nt-window-dim>
|
||||
<nt-window-mode>fit-net</nt-window-mode>
|
||||
<pan-distance>0.15</pan-distance>
|
||||
<paste-display-mode>2</paste-display-mode>
|
||||
<pcb-import-spec/>
|
||||
<rdb-context-mode>database-top</rdb-context-mode>
|
||||
<rdb-marker-color>auto</rdb-marker-color>
|
||||
<rdb-marker-dither-pattern>-1</rdb-marker-dither-pattern>
|
||||
<rdb-marker-halo>-1</rdb-marker-halo>
|
||||
<rdb-marker-line-width>-1</rdb-marker-line-width>
|
||||
<rdb-marker-vertex-size>-1</rdb-marker-vertex-size>
|
||||
<rdb-max-marker-count>1000</rdb-max-marker-count>
|
||||
<rdb-window-dim>1.0</rdb-window-dim>
|
||||
<rdb-window-mode>fit-marker</rdb-window-mode>
|
||||
<rdb-window-state-v2/>
|
||||
<reader-options-show-always>false</reader-options-show-always>
|
||||
<ruler-color>auto</ruler-color>
|
||||
<ruler-grid-snap>false</ruler-grid-snap>
|
||||
<ruler-halo>true</ruler-halo>
|
||||
<ruler-obj-snap>true</ruler-obj-snap>
|
||||
<ruler-snap-mode>any</ruler-snap-mode>
|
||||
<ruler-snap-range>8</ruler-snap-range>
|
||||
<ruler-templates>mode=normal,title=Ruler,category=_ruler,fmt=$D,fmt_x=$X,fmt_y=$Y,position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=ruler,outline=diag,snap=true,angle_constraint=global;mode=single_click,title=Cross,category=_cross,fmt='$U,$V',fmt_x='',fmt_y='',position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=cross_both,outline=diag,snap=true,angle_constraint=global;mode=auto_metric,title=Measure,category=_measure,fmt=$D,fmt_x=$X,fmt_y=$Y,position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=ruler,outline=diag,snap=true,angle_constraint=global;mode=normal,title=Ellipse,category='',fmt='',fmt_x='W=$(abs(X))',fmt_y='H=$(abs(Y))',position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=line,outline=ellipse,snap=true,angle_constraint=global;mode=normal,title=Box,category='',fmt='',fmt_x='W=$(abs(X))',fmt_y='H=$(abs(Y))',position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=line,outline=box,snap=true,angle_constraint=global;mode=normal,title=XSection,category=XS,fmt=XS,fmt_x='',fmt_y='',position=auto,xalign=auto,yalign=auto,xlabel_xalign=auto,xlabel_yalign=auto,ylabel_xalign=auto,ylabel_yalign=auto,style=arrow_both,outline=diag,snap=true,angle_constraint=global</ruler-templates>
|
||||
<rulers>-1</rulers>
|
||||
<salt-manager-window-state/>
|
||||
<search-range>5</search-range>
|
||||
<sel-color>auto</sel-color>
|
||||
<sel-dither-pattern>1</sel-dither-pattern>
|
||||
<sel-halo>true</sel-halo>
|
||||
<sel-inside-pcells-mode>false</sel-inside-pcells-mode>
|
||||
<sel-line-width>1</sel-line-width>
|
||||
<sel-transient-mode>true</sel-transient-mode>
|
||||
<sel-vertex-size>3</sel-vertex-size>
|
||||
<shb-context-cell/>
|
||||
<shb-context-mode>any-top</shb-context-mode>
|
||||
<shb-max-inst-count>1000</shb-max-inst-count>
|
||||
<shb-max-shape-count>1000</shb-max-shape-count>
|
||||
<shb-window-dim>1.0</shb-window-dim>
|
||||
<shb-window-mode>fit-marker</shb-window-mode>
|
||||
<shb-window-state/>
|
||||
<show-bookmarks-view>false</show-bookmarks-view>
|
||||
<show-hierarchy-panel>true</show-hierarchy-panel>
|
||||
<show-layer-panel>true</show-layer-panel>
|
||||
<show-layer-toolbox>true</show-layer-toolbox>
|
||||
<show-libraries-view>true</show-libraries-view>
|
||||
<show-navigator>false</show-navigator>
|
||||
<show-properties>false</show-properties>
|
||||
<show-toolbar>true</show-toolbar>
|
||||
<split-cell-list>false</split-cell-list>
|
||||
<split-lib-views>false</split-lib-views>
|
||||
<sr-max-item-count>1000</sr-max-item-count>
|
||||
<sr-window-dim>1.0</sr-window-dim>
|
||||
<sr-window-mode>fit-marker</sr-window-mode>
|
||||
<sr-window-state/>
|
||||
<stipple-offset>true</stipple-offset>
|
||||
<stipple-palette/>
|
||||
<stream-import-spec2/>
|
||||
<synchronized-views>false</synchronized-views>
|
||||
<tech-editor-window-state/>
|
||||
<technology-data/>
|
||||
<test-shapes-in-view>false</test-shapes-in-view>
|
||||
<text-color>auto</text-color>
|
||||
<text-font>0</text-font>
|
||||
<text-lazy-rendering>true</text-lazy-rendering>
|
||||
<text-visible>true</text-visible>
|
||||
<tip-window-hidden>editor-mode=4,editor-mode=0</tip-window-hidden>
|
||||
<window-geometry/>
|
||||
<window-state/>
|
||||
<xor-anotb>false</xor-anotb>
|
||||
<xor-axorb>true</xor-axorb>
|
||||
<xor-bnota>false</xor-bnota>
|
||||
<xor-input-mode>all</xor-input-mode>
|
||||
<xor-layer-offset/>
|
||||
<xor-num-workers>1</xor-num-workers>
|
||||
<xor-output-mode>rdb</xor-output-mode>
|
||||
<xor-region-mode>all</xor-region-mode>
|
||||
<xor-summarize>false</xor-summarize>
|
||||
<xor-tiling/>
|
||||
<xor-tolerances/>
|
||||
</config>
|
||||
@@ -0,0 +1,109 @@
|
||||
{
|
||||
"DESIGN_NAME": "user_project_wrapper",
|
||||
"VERILOG_FILES": [
|
||||
"dir::../../verilog/rtl/defines.v",
|
||||
"dir::../../verilog/rtl/user_project_wrapper.v"
|
||||
],
|
||||
"ROUTING_CORES": 8,
|
||||
"CLOCK_PERIOD": 25,
|
||||
"CLOCK_PORT": "wb_clk_i",
|
||||
"CLOCK_NET": "mprj.clk",
|
||||
"FP_PDN_MACRO_HOOKS": [],
|
||||
"MACRO_PLACEMENT_CFG": "dir::macro.cfg",
|
||||
"PDN_CFG": "dir::pdn_cfg.tcl",
|
||||
"MAGIC_DEF_LABELS": 0,
|
||||
"VERILOG_FILES_BLACKBOX": [
|
||||
"dir::../../verilog/rtl/defines.v",
|
||||
"dir::../../verilog/blackbox_project_includes.v"
|
||||
],
|
||||
"EXTRA_LEFS": [],
|
||||
"EXTRA_GDS_FILES": [],
|
||||
"BASE_SDC_FILE": "dir::base_user_project_wrapper.sdc",
|
||||
"RUN_KLAYOUT_XOR": 0,
|
||||
"RUN_KLAYOUT_DRC": 0,
|
||||
"RUN_MAGIC_DRC": 0,
|
||||
"IO_SYNC": 0,
|
||||
"FP_PDN_CHECK_NODES": 0,
|
||||
"GRT_ALLOW_CONGESTION": 1,
|
||||
"GRT_LAYER_ADJUSTMENTS": "1,0.99,0.99,0,0,0",
|
||||
"SYNTH_ELABORATE_ONLY": 1,
|
||||
"PL_RANDOM_GLB_PLACEMENT": 1,
|
||||
"PL_RESIZER_DESIGN_OPTIMIZATIONS": 0,
|
||||
"PL_RESIZER_TIMING_OPTIMIZATIONS": 0,
|
||||
"PL_RESIZER_BUFFER_INPUT_PORTS": 0,
|
||||
"FP_PDN_ENABLE_RAILS": 0,
|
||||
"DIODE_INSERTION_STRATEGY": 0,
|
||||
"RUN_FILL_INSERTION": 0,
|
||||
"RUN_TAP_DECAP_INSERTION": 0,
|
||||
"FP_PDN_VPITCH": 140,
|
||||
"FP_PDN_VOFFSET": 186,
|
||||
"FP_PDN_HPITCH": 185,
|
||||
"FP_PDN_HOFFSET": 160,
|
||||
"CLOCK_TREE_SYNTH": 0,
|
||||
"MAGIC_ZEROIZE_ORIGIN": 0,
|
||||
"FP_SIZING": "absolute",
|
||||
"RUN_CVC": 0,
|
||||
"UNIT": 2.4,
|
||||
"FP_IO_VEXTEND": "expr::2 * $UNIT",
|
||||
"FP_IO_HEXTEND": "expr::2 * $UNIT",
|
||||
"FP_IO_VLENGTH": "expr::$UNIT",
|
||||
"FP_IO_HLENGTH": "expr::$UNIT",
|
||||
"FP_IO_VTHICKNESS_MULT": 4,
|
||||
"FP_IO_HTHICKNESS_MULT": 4,
|
||||
"FP_PDN_CORE_RING": 1,
|
||||
"FP_PDN_CORE_RING_VWIDTH": 3.1,
|
||||
"FP_PDN_CORE_RING_HWIDTH": 3.1,
|
||||
"FP_PDN_CORE_RING_VOFFSET": 12.45,
|
||||
"FP_PDN_CORE_RING_HOFFSET": 12.45,
|
||||
"FP_PDN_CORE_RING_VSPACING": 1.7,
|
||||
"FP_PDN_CORE_RING_HSPACING": 1.7,
|
||||
"FP_PDN_VWIDTH": 3.1,
|
||||
"FP_PDN_HWIDTH": 3.1,
|
||||
"FP_PDN_VSPACING": "expr::(5 * $FP_PDN_CORE_RING_VWIDTH)",
|
||||
"FP_PDN_HSPACING": "expr::(5 * $FP_PDN_CORE_RING_HWIDTH)",
|
||||
"VDD_NETS": [
|
||||
"vccd1",
|
||||
"vccd2",
|
||||
"vdda1",
|
||||
"vdda2"
|
||||
],
|
||||
"GND_NETS": [
|
||||
"vssd1",
|
||||
"vssd2",
|
||||
"vssa1",
|
||||
"vssa2"
|
||||
],
|
||||
"SYNTH_USE_PG_PINS_DEFINES": "USE_POWER_PINS",
|
||||
"pdk::sky130*": {
|
||||
"RT_MAX_LAYER": "met4",
|
||||
"DIE_AREA": "0 0 2920 3520",
|
||||
"FP_DEF_TEMPLATE": "dir::fixed_dont_change/user_project_wrapper.def",
|
||||
"scl::sky130_fd_sc_hd": {
|
||||
"CLOCK_PERIOD": 25
|
||||
},
|
||||
"scl::sky130_fd_sc_hdll": {
|
||||
"CLOCK_PERIOD": 10
|
||||
},
|
||||
"scl::sky130_fd_sc_hs": {
|
||||
"CLOCK_PERIOD": 8
|
||||
},
|
||||
"scl::sky130_fd_sc_ls": {
|
||||
"CLOCK_PERIOD": 10,
|
||||
"SYNTH_MAX_FANOUT": 5
|
||||
},
|
||||
"scl::sky130_fd_sc_ms": {
|
||||
"CLOCK_PERIOD": 10
|
||||
}
|
||||
},
|
||||
"pdk::gf180mcuC": {
|
||||
"STD_CELL_LIBRARY": "gf180mcu_fd_sc_mcu7t5v0",
|
||||
"FP_PDN_CHECK_NODES": 0,
|
||||
"FP_PDN_ENABLE_RAILS": 0,
|
||||
"RT_MAX_LAYER": "Metal4",
|
||||
"DIE_AREA": "0 0 3000 3000",
|
||||
"FP_DEF_TEMPLATE": "dir::fixed_dont_change/user_project_wrapper_gf180mcu.def",
|
||||
"PL_OPENPHYSYN_OPTIMIZATIONS": 0,
|
||||
"DIODE_INSERTION_STRATEGY": 0,
|
||||
"MAGIC_WRITE_FULL_LEF": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// end of module instantiation
|
||||
|
||||
endmodule // user_project_wrapper
|
||||
`default_nettype wire
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: 2020 Efabless Corporation
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
`default_nettype none
|
||||
/*
|
||||
*-------------------------------------------------------------
|
||||
*
|
||||
* user_project_wrapper
|
||||
*
|
||||
* This wrapper enumerates all of the pins available to the
|
||||
* user for the user project.
|
||||
*
|
||||
* An example user project is provided in this wrapper. The
|
||||
* example should be removed and replaced with the actual
|
||||
* user project.
|
||||
*
|
||||
* THIS FILE HAS BEEN GENERATED USING multi_tools_project CODEGEN
|
||||
* IF YOU NEED TO MAKE EDITS TO IT, EDIT codegen/caravel_iface_header.txt
|
||||
*
|
||||
*-------------------------------------------------------------
|
||||
*/
|
||||
|
||||
module user_project_wrapper #(
|
||||
parameter BITS = 32
|
||||
)(
|
||||
`ifdef USE_POWER_PINS
|
||||
inout vdda1, // User area 1 3.3V supply
|
||||
inout vdda2, // User area 2 3.3V supply
|
||||
inout vssa1, // User area 1 analog ground
|
||||
inout vssa2, // User area 2 analog ground
|
||||
inout vccd1, // User area 1 1.8V supply
|
||||
inout vccd2, // User area 2 1.8v supply
|
||||
inout vssd1, // User area 1 digital ground
|
||||
inout vssd2, // User area 2 digital ground
|
||||
`endif
|
||||
|
||||
// Wishbone Slave ports (WB MI A)
|
||||
input wb_clk_i,
|
||||
input wb_rst_i,
|
||||
input wbs_stb_i,
|
||||
input wbs_cyc_i,
|
||||
input wbs_we_i,
|
||||
input [3:0] wbs_sel_i,
|
||||
input [31:0] wbs_dat_i,
|
||||
input [31:0] wbs_adr_i,
|
||||
output wbs_ack_o,
|
||||
output [31:0] wbs_dat_o,
|
||||
|
||||
// Logic Analyzer Signals
|
||||
input [127:0] la_data_in,
|
||||
output [127:0] la_data_out,
|
||||
input [127:0] la_oenb,
|
||||
|
||||
// IOs
|
||||
input [`MPRJ_IO_PADS-1:0] io_in,
|
||||
output [`MPRJ_IO_PADS-1:0] io_out,
|
||||
output [`MPRJ_IO_PADS-1:0] io_oeb,
|
||||
|
||||
// Analog (direct connection to GPIO pad---use with caution)
|
||||
// Note that analog I/O is not available on the 7 lowest-numbered
|
||||
// GPIO pads, and so the analog_io indexing is offset from the
|
||||
// GPIO indexing by 7 (also upper 2 GPIOs do not have analog_io).
|
||||
inout [`MPRJ_IO_PADS-10:0] analog_io,
|
||||
|
||||
// Independent clock (on independent integer divider)
|
||||
input user_clock2,
|
||||
|
||||
// User maskable interrupt signals
|
||||
output [2:0] user_irq
|
||||
);
|
||||
|
||||
// start of module instantiation
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
start_date: '2023-03-01'
|
||||
name: 'tt03' # used for university orders
|
||||
filler_project: https://github.com/TinyTapeout/tt03-test-invert
|
||||
project_dir: 'projects'
|
||||
tmp_dir: '/tmp/tt'
|
||||
num_projects: 250
|
||||
|
||||
layout:
|
||||
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
|
||||
|
||||
scan_control_x: 100
|
||||
scan_control_y: 100
|
||||
Executable
+308
@@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3
|
||||
from orders import Orders
|
||||
import datetime
|
||||
import yaml
|
||||
import argparse, logging, sys, os, collections
|
||||
from project import Project
|
||||
from documentation import Docs
|
||||
from caravel import CaravelConfig
|
||||
|
||||
|
||||
# pipe handling
|
||||
from signal import signal, SIGPIPE, SIG_DFL
|
||||
signal(SIGPIPE, SIG_DFL)
|
||||
|
||||
|
||||
class Projects():
|
||||
|
||||
def __init__(self, config, args):
|
||||
self.args = args
|
||||
self.config = config
|
||||
|
||||
if args.test:
|
||||
self.projects_file = 'test_projects.yaml'
|
||||
self.project_dir = config['test_project_dir']
|
||||
else:
|
||||
self.projects_file = 'projects.yaml'
|
||||
self.project_dir = config['project_dir']
|
||||
|
||||
if not os.path.exists(self.project_dir):
|
||||
os.makedirs(self.project_dir)
|
||||
|
||||
try:
|
||||
with open(self.projects_file) as fh:
|
||||
self.project_config = yaml.safe_load(fh)
|
||||
except FileNotFoundError:
|
||||
logging.error(f"{self.projects_file} 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
|
||||
|
||||
"""
|
||||
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']
|
||||
|
||||
# not sure if this is a good idea
|
||||
if self.project_config[index]['status'] == 'disable':
|
||||
filler = True
|
||||
|
||||
# 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
|
||||
# fill projects will load from the fill project's directory
|
||||
logging.debug("post clone setup")
|
||||
project.post_clone_setup()
|
||||
logging.debug(project)
|
||||
|
||||
# fetch the wokwi source
|
||||
if args.clone_all:
|
||||
if not project.is_fill() and project.is_wokwi():
|
||||
project.fetch_wokwi_files()
|
||||
|
||||
if args.harden:
|
||||
if filler is False:
|
||||
project.create_user_config()
|
||||
project.golden_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
|
||||
for project in self.projects:
|
||||
if project.get_git_remote() != project.git_url:
|
||||
logging.warning(f"{project} doesn't match remote: {project.get_git_remote()}")
|
||||
exit(1)
|
||||
|
||||
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)
|
||||
|
||||
logging.info(f"loaded {len(self.projects)} projects")
|
||||
|
||||
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_cell_project = None
|
||||
max_util = 0
|
||||
min_util = 100
|
||||
max_util_project = None
|
||||
languages = {}
|
||||
tags = []
|
||||
|
||||
for project in self.projects:
|
||||
if not project.is_fill():
|
||||
try:
|
||||
dt = datetime.datetime.strptime(project.metrics['total_runtime'][:-3], '%Hh%Mm%Ss')
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
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.get_cell_count_from_synth()
|
||||
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
|
||||
|
||||
try:
|
||||
tags += yaml_data['tag'].split(',')
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
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}")
|
||||
tags = [ x.strip().lower() for x in tags ]
|
||||
tags = list(filter(lambda a: a != "", tags))
|
||||
|
||||
def count_items(lst):
|
||||
freq = {}
|
||||
for item in lst:
|
||||
if item not in freq:
|
||||
freq[item] = 1
|
||||
else:
|
||||
freq[item] += 1
|
||||
|
||||
sorted_items = sorted(freq.items(), key=lambda x: x[1], reverse=True)
|
||||
for item, count in sorted_items[0:9]:
|
||||
logging.info(f"{item:10}: {count}")
|
||||
|
||||
logging.info("top 10 tags")
|
||||
count_items(tags)
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(description="TinyTapeout configuration and docs")
|
||||
|
||||
with open('config.yaml') as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
# default max projects is row * col - 2 (scan controller takes first 2 slots)
|
||||
config['num_projects'] = config['layout']['rows'] * config['layout']['cols'] - 2
|
||||
|
||||
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)
|
||||
parser.add_argument('--add-extra-project', help="specify a github url to add. Project is added to the extra_projects list specifed in config.yaml")
|
||||
|
||||
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)
|
||||
|
||||
if args.update_orders:
|
||||
# don't instantiate orders unless we have to as it needs stripe token
|
||||
orders = Orders(config)
|
||||
orders.fetch_orders()
|
||||
orders.update_project_list()
|
||||
|
||||
if args.add_extra_project:
|
||||
orders = Orders(config)
|
||||
orders.add_extra_project(args.add_extra_project)
|
||||
|
||||
projects = Projects(config, args)
|
||||
|
||||
docs = Docs(projects.projects, args=args)
|
||||
caravel = CaravelConfig(config, projects.projects, num_projects=args.limit_num_projects)
|
||||
|
||||
if args.list:
|
||||
caravel.list()
|
||||
|
||||
if args.metrics:
|
||||
projects.build_metrics()
|
||||
|
||||
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()
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import discord
|
||||
import yaml
|
||||
import re
|
||||
|
||||
TOKEN = os.environ['DISCORD_TOKEN']
|
||||
TARG = os.environ['TARGET_SERVER']
|
||||
ROLE_ID = os.environ['ROLE_ID']
|
||||
|
||||
intents = discord.Intents.default()
|
||||
intents.members = True
|
||||
|
||||
client = discord.Client(intents=intents)
|
||||
exit_code = 0
|
||||
|
||||
@client.event
|
||||
async def on_ready():
|
||||
try:
|
||||
print(f'{client.user} has connected to Discord!')
|
||||
server = None
|
||||
for g in client.guilds:
|
||||
if g.name == TARG:
|
||||
server = g
|
||||
break
|
||||
if server == None:
|
||||
print(f'Error: Bot is not in a server named {TARG}')
|
||||
exit_code = 1
|
||||
await client.close()
|
||||
|
||||
role = server.get_role(int(ROLE_ID))
|
||||
if not role:
|
||||
print('Error: requested role does not exist on server!')
|
||||
exit_code = 1
|
||||
await client.close()
|
||||
|
||||
participants = []
|
||||
for project in os.listdir('./projects'):
|
||||
info_file = os.path.join('projects', project, 'info.yaml')
|
||||
if not os.path.exists(info_file):
|
||||
print(f'info.yaml does not exist for project {project}. Skipping.')
|
||||
else:
|
||||
try:
|
||||
stream = open(info_file, 'r')
|
||||
data = yaml.load(stream, Loader=yaml.FullLoader)
|
||||
stream.close()
|
||||
dname = data['documentation']
|
||||
if 'discord' in dname:
|
||||
dname = dname['discord']
|
||||
parts = dname.split('#')
|
||||
if len(parts) != 2 or len(parts[0]) == 0 or not re.match('^[0-9]{4}$', parts[1]):
|
||||
print(f'Invalid format for discord username {dname} in project {project}. Skipping.')
|
||||
else:
|
||||
participants.append({'name': parts[0], 'discriminator': parts[1]})
|
||||
else:
|
||||
print(f'Project {project} has no discord username listed. Skipping.')
|
||||
except Exception as ex:
|
||||
print(f'Error parsing info.yaml for project {project}. Skipping.')
|
||||
print(f'Error was: {ex}')
|
||||
|
||||
for m in server.members:
|
||||
a = [d for d in participants if d['name'] == m.name and d['discriminator'] == m.discriminator]
|
||||
if len(a) != 0:
|
||||
if len([d for d in m.roles if d.id == role.id]) == 0:
|
||||
print(f'Giving the role to {m.name}#{m.discriminator}')
|
||||
await m.add_roles(role, reason='Automatic role assignment by GitHub actions pipeline.')
|
||||
|
||||
print('All done!')
|
||||
await client.close()
|
||||
except Exception as ex:
|
||||
print('Exception was thrown!')
|
||||
print(ex)
|
||||
exit_code = 1
|
||||
await client.close()
|
||||
|
||||
client.run(TOKEN)
|
||||
sys.exit(exit_code)
|
||||
@@ -0,0 +1,22 @@
|
||||
# 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
|
||||
* Jeremy Birch for help with STA
|
||||
* [Aisler](https://aisler.net/) for sponsoring PCB development
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
# 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](verilog/rtl/scanchain/scanchain.v).
|
||||
|
||||

|
||||
|
||||
## Scan chain changes since TT02
|
||||
|
||||
After doing more simulation, we found that the 500 buffers in the scanchain
|
||||
were distorting the clock pulse to the point where we needed to further slow
|
||||
the clock. For more information [watch this video](https://youtu.be/eQxAseGVMoo).
|
||||
|
||||
Sylvain 'tnt' Munaut suggested a change to the scanchain that prevents the distortion
|
||||
and reduces the number of buffers:
|
||||
|
||||
The main change to the `scanchain` block is related to the clocking and the
|
||||
data output. Previously, you'd have something like this :
|
||||
|
||||
clk_in ---|>----|>---- clk_out
|
||||
|
|
||||
'------- Internal FFs
|
||||
|
||||
That is, you had an input buffer receving clock from the previous block and
|
||||
sending it to all the scan chain FF and to a strong output buffer that would
|
||||
send it to the next block.
|
||||
|
||||
The issues that were identified there are :
|
||||
|
||||
- 2 buffers per block means 500 buffers in the whole chain and the more
|
||||
stages, the more delay.
|
||||
|
||||
- The buffers don't have symmetrical rise/fall time. So each buffer would
|
||||
slightly alter the pulse width. Multiply that by 500 buffers and the shape
|
||||
at the end of the scan chain would be nowhere close to what was fed in.
|
||||
|
||||
The new block is like this :
|
||||
|
||||
clk_in ---|>o--------- clk_out
|
||||
|
|
||||
'-|>o--- Internal FFs
|
||||
|
||||
You have a single inverter that receives the clock and sends it inverted to
|
||||
the next block. That inverted clock is also fed to another small inverter
|
||||
whose output will be used to clock all the internal effects.
|
||||
|
||||
The result is that we have half as many stages in the chain of inverter.
|
||||
And also because the clock is inverted at each stage, any change in duty
|
||||
cycle that one stage causes on the clock pulses is "undone" by the next.
|
||||
It's not perfect of course since they are not perfectly match, but it's
|
||||
MUCH MUCH better.
|
||||
|
||||
Of course this required some other changes to deal with the fact the clock
|
||||
is inverted at the output.
|
||||
|
||||
The first change is in scanchain block itself. Previously we'd have a negative
|
||||
edge FF at the output so that the output clock rising edge would happen in
|
||||
the middle of the output data valid window. Now because the clock is inverted,
|
||||
this has the same effect by itself. We still need a FF though (else the last
|
||||
bit of each block would always be reflected to the first bit of the next), but
|
||||
now it needs to be a rising edge FF.
|
||||
|
||||
The second change is in the scan controller since the drive waveform needs
|
||||
to be slightly different. Every two stages, a clock pulse is "lost" effectively
|
||||
and so an extra pulse must be injected to keep moving the data long.
|
||||
|
||||
### 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 17 times to deliver the data to the DUT (new scanchain needs 1 extra clock for every odd numbered project in the chain)
|
||||
* 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.5 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.
|
||||
* Generate the trace yourself by running `make test_single` in `verilog/dv/scan_controller`. See [VERIFICATION.md](VERIFICATION.md) for more info on running tests.
|
||||
|
||||
## 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,25 @@
|
||||
[](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/gds.yaml)
|
||||
[](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/verification.yaml)
|
||||
[](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/tt_datasheet.yaml)
|
||||
[](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/gl_verification.yaml)
|
||||
[](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/formal.yaml)
|
||||
[](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/sta.yaml)
|
||||
|
||||
# TinyTapeout 03
|
||||
|
||||
* 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
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# Toplevel STA and Spice analysis for TinyTapeout-02 and 03.
|
||||
|
||||
Contributed by J. Birch 22 March 2023
|
||||
|
||||
## Introduction
|
||||
|
||||
TinyTapeout is built using the OpenLane flow and consists of three major components:
|
||||
|
||||
1) up to 250 user designs with a common interface, each of which has been composed using the OpenLane flow,
|
||||
2) a scanchain block that is instanced for each user design to provide input data and to sink output data,
|
||||
3) a scan chain controller.
|
||||
|
||||
The OpenLane flow allows for timing analysis of each of these individual blocks but does not provide for
|
||||
analysis of the assembly of them, which leaves potential holes in timing that could cause failures.
|
||||
|
||||
This work allows a single design to be assembled out of the sub-blocks and this can then be submitted to
|
||||
STA for analysis.
|
||||
|
||||
In addition, an example critical path (for the clock that is passed along the scanchain) is created using
|
||||
a Python script so that it can be run through SPICE.
|
||||
|
||||
## GitHub action
|
||||
|
||||
The [STA github action](.github/workflows/sta.yaml) automatically runs the STA when the repository is updated.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
To run STA, the following files need to be available:
|
||||
|
||||
1) a gate level verilog netlist of the top level design (verilog/rtl/user_project_wrapper.v)
|
||||
2) a gate level verilog netlist for each sub-block (verilog/rtl/)
|
||||
3) a SPEF format parasitics file for the top level wiring (spef/)
|
||||
4) a SPEF format parasitics file for each sub-block (spef/)
|
||||
5) the relevant SkyWater libraries for STA analysis - installed with the PDK
|
||||
6) OpenSTA 2.4.0 (or later) needs to be on the PATH. rundocker.sh shows how to use STA included in the OpenLane docker
|
||||
7) python3.9 or later
|
||||
8) verilog parser (pip3 install verilog-parser)
|
||||
9) a constraints file (sta_top/top.sdc)
|
||||
|
||||
## Assumptions
|
||||
|
||||
* all of the Verilog files are in a single directory
|
||||
* all of the SPEF files are in a single directory
|
||||
|
||||
## Issues
|
||||
|
||||
The Verilog parser has two bugs: it does not recognise 'inout' and it does not cope
|
||||
with escaped names (starting with a '\'). Locally this has been fixed but to use the
|
||||
off-the-shelf parser we preprocess the file to change inout to input and remove the escapes and
|
||||
substitute the [nnn] with _nnn_ in the names
|
||||
|
||||
## Invoking STA analysis
|
||||
|
||||
sta_top/toplevel_sta.py <path to verilog> <path to spef> <sdc>
|
||||
|
||||
eg:
|
||||
|
||||
sta_top/toplevel_sta.py ./verilog/gl/user_project_wrapper.v
|
||||
./spef/user_project_wrapper.spef ./sta_top/top.sdc > sta.log
|
||||
|
||||
Alternatively you can enter interactive mode after analysis:
|
||||
|
||||
sta_top/toplevel_sta.py ./verilog/gl/user_project_wrapper.v
|
||||
./spef/user_project_wrapper.spef ./sta_top/top.sdc -i
|
||||
|
||||
## How it works
|
||||
|
||||
* preprocesses the main Verilog
|
||||
* parses the main Verilog to find which modules are used
|
||||
* creates a new merged Verilog containing all the module netlists and the main netlist
|
||||
* creates a tcl script in the spef directory to load the relevant spefs for each instanced module in the main verilog
|
||||
* creates all_sta.tcl in the verilog/gl directory that does the main STA steps (loading design, loading constraints,
|
||||
running analyses) - this mimics what OpenLane does
|
||||
* creates a script sta.sh in the verilog/gl directory which sets up the environment and invokes STA to run all_sta.tcl
|
||||
* runs sta.sh
|
||||
|
||||
## Invoking Spice simulation:
|
||||
|
||||
### Important notes
|
||||
|
||||
* This uses at least ngspice-34, tested on ngspice-39, which has features to increase speed
|
||||
and reduce memory image when running with the SkyWater spice models. In addition the following
|
||||
needs to be set in ~/spice.rc and/or ~/.spiceinit:
|
||||
|
||||
set ngbehavior=hs
|
||||
|
||||
* Note the spelling of ngbehavior (no u). If this is not set then the simulation will run out of memory]
|
||||
* If the critical path changes then the spice_path.py script will need to be adapted to follow suit
|
||||
|
||||
### Instructions
|
||||
|
||||
* go to where the included spice files will be found
|
||||
|
||||
cd $PDK_ROOT/sky130A/libs.tech/ngspice
|
||||
|
||||
* run the script to get the critical path spice circuit - 250 stages of the clock
|
||||
|
||||
<repo directory>/sta_top/spice_path.py path.spice
|
||||
|
||||
* invoke spice (takes about 2 minutes)
|
||||
|
||||
ngspice path.spice
|
||||
|
||||
* run the sim (takes about 2 minutes)
|
||||
|
||||
tran 1n 70n
|
||||
|
||||
* show the results
|
||||
|
||||
plot i0 i250
|
||||
|
||||
This chart shows the input clock and how it changes after 250 blocks. It was simulated for 200n to capture a clean pair of clock pulses.
|
||||
|
||||

|
||||
|
||||
@@ -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,25 @@
|
||||
---
|
||||
title: |
|
||||
{width=15cm}
|
||||
Tiny Tapeout 03 Datasheet
|
||||
subtitle: Project Repository [https://github.com/TinyTapeout/tinytapeout-03](https://github.com/TinyTapeout/tinytapeout-03)
|
||||
documentclass: scrartcl
|
||||
date: \today{}
|
||||
geometry: "left=2cm,right=2cm,top=2cm,bottom=2.5cm"
|
||||
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-03-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,84 @@
|
||||
---
|
||||
title: "Tiny Tapeout 03"
|
||||
weight: 20
|
||||
description: 249 designs, closed 24th April 2023
|
||||
---
|
||||
|
||||
{{% toc %}}
|
||||
|
||||
# Design details
|
||||
|
||||
* [Github repository](https://github.com/TinyTapeout/tinytapeout-03)
|
||||
* [PDF datasheet](https://github.com/TinyTapeout/tinytapeout-03/raw/main/datasheet.pdf)
|
||||
* [Verification](https://github.com/TinyTapeout/tinytapeout-03/blob/main/VERIFICATION.md)
|
||||
* [Efabless MPW submission](https://platform.efabless.com/projects/1971)
|
||||
|
||||
# Launch stats
|
||||
|
||||
* Launched: 1 March 2023
|
||||
* Submission closed: 24 April 2023
|
||||
* Submitted to Efabless 2304C chipIgnite shuttle using Skywater 130nm open source PDK
|
||||
|
||||
# Project statistics
|
||||
|
||||
* 100 projects submitted, 149 projects added from [TT02](/runs/tt02)
|
||||
* build time for all projects 22.68 hours
|
||||
* total cells 79674
|
||||
* max cells 1287 for [project 072](./072)
|
||||
* max utilisation 59.95% for [project 044](./044)
|
||||
* total wire length 4079 mm
|
||||
* 94 used Wokwi, 135 Verilog, 1 myhdl, 7 Amaranth, spade 1, xls 2, migen 1, systemverilog 3, mixed radix circuit synthesis (mrcs) 1, chisel 1.
|
||||
* top 10 tags:
|
||||
- test : 21
|
||||
- experiment: 6
|
||||
- cpu : 6
|
||||
- timer : 5
|
||||
- risc : 5
|
||||
- game : 4
|
||||
- pwm : 4
|
||||
- music : 4
|
||||
- counter : 4
|
||||
|
||||
# Whole die render
|
||||
|
||||

|
||||
|
||||
# Project Showcase
|
||||
|
||||
* [12 bit PDP8](./019)
|
||||
* [Prime detector](./031)
|
||||
* [Pulse density modulators](./033)
|
||||
* [Most of the Sky130 cells](./045)
|
||||
* [Synthesizable Digital Temperature Sensor](./047)
|
||||
* [Neptune guitar tuner](./052)
|
||||
* [MSF clock](./066)
|
||||
* [Ring oscillator temperature sensor](./058)
|
||||
* [Locked QTCore-A1](./072)
|
||||
|
||||
# Testimonials / Endorsements
|
||||
|
||||
Awesome! Sent my design to #TinyTapeout ! Emotion! Thanks
|
||||
@matthewvenn , @marunmagesh and crew, for making it possible!
|
||||
|
||||
---
|
||||
|
||||
#tinytapeout 3 submission is in! I'm very thankful to be a part of such an amazing project, even if my project is small.
|
||||
|
||||
---
|
||||
|
||||
Just to take moment, thank you! I didn't think I would ever get to do something like this, especially with so little cost or pressure.
|
||||
For context, at work, I was testing an ASIC a supplier designed for us, and that was quite the opposite of both of these.
|
||||
|
||||
---
|
||||
|
||||
Hurray, also made a last minute submission, haven't felt that rush since my last university homework submission deadline... Great that a chip design can be done on a weekend, looking forward to getting all the other designs, and thanks everybody who contributed to the platform, it is really easy to use!
|
||||
|
||||
---
|
||||
|
||||
I just submitted a last minute project to tt03, wanted to drop by and say kudos for all the resources and the project in general!
|
||||
|
||||
I've never done any hardware before (just software), but after reading the updated Code by Petzold earlier in the year and listening to an interview with Jim Keller I had my interest peeked to look at whats going on in the layers I usually take for granted. Really cool to find something like tiny tapeout, nice motivating to actually install some of the tools and give it a try.
|
||||
|
||||
---
|
||||
More on [Twitter](https://twitter.com/search?q=tinytapeout).
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
hidden: true
|
||||
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]} |
|
||||
+145
-69
@@ -1,84 +1,160 @@
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import os
|
||||
import glob
|
||||
import gdstk
|
||||
import cairosvg
|
||||
import logging
|
||||
import subprocess
|
||||
import shutil
|
||||
import json
|
||||
|
||||
|
||||
# doc check
|
||||
# makes sure that the basic info is present
|
||||
def check_yaml_docs(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)
|
||||
class Docs():
|
||||
|
||||
# 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)
|
||||
def __init__(self, projects, args):
|
||||
self.projects = projects
|
||||
self.args = args
|
||||
self.script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
# stuff related to docs
|
||||
def build_index(self):
|
||||
logging.info("building doc index")
|
||||
with open(os.path.join(self.script_dir, 'docs', 'README_init.md')) as fh:
|
||||
readme = fh.read()
|
||||
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())
|
||||
|
||||
# use pandoc to create a single page PDF preview
|
||||
def create_pdf(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()
|
||||
def update_image(self):
|
||||
ruby = os.path.join(self.script_dir, 'caravel_template', 'dump_pic.rb')
|
||||
klayoutrc = os.path.join(self.script_dir, 'caravel_template', 'klayoutrc')
|
||||
lyp = os.path.join(self.script_dir, 'caravel_template', 'caravel.lyp')
|
||||
cmd = f'klayout -l {lyp} gds/user_project_wrapper.gds* -r {ruby} -c {klayoutrc}'
|
||||
logging.info(cmd)
|
||||
os.system(cmd)
|
||||
|
||||
with open('datasheet.md', 'w') as fh:
|
||||
fh.write(doc_header)
|
||||
# handle pictures
|
||||
yaml['picture_link'] = ''
|
||||
if yaml['picture']:
|
||||
picture_name = yaml['picture']
|
||||
# create a json file of all the project info, this is then used by tinytapeout.com to show projects
|
||||
def dump_json(self):
|
||||
designs = []
|
||||
for project in self.projects:
|
||||
design = project.get_yaml()
|
||||
designs.append(design)
|
||||
|
||||
# check it's actually there
|
||||
if not os.path.exists(picture_name):
|
||||
logging.warning(f"picture {picture_name} not found")
|
||||
exit(1)
|
||||
with open(self.args.dump_json, 'w') as fh:
|
||||
fh.write(json.dumps(designs, indent=4))
|
||||
logging.info(f'wrote json to {self.args.dump_json}')
|
||||
|
||||
# and not SVG
|
||||
if picture_name.endswith('.svg'):
|
||||
logging.warning("svg not supported")
|
||||
exit(1)
|
||||
def dump_markdown(self):
|
||||
with open(os.path.join(self.script_dir, 'docs', 'doc_header.md')) as fh:
|
||||
doc_header = fh.read()
|
||||
|
||||
# create the link
|
||||
yaml['picture_link'] = ''.format(picture_name)
|
||||
with open(os.path.join(self.script_dir, 'docs', 'doc_template.md')) as fh:
|
||||
doc_template = fh.read()
|
||||
|
||||
# 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")
|
||||
with open(os.path.join(self.script_dir, 'docs', 'INFO.md')) as fh:
|
||||
doc_info = fh.read()
|
||||
|
||||
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")
|
||||
exit(1)
|
||||
with open(os.path.join(self.script_dir, 'docs', 'VERIFICATION.md')) as fh:
|
||||
doc_verification = fh.read()
|
||||
|
||||
with open(os.path.join(self.script_dir, 'docs', 'STA.md')) as fh:
|
||||
doc_sta = fh.read()
|
||||
|
||||
# SVG and PNG renders of the GDS
|
||||
def create_svg(args):
|
||||
gds = glob.glob(os.path.join(args.run_dir, 'results/final/gds/*gds'))
|
||||
library = gdstk.read_gds(gds[0])
|
||||
top_cells = library.top_level()
|
||||
top_cells[0].write_svg('gds_render.svg')
|
||||
with open(os.path.join(self.script_dir, 'docs', 'CREDITS.md')) as fh:
|
||||
doc_credits = fh.read()
|
||||
|
||||
if args.create_png:
|
||||
cairosvg.svg2png(url='gds_render.svg', write_to='gds_render.png')
|
||||
with open(self.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\clearpage\n")
|
||||
except IndexError:
|
||||
logging.warning("missing pins in info.yaml, skipping")
|
||||
|
||||
# ending
|
||||
fh.write(doc_info)
|
||||
fh.write("\n\clearpage\n")
|
||||
fh.write(doc_verification)
|
||||
fh.write("\n\clearpage\n")
|
||||
fh.write(doc_sta)
|
||||
fh.write("\n\clearpage\n")
|
||||
fh.write(doc_credits)
|
||||
|
||||
logging.info(f'wrote markdown to {self.args.dump_markdown}')
|
||||
|
||||
if self.args.dump_pdf:
|
||||
pdf_cmd = f'pandoc --toc --toc-depth 2 --pdf-engine=xelatex -i {self.args.dump_markdown} -o {self.args.dump_pdf}'
|
||||
logging.info(pdf_cmd)
|
||||
p = subprocess.run(pdf_cmd, shell=True)
|
||||
if p.returncode != 0:
|
||||
logging.error("pdf command failed")
|
||||
|
||||
def build_hugo_content(self):
|
||||
hugo_root = self.args.build_hugo_content
|
||||
hugo_images = os.path.join(hugo_root, 'images')
|
||||
shutil.rmtree(hugo_root)
|
||||
os.makedirs(hugo_root)
|
||||
os.makedirs(hugo_images)
|
||||
|
||||
with open(os.path.join(self.script_dir, 'docs', 'hugo_template.md')) as fh:
|
||||
doc_template = fh.read()
|
||||
|
||||
with open(os.path.join(self.script_dir, 'docs', 'hugo_index_template.md')) as fh:
|
||||
index_template = fh.read()
|
||||
|
||||
# copy image
|
||||
shutil.copyfile('pics/tinytapeout_numbered.png', os.path.join(hugo_images, 'tinytapeout-03.png')) # TODO fix hardcoded run
|
||||
|
||||
# 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)
|
||||
|
||||
+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
|
||||
@@ -1 +1 @@
|
||||
flake8 --ignore E203,E123,E501,E221,E401,W605 documentation.py project.py tt_tool.py reports.py
|
||||
flake8 --ignore E203,E123,E501,E221,E401,W605 configure.py project.py tt_tool.py orders.py documentation.py caravel.py
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import stripe, os
|
||||
import time
|
||||
from datetime import datetime
|
||||
import yaml
|
||||
import requests
|
||||
import pytz
|
||||
import logging
|
||||
|
||||
|
||||
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):
|
||||
orders = self.fetch_stripe_orders() + self.fetch_university_orders() + self.fetch_extra_orders()
|
||||
logging.info(f"total = {len(orders)}")
|
||||
self.orders = sorted(orders, key=lambda d: d['time'])
|
||||
|
||||
def add_extra_project(self, git_url):
|
||||
try:
|
||||
with open(self.config['extra_projects']) as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
except FileNotFoundError as e:
|
||||
config = []
|
||||
|
||||
config.append({'project': {'url' : git_url, 'timestamp' : time.time()}})
|
||||
with open(self.config['extra_projects'], 'w') as fh:
|
||||
yaml.safe_dump(config, fh)
|
||||
logging.info(f"added {git_url} to {self.config['extra_projects']}")
|
||||
|
||||
def fetch_extra_orders(self):
|
||||
with open(self.config['extra_projects']) as fh:
|
||||
config = yaml.safe_load(fh)
|
||||
|
||||
orders = []
|
||||
for project in config:
|
||||
orders.append({
|
||||
'git_url' : project['project']['url'],
|
||||
'email' : None,
|
||||
'time' : datetime.fromtimestamp(project['project']['timestamp']).replace(tzinfo=pytz.UTC)
|
||||
})
|
||||
|
||||
logging.info(f"fetched extra {len(orders)} orders")
|
||||
return orders
|
||||
|
||||
def fetch_university_orders(self):
|
||||
name = self.config['name']
|
||||
url = f'https://app.tinytapeout.com/api/submissions?shuttle={name}'
|
||||
r = requests.get(url)
|
||||
if r.status_code != 200:
|
||||
logging.warning("couldn't download {}".format(url))
|
||||
exit(1)
|
||||
|
||||
orders = []
|
||||
for item in r.json()['items']:
|
||||
order = {
|
||||
'git_url' : item['repo'],
|
||||
'email' : None,
|
||||
'time' : datetime.strptime(item['time'], '%Y-%m-%dT%H:%M:%S.%f%z').replace(tzinfo=pytz.UTC)
|
||||
}
|
||||
if order['time'] > self.open_date.replace(tzinfo=pytz.UTC):
|
||||
orders.append(order)
|
||||
logging.info(f"fetched university {len(orders)} orders")
|
||||
return orders
|
||||
|
||||
def fetch_stripe_orders(self):
|
||||
start_id = None
|
||||
after_open_date = True
|
||||
orders = []
|
||||
refunded_orders = []
|
||||
|
||||
while after_open_date:
|
||||
refunds = stripe.Refund.list(limit=10, starting_after=start_id)
|
||||
for refund in refunds:
|
||||
created = datetime.fromtimestamp(refund['created'])
|
||||
if created < self.open_date:
|
||||
after_open_date = False
|
||||
|
||||
refunded_orders.append(refund['payment_intent'])
|
||||
start_id = refund['id']
|
||||
|
||||
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']
|
||||
|
||||
if checkout['payment_intent'] in refunded_orders:
|
||||
logging.warning(f"found a refunded order! {git_url}")
|
||||
continue
|
||||
|
||||
order = {
|
||||
'git_url' : git_url,
|
||||
'email' : checkout['customer_details']['email'],
|
||||
'time' : datetime.fromtimestamp(checkout['created']).replace(tzinfo=pytz.UTC),
|
||||
}
|
||||
orders.append(order)
|
||||
|
||||
# pagination
|
||||
start_id = checkout['id']
|
||||
|
||||
# put in date order
|
||||
logging.info(f"fetched stripe {len(orders)} orders")
|
||||
return 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))
|
||||
+480
-138
@@ -1,144 +1,128 @@
|
||||
import yaml
|
||||
import logging
|
||||
import requests
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
import glob
|
||||
import os
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
import gdstk
|
||||
import cairosvg
|
||||
import csv
|
||||
import json
|
||||
import git_utils
|
||||
import git
|
||||
import shutil
|
||||
|
||||
# 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):
|
||||
with open(args.yaml, "r") as stream:
|
||||
return (yaml.safe_load(stream))
|
||||
class Project():
|
||||
|
||||
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 print_wokwi_id(yaml):
|
||||
print(yaml['project']['wokwi_id'])
|
||||
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 create_user_config(yaml):
|
||||
logging.info("creating include file")
|
||||
sources = get_project_source(yaml)
|
||||
top_module = get_top_module(yaml)
|
||||
def check_ports(self):
|
||||
top = self.get_macro_name()
|
||||
sources = [os.path.join(self.local_dir, 'src', src) for src in self.src_files]
|
||||
source_list = " ".join(sources)
|
||||
|
||||
if top_module == 'top':
|
||||
logging.error("top module cannot be called top - prepend your repo name to make it unique")
|
||||
exit(1)
|
||||
|
||||
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):
|
||||
logging.info("trying to download {}".format(url))
|
||||
r = requests.get(url)
|
||||
if r.status_code != 200:
|
||||
logging.warning("couldn't download {}".format(url))
|
||||
return False
|
||||
|
||||
if len(r.content) == 0:
|
||||
logging.warning("file was 0 bytes {}".format(url))
|
||||
return False
|
||||
|
||||
with open(filename, 'wb') as fh:
|
||||
logging.info("written to {}".format(filename))
|
||||
fh.write(r.content)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def fetch_wokwi_file(wokwi_id: int, file_requested: str,
|
||||
destination_name: str = None):
|
||||
'''
|
||||
fetch_wokwi_file -- perform a request on the wokwi api to
|
||||
retrieve a file from a given project.
|
||||
|
||||
@param wokwi_id: project id (integer)
|
||||
@param file_requested: the specific file requested for the
|
||||
project, e.g. verilog or diagram.json
|
||||
@param destination_name: file to write out to with result
|
||||
'''
|
||||
if destination_name is None or not len(destination_name):
|
||||
destination_name = file_requested
|
||||
|
||||
url = f'https://wokwi.com/api/projects/{wokwi_id}/{file_requested}'
|
||||
return fetch_file(url, destination_name)
|
||||
|
||||
|
||||
def install_wokwi_testing(wokwi_id: int, destination_dir: str = 'src',
|
||||
resource_dir: str = None):
|
||||
|
||||
wokwi_id_str = str(wokwi_id)
|
||||
|
||||
if resource_dir is None or not len(resource_dir):
|
||||
resource_dir = os.path.join(os.path.dirname(__file__), 'testing')
|
||||
|
||||
# directories in testing/lib to copy over
|
||||
pyLibsDir = os.path.join(resource_dir, 'lib')
|
||||
|
||||
# src template dir
|
||||
srcTplDir = os.path.join(resource_dir, 'src-tpl')
|
||||
|
||||
for libfullpath in glob.glob(os.path.join(pyLibsDir, '*')):
|
||||
logging.info(f'Copying {libfullpath} to {destination_dir}')
|
||||
shutil.copytree(
|
||||
libfullpath,
|
||||
os.path.join(destination_dir, os.path.basename(libfullpath)),
|
||||
dirs_exist_ok=True)
|
||||
|
||||
for srcTemplate in glob.glob(os.path.join(srcTplDir, '*')):
|
||||
with open(srcTemplate, 'r') as f:
|
||||
contents = f.read()
|
||||
customizedContents = re.sub('WOKWI_ID', wokwi_id_str, contents)
|
||||
outputFname = os.path.basename(srcTemplate)
|
||||
with open(os.path.join(destination_dir, outputFname), 'w') as outf:
|
||||
logging.info(f'writing src tpl to {outputFname}')
|
||||
outf.write(customizedContents)
|
||||
outf.close()
|
||||
|
||||
|
||||
def get_project_source(yaml):
|
||||
# wokwi_id must be an int or 0
|
||||
try:
|
||||
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:
|
||||
src_file = f'user_module_{wokwi_id}.v'
|
||||
if not fetch_wokwi_file(wokwi_id, 'verilog', os.path.join("src", src_file)):
|
||||
logging.error(f'Could not fetch verilog file for wokwi project {wokwi_id}')
|
||||
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)
|
||||
|
||||
# also fetch the wokwi diagram
|
||||
if not fetch_wokwi_file(wokwi_id, 'diagram.json', os.path.join("src", 'wokwi_diagram.json')):
|
||||
logging.error(f'Could not fetch diagram.json file for wokwi project {wokwi_id}')
|
||||
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 check_num_cells(self):
|
||||
num_cells = self.get_cell_count_from_synth()
|
||||
if self.is_hdl():
|
||||
if num_cells < 20:
|
||||
logging.warning(f"{self} only has {num_cells} cells")
|
||||
else:
|
||||
if num_cells < 11:
|
||||
logging.warning(f"{self} only has {num_cells} cells")
|
||||
|
||||
def is_fill(self):
|
||||
return self.fill
|
||||
|
||||
def is_wokwi(self):
|
||||
if self.wokwi_id != 0:
|
||||
return True
|
||||
|
||||
def is_hdl(self):
|
||||
return not self.is_wokwi()
|
||||
|
||||
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)
|
||||
|
||||
# attempt to download the *optional* truthtable for the project
|
||||
if fetch_wokwi_file(wokwi_id, 'truthtable.md', os.path.join('src', 'truthtable.md')):
|
||||
logging.info(f'Wokwi project {wokwi_id} has a truthtable included, will test!')
|
||||
install_wokwi_testing(wokwi_id)
|
||||
# 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']
|
||||
|
||||
# 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")
|
||||
exit(1)
|
||||
|
||||
source_files = yaml['project']['source_files']
|
||||
source_files = self.yaml['project']['source_files']
|
||||
if source_files is None:
|
||||
logging.error("must be more than 1 source file")
|
||||
exit(1)
|
||||
@@ -147,35 +131,393 @@ def get_project_source(yaml):
|
||||
logging.error("must be more than 1 source file")
|
||||
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")
|
||||
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:
|
||||
if '*' in filename:
|
||||
logging.error("* not allowed, please specify each file")
|
||||
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")
|
||||
exit(1)
|
||||
|
||||
return source_files
|
||||
|
||||
def get_yaml(self):
|
||||
return self.yaml
|
||||
|
||||
def get_top_module(yaml):
|
||||
wokwi_id = int(yaml['project']['wokwi_id'])
|
||||
if wokwi_id != 0:
|
||||
return "user_module_{}".format(wokwi_id)
|
||||
else:
|
||||
return yaml['project']['top_module']
|
||||
def get_hugo_row(self):
|
||||
return f'| {self.index} | [{self.yaml["documentation"]["title"]}]({self.index :03}) | {self.yaml["documentation"]["author"]}|\n'
|
||||
|
||||
# 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 harden():
|
||||
# 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)
|
||||
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"(?:^|[\s])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 get_git_remote(self):
|
||||
return list(git.Repo(self.local_dir).remotes[0].urls)[0]
|
||||
|
||||
def clone(self):
|
||||
if os.path.exists(self.local_dir):
|
||||
git_remote = self.get_git_remote()
|
||||
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.debug("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 __str__(self):
|
||||
"""
|
||||
if self.args.log_email:
|
||||
return f"[{self.index:03} : {self.email} : {self.git_url}]"
|
||||
else:
|
||||
"""
|
||||
return f"[{self.index:03} : {self.git_url}]"
|
||||
|
||||
def fetch_gds(self):
|
||||
git_utils.install_artifacts(self.git_url, self.local_dir)
|
||||
|
||||
def get_latest_action_url(self):
|
||||
return git_utils.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"
|
||||
|
||||
def get_macro_spef_filename(self):
|
||||
return f"{self.top_module}.spef"
|
||||
|
||||
# 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
|
||||
|
||||
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/spef/{self.get_macro_spef_filename()}", f"spef/{self.get_macro_spef_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")
|
||||
src_file = self.src_files[0]
|
||||
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):
|
||||
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 golden_harden(self):
|
||||
logging.info(f"hardening {self}")
|
||||
# copy golden config
|
||||
shutil.copyfile('golden_config.tcl', os.path.join(self.local_dir, 'src', 'config.tcl'))
|
||||
self.harden()
|
||||
|
||||
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_counts_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 = {}
|
||||
total = 0
|
||||
for cell_name in cell_count:
|
||||
cat_index = categories['map'][cell_name]
|
||||
cat_name = categories['categories'][cat_index]
|
||||
if cat_name in by_category:
|
||||
by_category[cat_name]['count'] += cell_count[cell_name]
|
||||
by_category[cat_name]['examples'].append(cell_name)
|
||||
else:
|
||||
by_category[cat_name] = {'count' : cell_count[cell_name], 'examples' : [cell_name]}
|
||||
|
||||
if cat_name not in ['Fill', 'Tap']:
|
||||
total += cell_count[cell_name]
|
||||
|
||||
print('# Cell usage by Category')
|
||||
print()
|
||||
print('| Category | Cells | Count |')
|
||||
print('|---------------|----------|-------|')
|
||||
for cat_name, cat_dict in sorted(by_category.items(), key=lambda x: x[1]['count'], reverse=True):
|
||||
cell_links = [f'[{name}]({CELL_URL}{name})' for name in cat_dict['examples']]
|
||||
print(f'|{cat_name} | {" ".join(cell_links)} | {cat_dict["count"]}|')
|
||||
|
||||
print(f'## {total} total cells (excluding fill and tap cells)')
|
||||
|
||||
# get cell count from synth report
|
||||
def get_cell_count_from_synth(self):
|
||||
num_cells = 0
|
||||
try:
|
||||
yosys_report = glob.glob(f'{self.local_dir}/runs/wokwi/reports/synthesis/1-synthesis.*0.stat.rpt')[0] # can't open a file with \ in the path
|
||||
with open(yosys_report) as fh:
|
||||
for line in fh.readlines():
|
||||
m = re.search(r'Number of cells:\s+(\d+)', line)
|
||||
if m is not None:
|
||||
num_cells = int(m.group(1))
|
||||
|
||||
except IndexError:
|
||||
logging.warning(f"couldn't open yosys cell report for cell checking {self}")
|
||||
|
||||
return num_cells
|
||||
|
||||
# Parse the lib, cell and drive strength an OpenLane gate-level Verilog file
|
||||
def get_cell_counts_from_gl(self):
|
||||
cell_count = {}
|
||||
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)
|
||||
|
||||
-115
@@ -1,115 +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_hd/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']
|
||||
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)
|
||||
@@ -1,4 +1,7 @@
|
||||
pytz
|
||||
gdstk
|
||||
stripe
|
||||
GitPython
|
||||
cairosvg
|
||||
requests
|
||||
PyYAML
|
||||
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
img = Image.open('pics/tinytapeout.png').convert('RGBA')
|
||||
overlay = Image.new('RGBA', img.size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(overlay)
|
||||
font = ImageFont.truetype("/usr/share/fonts/dejavu/DejaVuSans.ttf", 30)
|
||||
|
||||
# coordinate mapping
|
||||
xmap = ((88, 272), (2676, 1646)) # two pairs of (chip x coord, picture x coord)
|
||||
ymap = ((95, 1879), (3240, 213)) # two pairs of (chip y coord, picture y coord)
|
||||
xshift, yshift = 40, -43 # text position relative to macro origin
|
||||
|
||||
for line in open('openlane/user_project_wrapper/macro.cfg'):
|
||||
macro, x, y, orient = line.split()
|
||||
if macro == 'scan_controller':
|
||||
continue
|
||||
elif macro.startswith('scanchain_'):
|
||||
continue
|
||||
name, num = macro.rsplit('_', 1)
|
||||
num, x, y = int(num), int(x), int(y)
|
||||
x = int((x-xmap[0][0])/(xmap[1][0]-xmap[0][0])*(xmap[1][1]-xmap[0][1])+xmap[0][1])
|
||||
y = int((y-ymap[0][0])/(ymap[1][0]-ymap[0][0])*(ymap[1][1]-ymap[0][1])+ymap[0][1])
|
||||
x += xshift
|
||||
y += yshift
|
||||
msg = str(num)
|
||||
w, h = draw.textsize(msg, font=font)
|
||||
draw.text((x-w/2, y-h/2), msg, fill=(0, 0, 0, 255), font=font)
|
||||
|
||||
combined = Image.alpha_composite(img, overlay).convert('RGB')
|
||||
combined.save('pics/tinytapeout_numbered.png')
|
||||
+12
-17
@@ -2,18 +2,15 @@
|
||||
import sys
|
||||
import logging
|
||||
import argparse
|
||||
import documentation
|
||||
import project
|
||||
import reports
|
||||
from project import Project
|
||||
|
||||
if __name__ == '__main__':
|
||||
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('--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)
|
||||
|
||||
# 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-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)
|
||||
@@ -45,35 +42,33 @@ if __name__ == '__main__':
|
||||
ch.setFormatter(log_format)
|
||||
log.addHandler(ch)
|
||||
|
||||
project_yaml = project.load_yaml(args)
|
||||
project = Project(0, 'unknown', args.project_dir, args)
|
||||
project.post_clone_setup()
|
||||
|
||||
# handle the options
|
||||
if args.check_docs:
|
||||
documentation.check_yaml_docs(project_yaml)
|
||||
project.check_yaml_docs()
|
||||
|
||||
if args.print_cell_summary or args.print_cell_category:
|
||||
reports.summarize(args)
|
||||
|
||||
if args.create_cell_defs:
|
||||
reports.create_defs(args)
|
||||
project.summarize()
|
||||
|
||||
if args.print_stats:
|
||||
reports.print_stats(args)
|
||||
project.print_stats()
|
||||
|
||||
if args.print_warnings:
|
||||
reports.print_warnings(args)
|
||||
project.print_warnings()
|
||||
|
||||
if args.print_wokwi_id:
|
||||
project.print_wokwi_id(project_yaml)
|
||||
project.print_wokwi_id()
|
||||
|
||||
if args.create_user_config:
|
||||
project.create_user_config(project_yaml)
|
||||
project.create_user_config()
|
||||
|
||||
if args.harden:
|
||||
project.harden()
|
||||
|
||||
if args.create_pdf:
|
||||
documentation.create_pdf(project_yaml)
|
||||
project.create_pdf()
|
||||
|
||||
if args.create_svg or args.create_png:
|
||||
documentation.create_svg(args)
|
||||
project.create_svg()
|
||||
|
||||
Reference in New Issue
Block a user