| #!/usr/bin/env python3 |
| # Copyright 2020 The Skywater PDK Authors |
| # |
| # 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 |
| # |
| # https://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. |
| |
| import argparse |
| import glob |
| import os |
| import pprint |
| import re |
| import subprocess |
| import sys |
| import traceback |
| |
| from collections import defaultdict |
| from shutil import copyfile, move |
| from pathlib import Path |
| from multiprocessing import Pool |
| |
| from lef_canonical import canonicalize_lef |
| |
| import common |
| from common import \ |
| lib_extract_from_name, \ |
| extract_version_and_lib_from_path, \ |
| copy_file_to_output, \ |
| \ |
| convert_libname, \ |
| convert_cell_fullname, \ |
| convert_pinname |
| |
| superdebug = True |
| debug = True |
| |
| |
| def prepend_copyright(filename): |
| if filename.endswith('.lef'): |
| header = common.copyright_header['#'] |
| elif filename.endswith('.spice'): |
| header = common.copyright_header['*'] |
| else: |
| raise IOError("Don't know how to add copyright to: "+filename) |
| |
| with open(filename, 'r+') as f: |
| content = f.read() |
| f.seek(0, 0) |
| f.write(header + content) |
| |
| |
| def _magic_tcl_header(ofile, gdsfile): |
| print('#!/bin/env wish', file=ofile) |
| print('drc off', file=ofile) |
| print('scalegrid 1 2', file=ofile) |
| print('cif istyle vendorimport',file=ofile) |
| print('gds readonly true', file=ofile) |
| print('gds rescale false', file=ofile) |
| print('tech unlock *', file=ofile) |
| print('cif warning default', file=ofile) |
| print('set VDD VPWR', file=ofile) |
| print('set GND VGND', file=ofile) |
| print('set SUB SUBS', file=ofile) |
| print('gds read ' + gdsfile, file=ofile) |
| |
| |
| def magic_get_cells(input_gds, input_techfile): |
| destdir, gdsfile = os.path.split(input_gds) |
| tcl_path = destdir + '/get_cells.tcl' |
| |
| # Generate a tcl script for Magic |
| with open(tcl_path, 'w') as ofile: |
| _magic_tcl_header(ofile, gdsfile) |
| print('quit -noprompt', file=ofile) |
| return run_magic(destdir, tcl_path, input_techfile) |
| |
| |
| def magic_split_gds(input_gds, input_techfile, cell_list): |
| destdir, gdsfile = os.path.split(input_gds) |
| tcl_path = destdir + '/split_gds.tcl' |
| |
| # Generate a tcl script for Magic |
| with open(tcl_path, 'w') as ofile: |
| _magic_tcl_header(ofile, gdsfile) |
| |
| # Write out the cells |
| for cell in cell_list: |
| escaped_cell = '{'+cell+'}' |
| print(f'load {escaped_cell}', file=ofile) |
| print(f'gds write {escaped_cell}', file=ofile) |
| print(f'save {escaped_cell}', file=ofile) |
| |
| print('quit -noprompt', file=ofile) |
| return run_magic(destdir, tcl_path, input_techfile) |
| |
| |
| FATAL_ERROR = re.compile('((Error parsing)|(No such file or directory)|(couldn\'t be read))') |
| READING_REGEX = re.compile('Reading "([^"]*)".') |
| |
| |
| def run_magic(destdir, tcl_path, input_techfile, d="null"): |
| cmd = [ |
| 'magic', |
| '-nowrapper', |
| '-d'+d, |
| '-noconsole', |
| '-T', input_techfile, |
| os.path.abspath(tcl_path) |
| ] |
| with open(tcl_path.replace(".tcl", ".sh"), "w") as f: |
| f.write("#!/bin/sh\n") |
| f.write(" ".join(cmd)) |
| mproc = subprocess.run( |
| cmd, |
| stdin = subprocess.DEVNULL, |
| stdout = subprocess.PIPE, |
| stderr = subprocess.STDOUT, |
| cwd = destdir, |
| universal_newlines = True) |
| assert mproc.stdout |
| |
| max_cellname_width = 0 |
| output_by_cells = [('', [])] |
| fatal_errors = [] |
| for line in mproc.stdout.splitlines(): |
| if line.startswith('CIF file read warning: Input off lambda grid by 1/2; snapped to grid'): |
| continue |
| m = FATAL_ERROR.match(line) |
| if m: |
| fatal_errors.append(line) |
| m = READING_REGEX.match(line) |
| if m: |
| cell_name = m.group(1) |
| max_cellname_width = max(max_cellname_width, len(cell_name)) |
| output_by_cells.append((cell_name, [])) |
| output_by_cells[-1][-1].append(line) |
| |
| for cell, lines in output_by_cells: |
| prefix = "magic " + cell.ljust(max_cellname_width) + ':' |
| for l in lines: |
| is_error = 'rror' in l |
| if superdebug or (debug and is_error): |
| print(prefix, l) |
| |
| assert not mproc.stderr, mproc.stderr |
| |
| if mproc.returncode != 0 or fatal_errors: |
| if fatal_errors: |
| msg = ['ERROR: Magic had fatal errors in output:'] + fatal_errors |
| else: |
| msg = ['ERROR: Magic exited with status ' + str(mproc.returncode)] |
| msg.append("") |
| msg.append(" ".join(cmd)) |
| msg.append('='*75) |
| msg.append(mproc.stdout) |
| msg.append('='*75) |
| msg.append(destdir) |
| msg.append(tcl_path) |
| msg.append('-'*75) |
| msg.append(msg[0]) |
| raise SystemError('\n'.join(msg)) |
| |
| return output_by_cells |
| |
| |
| def magic_generate_output(techfile, gds_file, lef_file, cdl_file, cell): |
| print("magic_generate_output", techfile, gds_file, lef_file, cdl_file, cell) |
| assert os.path.exists(techfile), techfile |
| |
| destdir, _ = os.path.split(gds_file) |
| tcl_path = destdir + f'/gen_output.{cell}.tcl' |
| assert not os.path.exists(tcl_path), tcl_path |
| |
| # Generate a tcl script for Magic |
| with open(tcl_path, 'w') as ofile: |
| assert os.path.exists(gds_file), gds_file |
| _magic_tcl_header(ofile, gds_file) |
| |
| escaped_cell = '{'+cell+'}' |
| |
| # Force a canonical port order |
| print(f'select top cell', file=ofile) |
| print(f'port renumber', file=ofile) |
| |
| # LEF writing |
| if lef_file: |
| assert os.path.exists(lef_file), lef_file |
| print(f'lef read '+lef_file, file=ofile) |
| # Complement missing annotations from the SkyWater LEF |
| print(f'load {escaped_cell}', file=ofile) |
| print( 'if {![catch {set vpbpin [port VPB index]}]} {', file=ofile) |
| print( ' if {[port VPB use] == "default"} {', file=ofile) |
| print( ' port VPB use power', file=ofile) |
| print( ' }', file=ofile) |
| print( ' if {[port VPB class] == "default"} {', file=ofile) |
| print( ' port VPB class bidirectional', file=ofile) |
| print( ' }', file=ofile) |
| print( '}', file=ofile) |
| print( 'if {![catch {set vnbpin [port VNB index]}]} {', file=ofile) |
| print( ' if {[port VNB use] == "default"} {', file=ofile) |
| print( ' port VNB use ground', file=ofile) |
| print( ' }', file=ofile) |
| print( ' if {[port VNB class] == "default"} {', file=ofile) |
| print( ' port VNB class bidirectional', file=ofile) |
| print( ' }', file=ofile) |
| print( '}', file=ofile) |
| |
| # Override the port order to match the order given in the CDL file. |
| if cdl_file: |
| assert os.path.exists(cdl_file), cdl_file |
| print(f'readspice '+cdl_file, file=ofile) |
| print(f'load {escaped_cell}', file=ofile) |
| print(f'save', file=ofile) |
| |
| print(f'lef write {escaped_cell} -toplayer', file=ofile) |
| |
| # Netlist extraction |
| print(f'load {escaped_cell}', file=ofile) |
| print( 'extract do local', file=ofile) |
| print( 'extract no all', file=ofile) |
| print( 'extract all', file=ofile) |
| print( 'ext2spice lvs', file=ofile) |
| print( 'ext2spice', file=ofile) |
| |
| print('quit -noprompt', file=ofile) |
| return run_magic(destdir, tcl_path, techfile) |
| |
| |
| class Rewriter: |
| |
| other_replacements = [ |
| (re.compile('S8PIR[_-]10R', re.I), |
| lambda m: 'SKY130'), |
| (re.compile('S8', re.I), |
| lambda m: 'SKY130'), |
| ] |
| |
| def __init__(self, original_path): |
| self._strings = set() |
| self._cache_libname = None |
| self._cache_cellname = {} |
| self._cache_pinname = {} |
| self._cache_string = {} |
| |
| self.structures_reset() |
| |
| self.old_lib, self.new_lib, self.version = extract_version_and_lib_from_path(original_path) |
| |
| self.set_string = self.set_generic_string |
| self.rewrite_string = self.rewrite_generic_string |
| |
| def add_string(self, s): |
| self._strings.add(s) |
| |
| def strings(self): |
| o = list(self._strings) |
| o.sort() |
| return o |
| |
| def structures(self): |
| return dict(self._structures) |
| |
| def structures_usecount(self): |
| structs = list(self._structures_instances.items()) |
| max_count = max(c for n, c in structs) |
| structs.sort(key=lambda x: (max_count-x[1], x[1])) |
| return structs |
| |
| def structures_reset(self): |
| self._structures = {} |
| self._structures_instances = {} |
| self._structures_pins = {} |
| self._structure_last = None |
| |
| def replacements(self): |
| r = [] |
| r.append((self._cache_libname, self.new_lib)) |
| r.extend(self._cache_cellname.items()) |
| r.extend(self._cache_pinname.items()) |
| r.extend(self._cache_string.items()) |
| |
| o = {} |
| for a, b in r: |
| if a == b: |
| continue |
| elif a is None: |
| continue |
| o[a] = b |
| |
| return o |
| |
| def rewrite_library_name(self, s): |
| if self._cache_libname is not None: |
| assert s == self._cache_libname, (s, self._cache_libname) |
| else: |
| self._cache_libname = s |
| if s != self.new_lib: |
| print("Rewriting library from", repr(s), "to", repr(self.new_lib)) |
| return self.new_lib |
| |
| def set_library_name(self, s): |
| self.add_string(s) |
| return |
| |
| def _convert_cell(self, old_name, is_real_structure=True): |
| if '$$' in old_name: |
| old_cellname, number = old_name.split('$$', 1) |
| self.rewrite_structure_name(old_cellname, False) |
| |
| old_cellname = old_name |
| if self.old_lib == 's8rf': |
| if not old_name.startswith('s8rf_'): |
| old_cellname = 's8rf_'+old_name |
| |
| new_name = convert_cell_fullname(old_cellname, self.new_lib) |
| assert new_name.startswith(self.new_lib), (new_name, self.new_lib) |
| assert old_name not in self._cache_cellname, (old_name, self._cache_cellname) |
| self._cache_cellname[old_name] = new_name |
| |
| def pins(self, s): |
| return list(self._structures_pins[s].keys()) |
| |
| def rewrite_structure_name(self, s, is_real_structure=True): |
| assert (not is_real_structure) or (s in self._structures), s |
| if s not in self._cache_cellname: |
| self._convert_cell(s, is_real_structure) |
| ns = self._cache_cellname[s] |
| if is_real_structure: |
| assert s in self._structures, repr((s, ns))+'\n'+pprint.pformat(self._structures) |
| assert self._structures[s] is None, repr((sn, self._structures[s]))+'\n'+pprint.pformat(self._structures) |
| self._structures[s] = ns |
| |
| if s != ns: |
| print("Rewriting structure from", repr(s), "to", repr(ns)) |
| |
| return ns |
| |
| def set_structure_name(self, sn): |
| self.add_string(sn) |
| #assert 'pnp' in sn or sn not in self._structures, sn+'\n'+pprint.pformat(self._structures) |
| |
| if debug: |
| if self._structure_last != None: |
| print("Clearing current structure (was", self._structure_last+")") |
| print() |
| |
| self._structures[sn] = None |
| self._structures_instances[sn] = 0 |
| self._structures_pins[sn] = {} |
| self._structure_last = sn |
| |
| if debug: |
| print() |
| print("Setting current structure to", self._structure_last) |
| |
| def set_instance_struct(self, s): |
| self.add_string(s) |
| assert s in self._structures, (s, self._structures) |
| self._structures_instances[s] += 1 |
| #if debug: |
| # if self._structure_last != None: |
| # print("Clearing current structure (was", self._structure_last+")") |
| # print() |
| #self._structure_last = None |
| |
| def rewrite_instance_struct(self, s): |
| self.add_string(s) |
| assert s in self._cache_cellname, (s, self._cache_cellname) |
| sn = self._cache_cellname[s] |
| if s != sn: |
| print("Rewriting structure instance from", repr(s), "to", repr(sn)) |
| return sn |
| |
| def set_pin_name(self, s): |
| self.add_string(s) |
| assert self._structure_last is not None, s |
| if s not in self._structures_pins[self._structure_last]: |
| self._structures_pins[self._structure_last][s] = None |
| |
| def rewrite_pin_name(self, s): |
| self.add_string(s) |
| assert self._structure_last is not None, s |
| assert self._structure_last in self._structures, self._structure_last |
| |
| new_cellname = self._structures[self._structure_last] |
| if new_cellname is None: |
| new_cellname = self._structure_last |
| |
| pk = f'{self._structure_last}:{s}' |
| if pk in self._cache_pinname: |
| return self._cache_pinname[pk] |
| |
| pn = convert_pinname(s.upper(), new_cellname) |
| if s != pn: |
| print(" Rewriting pin on", self._structure_last, "from", repr(s), "to", repr(pn)) |
| self._cache_pinname[pk] = pn |
| self._structures_pins[self._structure_last][s] = pn |
| return pn |
| |
| overrides = { |
| ('sky130_fd_sc_hvl','scs8ls', 'inv_2'): 'scs8hvl_inv_2', |
| ('sky130_fd_sc_hvl','scs8ls', 'inv_4'): 'scs8hvl_inv_4', |
| ('sky130_fd_sc_ls', 'scs8ms', 'tapvgndnovpb_1'): 'scs8ls_tapvgndnovpb_1', |
| ('sky130_fd_sc_ls', 'scs8lp', 'diode_2'): 'scs8ls_diode_2', |
| ('sky130_fd_sc_ls', 'scs8lp', 'tap_2'): 'scs8ls_tap_2', |
| ('sky130_fd_sc_ls', 'scs8ms', 'tapvgnd2_1'): 'scs8ls_tapvgnd2_1', |
| ('sky130_fd_sc_ls', 'scs8ms', 'tapvgnd_1'): 'scs8ls_tapvgnd_1', |
| ('sky130_fd_sc_ls', 'scs8ms', 'tapvpwrvgnd_1'): 'scs8ls_tapvpwrvgnd_1', |
| ('sky130_fd_sc_ms', 'scs8ls', 'clkdlyinv3sd1_1'): 'scs8ms_clkdlyinv3sd1_1', |
| ('sky130_fd_sc_ms', 'scs8ls', 'clkdlyinv3sd2_1'): 'scs8ms_clkdlyinv3sd2_1', |
| ('sky130_fd_sc_ms', 'scs8ls', 'clkdlyinv3sd3_1'): 'scs8ms_clkdlyinv3sd3_1', |
| ('sky130_fd_sc_ms', 'scs8ls', 'clkdlyinv5sd1_1'): 'scs8ms_clkdlyinv5sd1_1', |
| ('sky130_fd_sc_ms', 'scs8lp', 'dlygate4s15_1'): 'scs8ms_dlygate4s15_1', |
| ('sky130_fd_sc_ms', 'scs8ls', 'tap_1'): 'scs8ms_tap_1', |
| ('sky130_fd_sc_ms', 'scs8lp', 'tap_2'): 'scs8ms_tap_2', |
| } |
| |
| def set_generic_string(self, s): |
| self.add_string(s) |
| |
| def rewrite_generic_string(self, old_str): |
| self.add_string(old_str) |
| if old_str in self._cache_string: |
| return self._cache_string[old_str] |
| |
| new_str = old_str |
| |
| # Is this a simple cell name string? |
| if old_str in self._cache_cellname: |
| cell_fullname = self._cache_cellname[old_str] |
| lib_name, cell_name = cell_fullname.split('__', 1) |
| if old_str != cell_name: |
| print(" Rewriting string (cell name) from", repr(old_str), "to", repr(cell_name)) |
| return cell_name |
| |
| # Is this a simple pin name string? |
| if self._structure_last: |
| if old_str in self._structures_pins[self._structure_last]: |
| new_str = self._structures_pins[self._structure_last][old_str] |
| if old_str != new_str: |
| print(" Rewriting string (pin name on ", self._structure_last, ") from", repr(old_str), "to", repr(new_str)) |
| return new_str |
| |
| # Check this isn't for a different library.... |
| ext_libname, ext_cellname = lib_extract_from_name(old_str) |
| okey = (self.new_lib, ext_libname, ext_cellname) |
| if okey in self.overrides: |
| override = self.overrides[okey] |
| if debug: |
| print('Overriding {} with {}'.format(old_str, override)) |
| return self.rewrite_generic_string(override) |
| elif (self.new_lib, ext_libname) == ('sky130_fd_sc_hdll', 'scs8hd'): |
| override = 'scs8hdll_'+ext_cellname |
| if debug: |
| print('Overriding {} with {}'.format(old_str, override)) |
| return self.rewrite_generic_string(override) |
| else: |
| assert ext_libname is None or ext_libname == self.old_lib, ( |
| old_str, ext_libname, ext_cellname, self.old_lib) |
| |
| if ext_libname is not None and ext_cellname is not None: |
| new_cellname = convert_cell_fullname(ext_cellname, self.new_lib) |
| assert new_cellname.startswith(self.new_lib), (new_cellname, self.new_lib) |
| new_str = new_str.replace( |
| f'{ext_libname}_{ext_cellname}', new_cellname) |
| |
| # Does this string contain a cell name? |
| for old_cellname, new_cellname in self._cache_cellname.items(): |
| if old_cellname in new_str: |
| new_str = new_str.replace(old_cellname, new_cellname) |
| |
| # Does this contain other things needed to replaced? |
| for regex, rep in self.other_replacements: |
| new_str = regex.sub(rep, new_str) |
| |
| self._cache_string[old_str] = new_str |
| if old_str != new_str: |
| print(" Rewriting string from", repr(old_str), "to", repr(new_str)) |
| return new_str |
| |
| def set_string_type(self, i): |
| # Processed Name Processed Description Legacy Name Description GDS Layer+Purpose |
| # ('nwell', 'label') ('nwell', 'label') nwellLabel nwell label 64 5 -- VPB? |
| # ('tap', 'label') ('tap', 'label') tapLabel tap label 65 5 |
| # ('poly', 'label') ('poly', 'label') polyLabel poly label 66 5 |
| # ('polytt',) ('poly', 'drawing') polytt poly label 66 5 |
| # (('li', 1), 'text') (('li', 1), 'drawing') li1tt li1 label 67 5 |
| # (('metal', 1), 'label') (('metal', 1), 'label') met1Label met1 label 68 5 |
| # (('metal', 1), 'text') (('metal', 1), 'drawing') met1tt met1 label 68 5 |
| # (('metal', 2), 'label') (('metal', 2), 'label') met2Label met2 label 69 5 |
| # (('metal', 2), 'text') (('metal', 2), 'drawing') met2tt met2 label 69 5 |
| # (('metal', 3), 'label') (('metal', 3), 'label') met3Label met3 label 70 5 |
| # (('metal', 3), 'text') (('metal', 3), 'drawing') met3tt met3 label 70 5 |
| # (('metal', 4), 'label') (('metal', 4), 'label') met4Label met4 label 71 5 |
| # (('metal', 4), 'text') (('metal', 4), 'drawing') met4tt met4 label 71 5 |
| # (('metal', 5), 'label') (('metal', 5), 'label') met5Label met5 label 72 5 |
| # (('metal', 5), 'text') (('metal', 5), 'drawing') met5tt met5 label 72 5 |
| # ('pad', 'text') ('pad', 'label') padText pad label 76 5 |
| |
| # ('pwelltt',) ('pwell', 'label') pwelltt pwell label 64 59 -- VNB? |
| # ('difftt',) ('diffusion', 'drawing') difftt diff label 65 6 |
| |
| # NOTE: Purposes 5 and 16 are confused in the SkyWater GDS files. |
| # One should be used for pins, the other for text, not intermingled |
| # as they are. Most non-pin text seems to be on layer 83:44. |
| |
| if i in (5, 16, 20, 59): |
| if superdebug: |
| print(" Next string is a pin name on", self._structure_last) |
| # FIXME: Hack -- self._structure should never be none for these types!? |
| if self._structure_last is None: |
| self.set_string = self.set_generic_string |
| self.rewrite_string = self.rewrite_generic_string |
| else: |
| self.set_string = self.set_pin_name |
| self.rewrite_string = self.rewrite_pin_name |
| else: |
| if superdebug: |
| print(" Next string is a generic string") |
| self.set_string = self.set_generic_string |
| self.rewrite_string = self.rewrite_generic_string |
| |
| def set_label(self, s): |
| return self.set_generic_string(s) |
| |
| |
| |
| def replace_gds_strings(gds_filename, call_rewrite_on_rtypes, rewriter): |
| source = gds_filename |
| dest = gds_filename |
| |
| sourcedir, gdsinfile = os.path.split(source) |
| destdir, gdsoutfile = os.path.split(dest) |
| |
| with open(source, 'rb') as ifile: |
| gdsdata = bytearray(ifile.read()) |
| |
| # If we are rewriting the structure names, we need to rewrite the instances |
| # which point to the structure. |
| if 'structure_name' in call_rewrite_on_rtypes: |
| call_rewrite_on_rtypes.append('instance_struct') |
| |
| rtype_mapping = { |
| 2: 'library_name', # libname |
| 6: 'structure_name', # strname - Structure Definition Name |
| 18: 'instance_struct',# sname - Instances structure |
| 22: 'string_type', # Indicates what the next string is in reference too.. |
| 25: 'string', # string |
| 44: 'label', # text 83 44 -- textlabel |
| } |
| rtype_rmapping = {v:k for k,v in rtype_mapping.items()} |
| |
| for r in call_rewrite_on_rtypes: |
| assert r in rtype_rmapping, r |
| |
| datalen = len(gdsdata) |
| if superdebug: |
| print('Original data length = ' + str(datalen)) |
| dataptr = 0 |
| while dataptr < datalen: |
| # Read stream records up to any string, then search for search text. |
| bheader = gdsdata[dataptr:dataptr + 2] |
| reclen = int.from_bytes(bheader, 'big') |
| newlen = reclen |
| |
| # The GDS files seem to occasionally end up with trailing zero bytes. |
| if newlen == 0: |
| if debug: |
| print("{:10d} (of {:10d} - {:10d} left)".format(dataptr, datalen, datalen-dataptr), 'Found zero-length record at position in', source) |
| if superdebug: |
| print('Remaining data', repr(gdsdata[dataptr:])) |
| for i in range(dataptr, datalen): |
| if gdsdata[i] != 0: |
| raise SystemError('Found non-zero pad byte at {} ({}): {}'.format(i, hex(i), repr(gdsdata[i]))) |
| break |
| |
| rtype = gdsdata[dataptr + 2] |
| rtype_name = rtype_mapping.get(rtype, '??? - {}'.format(rtype)) |
| datatype = gdsdata[dataptr + 3] |
| |
| # FIXME: Hack to use different method for pin names... |
| if datatype == 2 and rtype in rtype_mapping: |
| assert datatype == 2, (rtype, datatype) |
| value = int.from_bytes(gdsdata[dataptr+4:dataptr+6], 'big') |
| if superdebug: |
| print( |
| "{:10d} (of {:10d} - {:10d} left)".format(dataptr, datalen, datalen-dataptr), |
| 'Record type = {:15s} '.format(rtype_name), |
| value, |
| ) |
| getattr(rewriter, 'set_'+rtype_name)(value) |
| |
| # Datatype 6 is STRING |
| if datatype == 6: |
| bstring = gdsdata[dataptr + 4: dataptr + reclen] |
| |
| if bstring[-1] == 0: |
| # Was original string padded with null byte? If so, |
| # remove the null byte. |
| decoded = bstring[:-1].decode('ascii') |
| else: |
| decoded = bstring.decode('ascii') |
| |
| rewriter.add_string(decoded) |
| |
| if '???' in rtype_name: |
| print(rtype_name, decoded) |
| |
| if rtype not in (44,): |
| getattr(rewriter, 'set_'+rtype_name)(decoded) |
| |
| if rtype_name in call_rewrite_on_rtypes: |
| skipped = False |
| repstring = getattr(rewriter, 'rewrite_'+rtype_name)(decoded) |
| else: |
| skipped = True |
| repstring = decoded |
| assert repstring is not None |
| |
| changed = (decoded != repstring) |
| if superdebug: |
| print( |
| "{:10d} (of {:10d} - {:10d} left)".format(dataptr, datalen, datalen-dataptr), |
| 'Record type = {:15s} '.format(rtype_name), |
| 'Skipped = {:5s}'.format(str(skipped)), |
| end=" ", |
| ) |
| if changed: |
| print(repr(decoded), '->', repr(repstring)) |
| else: |
| print(repr(decoded), '==', repr(repstring)) |
| |
| brepstring = repstring.encode('ascii') |
| newlen = len(brepstring) + 4 |
| |
| # Record sizes must be even |
| if newlen % 2 != 0: |
| brepstring += b'\x00' |
| newlen += 1 |
| |
| if changed: |
| #before = gdsdata[0:dataptr] |
| #after = gdsdata[dataptr + reclen:] |
| |
| bnewlen = newlen.to_bytes(2, byteorder='big') |
| brtype = rtype.to_bytes(1, byteorder='big') |
| bdatatype = datatype.to_bytes(1, byteorder='big') |
| |
| # Assemble the new record |
| newrecord = bnewlen + brtype + bdatatype + brepstring |
| |
| # Reassemble the GDS data around the new record |
| #gdsdata = before + newrecord[0:newlen] + after |
| gdsdata[dataptr:dataptr+reclen] = newrecord[0:newlen] |
| |
| # Adjust the data end location |
| datalen += (newlen - reclen) |
| |
| # Advance the pointer past the data |
| dataptr += newlen |
| |
| with open(dest, 'wb') as ofile: |
| ofile.write(gdsdata) |
| |
| |
| def gds_strings_get(input_path, tmp_gds): |
| rewriter = Rewriter(input_path) |
| debug_stop() |
| replace_gds_strings(tmp_gds, [], rewriter) |
| assert not rewriter.replacements(), (tmp_gds, rewriter.replacements()) |
| debug_restart() |
| return rewriter.strings() |
| |
| |
| def gds_strings_print(h, input_path, tmp_gds): |
| strings = gds_strings_get(input_path, tmp_gds) |
| max_len = max(len(s) for s in strings) |
| print() |
| print(h, end=" ") |
| print(f"strings in GDS file {tmp_gds} (using GDS reader):",) |
| print('---') |
| for s in strings: |
| print(" ", s.rjust(max_len)) |
| print('---') |
| |
| |
| def strings_get(pathname): |
| string_counts = {} |
| lines = subprocess.check_output( |
| "strings {} | sort | uniq -c".format(pathname), shell=True).decode('utf-8').splitlines() |
| for l in lines: |
| c, s = l.strip().split(' ', 1) |
| string_counts[s] = int(c) |
| return string_counts |
| |
| |
| def strings_print(h, strings): |
| max_len = max(len(s) for s in strings.keys()) |
| max_count = max(strings.values()) |
| print() |
| print(h) |
| print("---") |
| for s, c in sorted(strings.items(), key=lambda x: (max_count-x[-1], x[0])): |
| print(" ", s.rjust(max_len)+':', c) |
| print("---") |
| |
| |
| ldebug = [] |
| lsuperdebug = [] |
| |
| def debug_stop(): |
| global debug |
| global superdebug |
| global ldebug |
| global lsuperdebug |
| |
| ldebug.append(debug) |
| debug = False |
| |
| lsuperdebug.append(superdebug) |
| superdebug = False |
| |
| |
| def debug_restart(): |
| global debug |
| global superdebug |
| global ldebug |
| global lsuperdebug |
| |
| assert ldebug |
| assert lsuperdebug |
| |
| debug = ldebug.pop(-1) |
| superdebug = lsuperdebug.pop(-1) |
| |
| |
| def pool_write_new(args): |
| src_temp_dir, final_dir, techfile, input_path, new_cellname = args |
| |
| temp_dir = os.path.join(src_temp_dir, new_cellname) |
| if not os.path.exists(temp_dir): |
| os.makedirs(temp_dir) |
| |
| cell_gds_file = os.path.join(src_temp_dir, new_cellname+'.gds') |
| gds_file = os.path.abspath(os.path.join(temp_dir, new_cellname+'.gds')) |
| assert os.path.exists(cell_gds_file), cell_gds_file |
| assert not os.path.exists(gds_file), gds_file |
| copyfile(cell_gds_file, gds_file) |
| |
| subprocess.check_call(CHANGE_GDS_DATE+" 1 0 "+gds_file, shell=True) |
| |
| if debug: |
| strings_print(f"Final strings in GDS file {gds_file} (using strings):", strings_get(gds_file)) |
| gds_strings_print('Final', input_path, gds_file) |
| |
| debug_stop() |
| rewriter = Rewriter(input_path) |
| replace_gds_strings(gds_file, [], rewriter) |
| r = rewriter.replacements() |
| assert not r, (new_cellname, r) |
| debug_restart() |
| |
| # Write the output .pins files |
| no_pins = False |
| for name in common.CELLS_WITH_NO_PINS: |
| if name in new_cellname: |
| no_pins = True |
| break |
| #assert no_pins or rewriter.pins(new_cellname), (new_cellname, rewriter.pins(new_cellname)) |
| if not rewriter.pins(new_cellname): |
| print("WARNING: No pins on", new_cellname) |
| |
| gds_opin_file = os.path.join(temp_dir, new_cellname+'.gds.pins') |
| assert not os.path.exists(gds_opin_file), gds_opin_file |
| with open(gds_opin_file, 'w') as f: |
| for pin in rewriter.pins(new_cellname): |
| f.write(pin) |
| f.write('\n') |
| |
| lib = rewriter.new_lib |
| ver = rewriter.version |
| |
| in_cdl_file = common.get_final_path(final_dir, lib, ver, new_cellname, '.cdl') |
| if not in_cdl_file: |
| return |
| |
| in_lef_file = common.get_final_path(final_dir, lib, ver, new_cellname, '.lef') |
| if not in_lef_file: |
| return |
| |
| if not os.path.exists(in_cdl_file): |
| print("Missing CDL:", in_cdl_file) |
| in_cdl_file = None |
| |
| if not os.path.exists(in_lef_file): |
| print("Missing LEF:", in_lef_file) |
| in_lef_file = None |
| |
| if in_cdl_file and in_lef_file: |
| print(new_cellname, "found both LEF and CDL inputs") |
| |
| out_lef_file = os.path.join(temp_dir, new_cellname+'.lef') |
| assert not os.path.exists(out_lef_file), ("Existing lef:", out_lef_file) |
| |
| out_spice_file = os.path.join(temp_dir, new_cellname+'.spice') |
| assert not os.path.exists(out_spice_file), ("Existing spice:", out_spice_file) |
| |
| out_mag_file = os.path.join(temp_dir, new_cellname+'.mag') |
| out_clean_mag_file = os.path.join(temp_dir, new_cellname+'-temp.mag') |
| assert not os.path.exists(out_mag_file), ("Existing mag:", out_mag_file) |
| assert not os.path.exists(out_clean_mag_file), ("Existing mag:", out_clean_mag_file) |
| |
| magic_generate_output(techfile, gds_file, in_lef_file, in_cdl_file, new_cellname) |
| |
| # .magic.lef file -> output |
| assert os.path.exists(out_lef_file), ("Missing new LEF:", out_lef_file) |
| out_mlef_file = os.path.join(temp_dir, new_cellname+'.magic.lef') |
| assert not os.path.exists(out_mlef_file), ("Existing .magic.lef:", out_mlef_file) |
| rewrite_out_lef(out_lef_file, out_mlef_file) |
| copy_file_to_output(out_mlef_file, final_dir, lib, ver, new_cellname) |
| |
| # spice file -> output |
| assert os.path.exists(out_spice_file), ("Missing new spice:", out_spice_file) |
| rewrite_out_spice(out_spice_file) |
| copy_file_to_output(out_spice_file, final_dir, lib, ver, new_cellname) |
| |
| # mag file -> output |
| assert os.path.exists(out_mag_file), ("Missing new mag:", out_mag_file) |
| rewrite_out_mag(out_mag_file, out_clean_mag_file) |
| copy_file_to_output(out_clean_mag_file, final_dir, lib, ver, new_cellname) |
| |
| # GDS file -> output |
| copy_file_to_output(gds_file, final_dir, lib, ver, new_cellname) |
| copy_file_to_output(gds_opin_file, final_dir, lib, ver, new_cellname) |
| |
| |
| return new_cellname |
| |
| |
| def rewrite_out_lef(lef_in_file, lef_out_file=None): |
| if lef_out_file is None: |
| lef_out_file = lef_in_file |
| |
| with open(lef_in_file, 'r') as f: |
| contents = f.read() |
| |
| contents = canonicalize_lef(contents) |
| |
| with open(lef_out_file, 'w') as f: |
| f.write(contents) |
| |
| prepend_copyright(lef_out_file) |
| |
| |
| def rewrite_out_mag(mag_in_file, mag_out_file=None): |
| if mag_out_file is None: |
| mag_out_file = mag_in_file |
| |
| with open(mag_in_file, 'r') as f: |
| contents = f.read() |
| |
| contents = re.sub('timestamp [0-9]+', 'timestamp 0', contents) |
| def basename(m): |
| return 'string GDS_FILE '+os.path.basename(m.group(1)) |
| contents = re.sub('string GDS_FILE (.*)', basename, contents) |
| |
| with open(mag_out_file, 'w') as f: |
| f.write(contents) |
| |
| |
| RE_NGSPICE = re.compile('\\* NGSPICE file created from .* - technology: .*') |
| RE_WHITESPACE = re.compile('\\n+') |
| RE_SUBCKT = re.compile('^\\.subckt (?P<subckt>[^ ]*)([^\\n]*)$(.*?)^\\.ends', flags=re.M|re.DOTALL) |
| |
| def rewrite_contents_spice(spice_file, contents): |
| """ |
| |
| >>> print(rewrite_contents_spice('RANDOM/sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhvtop.spice', ''' |
| ... .subckt sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv C1 C0 MET5 |
| ... X0 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| ... X1 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| ... .ends |
| ... .subckt sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhvtop M5 C0 SUB |
| ... Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[0|0] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| ... Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[1|0] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| ... Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[0|1] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| ... Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[1|1] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| ... .ends |
| ... ''').strip()) |
| .subckt sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhvtop M5 C0 SUB |
| Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[0|0] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[1|0] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[0|1] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| Xsky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv_0[1|1] SUB C0 M5 sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv |
| .ends |
| |
| >>> print(rewrite_contents_spice('RANDOM/sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhvtop.spice', ''' |
| ... .subckt sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv C1 C0 MET5 |
| ... X0 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| ... X1 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| ... .ends |
| ... ''').strip()) |
| .subckt sky130_fd_pr__cap_vpp_11p3x11p8_l1m1m2m3m4_shieldm5_nhv C1 C0 MET5 |
| X0 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| X1 C1 C0 C1 C1 sky130_fd_pr__nfet_05v0_nvt w=1e+07u l=4e+06u |
| .ends |
| |
| """ |
| contents = contents.replace('technology: (null)', 'technology: sky130A') |
| |
| contents = RE_NGSPICE.sub('', contents) |
| contents = RE_WHITESPACE.sub('\n', contents) |
| |
| if contents.count('.subckt') > 1: |
| basename = spice_file.rsplit('/', 1)[-1].split('.', 1)[0] |
| def replace_subckt(m): |
| if m.group('subckt') != basename: |
| return '' |
| return m.group(0) |
| |
| new_contents = RE_SUBCKT.sub(replace_subckt, contents) |
| if new_contents.count('.subckt') == 1: |
| contents = new_contents |
| return contents |
| |
| |
| def rewrite_out_spice(spice_file): |
| with open(spice_file, 'r') as f: |
| contents = f.read() |
| |
| contents = rewrite_contents_spice(spice_file, contents) |
| with open(spice_file, 'w') as f: |
| f.write(contents) |
| |
| prepend_copyright(spice_file) |
| |
| |
| def filemain(input_path, temp_dir, final_dir, args): |
| |
| techfile = args.techfile |
| |
| tmp_gds = temp_dir + '/input.gds' |
| copyfile(input_path, tmp_gds) |
| |
| rewriter = Rewriter(input_path) |
| |
| if debug: |
| gds_strings_print('Initial', input_path, tmp_gds) |
| |
| # First rewrite the cell names |
| if debug: |
| print() |
| print("Rewriting library and structure names") |
| print("-------------------------------------") |
| replace_gds_strings(tmp_gds, ['library_name', 'structure_name'], rewriter) |
| if debug: |
| print("-------------------------------------") |
| |
| cell_rewrites = list(rewriter.structures().items()) |
| if not cell_rewrites: |
| print("WARNING: No cells found!") |
| return |
| |
| #if 's8rf' in input_path and 's8rf2' not in input_path: |
| # cell_removed = [x for x in cell_rewrites if not x.startswith('s8rf_')] |
| # print('Ignoring:', cell_removed) |
| # cell_rewrites = [x for x in cell_rewrites if x.startswith('s8rf_')] |
| |
| # Write out the cell list |
| with open(temp_dir + '/cells.list', 'w') as f: |
| max_len = max(len(s) for s in cell_rewrites) |
| for from_str, to_str in sorted(cell_rewrites): |
| f.write(" {:s} -> {:s}".format(from_str.rjust(max_len), to_str)) |
| f.write('\n') |
| |
| # Second rewrite any remaining strings. |
| # ----- |
| # The strings could have references to cell names and there is no guarantee |
| # that the cell instance definition will appear before a string which |
| # happens to contain the name. |
| # Hence, we want to know all the cell rewrites before rewriting strings. |
| if debug: |
| print() |
| print("Rewriting strings (inc pin names)") |
| print("-------------------------------------") |
| rewriter.structures_reset() |
| replace_gds_strings(tmp_gds, ['string'], rewriter) |
| if debug: |
| print("-------------------------------------") |
| |
| if debug: |
| gds_strings_print('Before split', input_path, tmp_gds) |
| |
| structs = list(rewriter.structures_usecount()) |
| for name, count in structs: |
| with open(os.path.join(temp_dir, name+'.gds.src.pins'), 'w') as f: |
| for pin in rewriter.pins(name): |
| f.write(pin) |
| f.write('\n') |
| |
| # Write out the rewrite list |
| with open(temp_dir + '/rewrite.list', 'w') as f: |
| r = rewriter.replacements() |
| max_len = max(len(x) for x in r) |
| for from_str, to_str in sorted(r.items()): |
| f.write(from_str.rjust(max_len)) |
| f.write(" -> ") |
| f.write(to_str) |
| f.write('\n') |
| |
| filtered_cells = set() |
| for name in rewriter.structures(): |
| if 'libcell' in name: |
| print("Skipping", name) |
| continue |
| if 'vcells' in name: |
| print("Skipping", name) |
| continue |
| filtered_cells.add(name) |
| filtered_cells = list(filtered_cells) |
| filtered_cells.sort() |
| |
| # Split apart the GDS file |
| input_techfile = os.path.abspath(techfile) |
| output_by_cells = magic_split_gds(tmp_gds, input_techfile, filtered_cells) |
| |
| #for cellname in filtered_cells: |
| # pool_write_new([temp_dir, final_dir, input_techfile, input_path, cellname]) |
| # |
| #return |
| |
| # Extract the netlist |
| def pool_write_new_args(filtered_cells): |
| for new_cellname in filtered_cells: |
| yield temp_dir, final_dir, input_techfile, input_path, new_cellname |
| |
| pool = Pool() |
| for cellname in pool.imap_unordered(pool_write_new, pool_write_new_args(filtered_cells)): |
| if not cellname: |
| continue |
| if superdebug: |
| print("Finished creating new files for:", cellname) |
| pool.close() |
| pool.join() |
| |
| |
| |
| __dir__ = os.path.dirname(os.path.realpath(__file__)) |
| CHANGE_GDS_DATE = os.path.join(__dir__, 'change_gds_date.py') |
| |
| |
| if __name__ == "__main__": |
| import doctest |
| fails, _ = doctest.testmod() |
| if fails != 0: |
| sys.exit("Some test failed") |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "input", |
| help="The path to the source directory/file", |
| type=Path) |
| parser.add_argument( |
| "output", |
| help="The path to the output directory", |
| type=Path) |
| parser.add_argument( |
| "techfile", |
| help="Full path to the techfile", |
| type=str) |
| parser.add_argument( |
| "temp", |
| help="The path to the temp directory", |
| type=Path) |
| args = parser.parse_args() |
| common.main('gds', filemain, args) |