blob: 7e270c1ff718013ece75d05c49b6baa12514fd2b [file] [log] [blame]
#!/bin/env python3
#-------------------------------------------------------------------------
# sky130_gen_sealring.py --- a seal ring generator for the Google/SkyWater
# sky130 PDK using magic.
#
# Because the seal ring contains many layers that do not appear in standard
# layout editing, they are all specially implemented in the sky130seal_ring.tech
# file in this directory.
#
# An example seal ring was generated by SkyWater and imported using magic's
# gdsquery.sh script. It was then hand-edited to contain only the bottom
# quarter. Then it was saved in .mag databases.
#
# The generator script sky130_gen_sealring.py calls magic using the
# sky130seal_ring.tech file, and automatically modifies the geometry to
# stretch to the half width and height of the specified dimensions. Then
# the lower-left cells are copied and folded over the centerline to make
# the complete seal ring. The seal ring is then written out in GDS format.
# Then a simplified magic view is generated in the usual user-facing sky130
# technology file, with the GDS_FILE property pointing to the seal ring GDS.
# This layout and GDS can then be imported into a layout.
#
# Note: All magic files are in base units of centimicrons. Because the
# manufacturing grid is 5nm, if the half-width of the seal ring is on 5nm,
# the seal ring quadrants will overlap at the center by 5nm, which is not
# an issue.
#
# Usage:
#
# sky130_gen_sealring.py width height target_dir [-force] [-outer] [-keep]
#
# Where:
# width = the full-chip layout width
# height = the full-chip layout height
# target_dir = location of the full-chip layout
#
# -force = overwrite any existing files at the target
# -outer = width and height are the seal ring outer edge, not the chip area
# -keep = keep local working directory of results
#
# Results:
# Files advSeal_6um_gen.mag and advSeal_6um_gen.gds are generated and placed in
# target_dir. advSeal_6um_gen.mag is an "abstract" view that represents the
# seal ring in diffusion and the nikon cross in metal1, and references
# the advSeal_6um_gen.gds file in the same directory as a GDS_FILE property.
#-------------------------------------------------------------------------
import subprocess
import shutil
import sys
import os
import re
def generate_sealring(width, height, target_dir, force, keep):
starting_dir=os.getcwd()
# Find directory where the script and relevant files exists
abspath = os.path.abspath(__file__)
script_dir = os.path.dirname(abspath)
# All files of interest are listed below.
script = 'generate_gds.tcl'
tech = 'sky130seal_ring.tech'
corner = 'seal_ring_corner.mag'
abstract = 'seal_ring_corner_abstract.mag'
slots = 'sealring_slots.mag'
array = 'seal_ring_slots_array.mag'
nikon = 'nikon_sealring_shape.mag'
polygons = ['sr_polygon00007.mag',
'sr_polygon00027.mag', 'sr_polygon00011.mag', 'sr_polygon00028.mag',
'sr_polygon00001.mag', 'sr_polygon00015.mag', 'sr_polygon00031.mag',
'sr_polygon00002.mag', 'sr_polygon00016.mag', 'sr_polygon00032.mag',
'sr_polygon00003.mag', 'sr_polygon00019.mag', 'sr_polygon00035.mag',
'sr_polygon00004.mag', 'sr_polygon00020.mag', 'sr_polygon00036.mag',
'sr_polygon00005.mag', 'sr_polygon00023.mag', 'sr_polygon00039.mag',
'sr_polygon00006.mag', 'sr_polygon00024.mag']
# Tries to create 'temp' directory
temp_dir='temp'
if os.path.exists('temp'):
print('temp/ directory already exists, trying to create /tmp/sky130_sealring...')
temp_dir='/tmp/sky130_sealring'
else:
try:
os.makedirs('temp')
except:
print('Couldn\'t create the temp/ directory, trying to create /tmp/sky130_sealring...')
temp_dir='/tmp/sky130_sealring'
# Tries to create /tmp/sky130_sealring if 'temp' is unavailable
if temp_dir!='temp':
try:
if os.path.exists(temp_dir):
error='Couldn\'t delete files from /tmp/sky130_sealring'
for filename in os.listdir(temp_dir):
file_path = os.path.join(temp_dir, filename)
os.unlink(file_path)
else:
error='Couldn\'t create /tmp/sky130_sealring'
os.makedirs(temp_dir)
except:
print(error)
sys.exit(1)
os.chdir(temp_dir)
# Copy all .mag files, .magicrc file, and sky130seal_ring.tech file to temp/
files_to_copy = polygons[:]
files_to_copy.append(nikon)
files_to_copy.append(slots)
files_to_copy.append(array)
files_to_copy.append(corner)
files_to_copy.append(abstract)
files_to_copy.append(tech)
files_to_copy.append(script)
for file in files_to_copy:
shutil.copy(script_dir +"/" + file, '.')
# Seal ring is placed 6um outside of the chip, so add 12um to width and height
fwidth = float(width) + 12
fheight = float(height) + 12
dbhwidth = round(fwidth * 100)
dbhheight = round(fheight * 100)
swidth = str(dbhwidth)
sheight = str(dbhheight)
swidthx5 = str(dbhwidth * 5)
dwidth = str(int(fwidth * 200))
dheight = str(int(fheight * 200))
# Modify every polygon to half width and height
for file in polygons:
with open(file, 'r') as ifile:
maglines = ifile.read().splitlines()
with open(file, 'w') as ofile:
for line in maglines:
newline = re.sub('51200', swidth, line)
newline = re.sub('51210', sheight, newline)
# NOTE: polygon 39 is at scale 10, not 2, due to
# corner positions of 45 degree angled geometry.
newline = re.sub('256000', swidthx5, newline)
print(newline, file=ofile)
# Abstract corner view gets the same treatment
qwidth = str(round(fwidth * 50))
qheight = str(round(fheight * 50))
with open(abstract, 'r') as ifile:
maglines = ifile.read().splitlines()
with open(abstract, 'w') as ofile:
for line in maglines:
newline = re.sub('25600', qwidth, line)
newline = re.sub('25605', qheight, newline)
print(newline, file=ofile)
# Slots arrays are recalculated to span the width and height
with open(array, 'r') as ifile:
maglines = ifile.read().splitlines()
slotsX = False
with open(array, 'w') as ofile:
for line in maglines:
newline = line
if 'slots_X' in line:
slotsX = True
elif 'array 0' in line:
if slotsX:
nslots = int((fwidth - 25.0) / 25.0) - 1
newline = 'array 0 ' + str(nslots) + ' 5000 0 0 430'
else:
nslots = int((fheight - 25.0) / 25.0) - 1
newline = 'array 0 ' + str(nslots) + ' 5000 0 0 430'
print(newline, file=ofile)
# Corner cell changes bounding boxes to half width and height.
with open(corner, 'r') as ifile:
maglines = ifile.read().splitlines()
slotsX = False
with open(corner, 'w') as ofile:
for line in maglines:
newline = re.sub('51200', swidth, line)
newline = re.sub('51210', sheight, newline)
print(newline, file=ofile)
# Create a new top-level layout called 'advSeal_6um_gen.mag'
# Mirrors uses in X and Y, and adds slots arrays at lower left
# and upper right
with open('advSeal_6um_gen.mag', 'w') as ofile:
print('magic', file=ofile)
print('tech sky130seal_ring', file=ofile)
print('magscale 1 2', file=ofile)
print('timestamp 1584630000', file=ofile)
# Lower left original
print('use seal_ring_corner seal_ring_corner_0', file=ofile)
print('timestamp 1584562315', file=ofile)
print('transform 1 0 0 0 1 0', file=ofile)
print('box -30480 -30480 ' + swidth + ' ' + sheight, file=ofile)
# Mirrored in X
print('use seal_ring_corner seal_ring_corner_3', file=ofile)
print('timestamp 1584562315', file=ofile)
print('transform -1 0 ' + dwidth + ' 0 1 0', file=ofile)
print('box -30480 -30480 ' + swidth + ' ' + sheight, file=ofile)
# Mirrored in Y
print('use seal_ring_corner seal_ring_corner_1', file=ofile)
print('timestamp 1584562315', file=ofile)
print('transform 1 0 0 0 -1 ' + dheight, file=ofile)
print('box -30480 -30480 ' + swidth + ' ' + sheight, file=ofile)
# Mirrored in both X and Y
print('use seal_ring_corner seal_ring_corner_2', file=ofile)
print('timestamp 1584562315', file=ofile)
print('transform -1 0 ' + dwidth + ' 0 -1 ' + dheight, file=ofile)
print('box -30480 -30480 ' + swidth + ' ' + sheight, file=ofile)
# Lower left slot arrays (bottom and left sides slots)
print('use seal_ring_slots_array seal_ring_slots_array_0', file=ofile)
print('timestamp 1584629764', file=ofile)
print('transform 1 0 0 0 1 0', file=ofile)
print('box 285 285 ' + swidth + ' ' + sheight, file=ofile)
# Upper right slot arrays (top and right sides slots)
print('use seal_ring_slots_array seal_ring_slots_array_1', file=ofile)
print('timestamp 1584629764', file=ofile)
print('transform -1 0 ' + dwidth + ' 0 -1 ' + dheight, file=ofile)
print('box 285 285 ' + swidth + ' ' + sheight, file=ofile)
print('<< end >>', file=ofile)
# Create a new abstract layout TO BE called 'advSeal_6um_gen.mag'
# This is the view in technology sky130A. Since there is already
# a cell with this name that is used to generate GDS, the cell
# will be called "seal_ring.mag" and copied to "advSeal_6um_gen.mag"
# in the target directory.
xwidth = str(dbhwidth)
xheight = str(dbhheight)
with open('seal_ring.mag', 'w') as ofile:
print('magic', file=ofile)
print('tech sky130A', file=ofile)
print('timestamp 1584566829', file=ofile)
# Lower left original
print('use seal_ring_corner_abstract seal_ring_corner_abstract_0', file=ofile)
print('timestamp 1584566221', file=ofile)
print('transform 1 0 0 0 1 0', file=ofile)
print('box 0 0 ' + qwidth + ' ' + qheight, file=ofile)
# Mirrored in X
print('use seal_ring_corner_abstract seal_ring_corner_abstract_3', file=ofile)
print('timestamp 1584566221', file=ofile)
print('transform -1 0 ' + xwidth + ' 0 1 0', file=ofile)
print('box 0 0 ' + qwidth + ' ' + qheight, file=ofile)
# Mirrored in Y
print('use seal_ring_corner_abstract seal_ring_corner_abstract_1', file=ofile)
print('timestamp 1584566221', file=ofile)
print('transform 1 0 0 0 -1 ' + xheight, file=ofile)
print('box 0 0 ' + qwidth + ' ' + qheight, file=ofile)
# Mirrored in both X and Y
print('use seal_ring_corner_abstract seal_ring_corner_abstract_2', file=ofile)
print('timestamp 1584566221', file=ofile)
print('transform -1 0 ' + xwidth + ' 0 -1 ' + xheight, file=ofile)
print('box 0 0 ' + qwidth + ' ' + qheight, file=ofile)
print('<< properties >>', file=ofile)
print('string LEFview no_prefix', file=ofile)
print('string GDS_FILE advSeal_6um_gen.gds', file=ofile)
print('string GDS_START 0', file=ofile)
print('string FIXED_BBOX 0 0 ' + swidth + ' ' + sheight, file=ofile)
print('<< end >>', file=ofile)
# Create the GDS of the seal ring
mproc = subprocess.run(['magic', '-dnull', '-noconsole',
'generate_gds.tcl'],
stdin = subprocess.DEVNULL, stdout = subprocess.PIPE,
stderr = subprocess.PIPE, universal_newlines = True)
if mproc.stdout:
for line in mproc.stdout.splitlines():
print(line)
if mproc.stderr:
print('Error message output from magic:')
for line in mproc.stderr.splitlines():
print(line)
if mproc.returncode != 0:
print('ERROR: Magic exited with status ' + str(mproc.returncode))
# Copy the GDS file and the abstract view to the target directory
os.chdir(starting_dir)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
print('Installing files to ' + target_dir)
if force or not os.path.exists(target_dir + '/advSeal_6um_gen.gds'):
shutil.copy(temp_dir+'/advSeal_6um_gen.gds', target_dir)
else:
print('ERROR: advSeal_6um_gen.gds already exists at target! Use -force to overwrite.')
if force or not os.path.exists(target_dir + '/advSeal_6um_gen.mag'):
shutil.copy(temp_dir+'/seal_ring.mag', target_dir + '/advSeal_6um_gen.mag')
else:
print('ERROR: advSeal_6um_gen.mag already exists at target! Use -force to overwrite.')
if force or not os.path.exists(target_dir + '/seal_ring_corner_abstract.mag'):
shutil.copy(temp_dir+'/seal_ring_corner_abstract.mag', target_dir)
else:
print('ERROR: seal_ring_corner_abstract.mag already exists at target! Use -force to overwrite.')
# Remove the temporary directory and its contents
if not keep:
shutil.rmtree(temp_dir)
else:
print('Retaining generated files in '+temp_dir+'/ directory')
# Done!
print('Done generating files advSeal_6um_gen.gds and advSeal_6um_gen.mag in ' + target_dir)
print('Place the seal ring cell in the final layout at (0um, 0um) before generating GDS.')
print('The top level layout minus seal ring must have a lower left corner of (6um, 6um)')
# If called as main, run generate_sealring()
if __name__ == '__main__':
# Divide up command line into options and arguments
options = []
arguments = []
for item in sys.argv[1:]:
if item.find('-', 0) == 0:
options.append(item)
else:
arguments.append(item)
force = True if '-force' in options else False
keep = True if '-keep' in options else False
outer = True if '-outer' in options else False
# Need one argument: path to verilog netlist
# If two arguments, then 2nd argument is the output file.
if len(arguments) == 3:
width = arguments[0]
height = arguments[1]
target_dir = arguments[2]
# Seal ring is 12um thick, so if "outer" option is used, subtract 12um
# from both width and height.
if outer:
width = str(float(width) - 12.0)
height = str(float(height) - 12.0)
generate_sealring(width, height, target_dir, force, keep)
else:
print("Usage: sky130_gen_sealring.py <width> <height> <target_dir> [options]")
print("Options:")
print(" -outer : Width and height are seal ring outer edge, not chip area")
print(" -force : Overwrite any existing files at <target_dir>")
print(" -keep : Keep generated files in temp/ directory")