This commit is contained in:
Matt Venn
2023-05-25 13:51:16 +02:00
30 changed files with 11068 additions and 340 deletions
+3
View File
@@ -0,0 +1,3 @@
*swp
setuptokens.sh
__pycache__
+250
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
RBA::Application.instance.main_window.current_view.save_image("./pics/tinytapeout.png",2000,2000)
+247
View File
@@ -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>
+109
View File
@@ -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
}
}
+4
View File
@@ -0,0 +1,4 @@
// end of module instantiation
endmodule // user_project_wrapper
`default_nettype wire
+84
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+22
View File
@@ -0,0 +1,22 @@
# Sponsored by
[![efabless](pics/efabless.png)](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
View File
@@ -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).
![block diagram](pics/block_diagram.png)
## 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
![update cycle](pics/update_cycle.png)
*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)```
+25
View File
@@ -0,0 +1,25 @@
[![tinytapeout](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/gds.yaml/badge.svg)](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/gds.yaml)
[![verification](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/verification.yaml/badge.svg)](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/verification.yaml)
[![tt_datasheet](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/tt_datasheet.yaml/badge.svg)](https://github.com/tinytapeout/tinytapeout-03/actions/workflows/tt_datasheet.yaml)
[![gl_verification](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/gl_verification.yaml/badge.svg)](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/gl_verification.yaml)
[![formal](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/formal.yaml/badge.svg)](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/formal.yaml)
[![sta](https://github.com/TinyTapeout/tinytapeout-03/actions/workflows/sta.yaml/badge.svg)](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
![tiny tapeout](pics/tinytapeout_numbered.png)
# Project Index
+115
View File
@@ -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.
![clock_spice.png](pics/clock_spice.png)
+149
View File
@@ -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
+25
View File
@@ -0,0 +1,25 @@
---
title: |
![](pics/ttlogo.png){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
![Full GDS](pics/tinytapeout_numbered.png)
\pagebreak
# Projects
+32
View File
@@ -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]} |
+28
View File
@@ -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
+84
View File
@@ -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
![whole die](images/tinytapeout-03.png)
# 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).
+39
View File
@@ -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
View File
@@ -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'] = '![picture]({})'.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'] = '![picture]({})'.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'![picture](images/{picture_basename})'
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
View File
@@ -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
View File
@@ -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
+137
View File
@@ -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
View File
@@ -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'] = '![picture]({})'.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
View File
@@ -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)
+3
View File
@@ -1,4 +1,7 @@
pytz
gdstk
stripe
GitPython
cairosvg
requests
PyYAML
Executable
+32
View File
@@ -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
View File
@@ -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()