| #!/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 os |
| import sys |
| import subprocess |
| import re |
| import argparse |
| import glob |
| from shutil import copyfile, move |
| from pathlib import Path |
| from common import convert_libname, lib_extract_from_path, version_extract_from_path, lib_extract_from_name |
| from verilog2full import Copyright_header |
| import lef_split |
| |
| debug = False |
| debug_print = lambda x: print(x) if debug else 0 |
| |
| Copyright_header = """/*\n |
| * Copyright 2020 The Skywater PDK Authors\n |
| *\n |
| * Licensed under the Apache License, Version 2.0 (the "License");\n |
| * you may not use this file except in compliance with the License.\n |
| * You may obtain a copy of the License at\n |
| *\n |
| * https://www.apache.org/licenses/LICENSE-2.0\n |
| *\n |
| * Unless required by applicable law or agreed to in writing, software\n |
| * distributed under the License is distributed on an "AS IS" BASIS,\n |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n |
| * See the License for the specific language governing permissions and\n |
| * limitations under the License.\n |
| */\n |
| \n |
| """ |
| |
| def prepend_copyright(filename): |
| with open(filename, 'r+') as f: |
| content = f.read() |
| content = content.replace(Copyright_header, '') |
| f.seek(0, 0) |
| f.write(lef_split.Copyright_header + content) |
| |
| def split_gds_into(input_gds, input_techfile, input_celllist, output_format): |
| |
| assert output_format in ('gds', 'lef'), "Wrong output format. Supported formats are 'gds' and 'lef'" |
| |
| if os.path.isfile(input_celllist): |
| with open(input_celllist, 'r') as ifile: |
| contents = ifile.read() |
| input_celllist = contents.splitlines() |
| if len(input_celllist) == 1: |
| input_celllist = input_celllist[0].split(' ') |
| |
| destdir = os.path.split(input_gds)[0] |
| gdsfile = os.path.split(input_gds)[1] |
| tcl_path = destdir + '/split_gds.tcl' |
| |
| with open(tcl_path, 'w') as ofile: |
| print('#!/bin/env wish', file=ofile) |
| print('drc off', file=ofile) |
| print('gds readonly true', file=ofile) |
| print('gds rescale false', file=ofile) |
| print('tech unlock *', file=ofile) |
| print('gds read ' + gdsfile, file=ofile) |
| |
| for cell in input_celllist: |
| |
| lib, _ = lib_extract_from_name(cell) |
| new_lib = convert_libname(lib) |
| |
| if cell == 'libcell' or cell == lib: |
| continue |
| # known to be useless and kills magic ;( |
| if any([x in cell for x in ('gpio_opath', 'i2c_fix', 'hotswap', 'odrvr', 'opamp')]): |
| continue |
| |
| if cell.find(lib) == -1: |
| new_cell_name = new_lib +'__' + cell |
| else: |
| new_cell_name = cell.replace(lib + '_', new_lib + '__') |
| print('cellname rename ' + cell + ' ' + new_cell_name, file=ofile) |
| print('load ' + new_cell_name, file=ofile) |
| print(f'gds write ' + new_cell_name, file=ofile) |
| print(f'lef write ' + new_cell_name, file=ofile) |
| print(f'save ' + new_cell_name, file=ofile) |
| |
| print('quit -noprompt', file=ofile) |
| |
| mproc = subprocess.run(['magic', '-dnull', '-noconsole', |
| '-T', input_techfile, os.path.abspath(tcl_path)], |
| stdin = subprocess.DEVNULL, |
| stdout = subprocess.PIPE, |
| stderr = subprocess.PIPE, cwd = destdir, |
| 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)) |
| os.remove(tcl_path) |
| |
| def split_vcells_gds_into(input_gds, input_techfile, input_celllist): |
| |
| if os.path.isfile(input_celllist): |
| with open(input_celllist, 'r') as ifile: |
| contents = ifile.read() |
| input_celllist = contents.splitlines() |
| if len(input_celllist) == 1: |
| input_celllist = input_celllist[0].split(' ') |
| |
| destdir = os.path.split(input_gds)[0] |
| gdsfile = os.path.split(input_gds)[1] |
| tcl_path = destdir + '/split_gds.tcl' |
| |
| with open(tcl_path, 'w') as ofile: |
| print('#!/bin/env wish', file=ofile) |
| print('drc off', file=ofile) |
| print('gds readonly true', file=ofile) |
| print('gds rescale false', file=ofile) |
| print('tech unlock *', file=ofile) |
| print('gds read ' + gdsfile, file=ofile) |
| |
| for cell in input_celllist: |
| if 's8rf2' in cell: |
| lib = 's8rf2' |
| new_lib = 'sky130_fd_pr_rf2' |
| elif 's8rf' in cell: |
| lib = 's8rf' |
| new_lib = 'sky130_fd_pr_rf' |
| else: |
| lib = 's8' |
| new_lib = 'sky130_fd_pr_base' |
| |
| if cell.find(lib) == -1: |
| new_cell_name = new_lib +'__' + cell |
| else: |
| new_cell_name = cell.replace(lib + '_', new_lib + '__') |
| print('cellname rename ' + cell + ' ' + new_cell_name, file=ofile) |
| print('load ' + new_cell_name, file=ofile) |
| print(f'gds write ' + new_cell_name, file=ofile) |
| print(f'lef write ' + new_cell_name, file=ofile) |
| print(f'save ' + new_cell_name, file=ofile) |
| |
| print('quit -noprompt', file=ofile) |
| |
| mproc = subprocess.run(['magic', '-dnull', '-noconsole', |
| '-T', input_techfile, os.path.abspath(tcl_path)], |
| stdin = subprocess.DEVNULL, |
| stdout = subprocess.PIPE, |
| stderr = subprocess.PIPE, cwd = destdir, |
| 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)) |
| os.remove(tcl_path) |
| |
| |
| def create_dir_path(output_dir, mod, newlib, version): |
| """ |
| >>> create_dir_path('output', 'top_filter_narrow', 'sky130_fd_io', 'v0.0.1') |
| 'output/skywater-pdk/libraries/sky130_fd_io/v0.0.1/cells/top_filter_narrow' |
| >>> create_dir_path('output', 'top_filter_narrow_1', 'sky130_fd_io', 'v0.0.1') |
| 'output/skywater-pdk/libraries/sky130_fd_io/v0.0.1/cells/top_filter_narrow' |
| >>> create_dir_path('output', 'a2bb2oi_1', 'sky130_fd_io', 'v0.0.1') |
| 'output/skywater-pdk/libraries/sky130_fd_io/v0.0.1/cells/a2bb2oi' |
| >>> create_dir_path('output', 'aaa', 'sky130_fd_pr_base', None) |
| 'output/skywater-pdk/libraries/sky130_fd_pr_base/cells/aaa' |
| |
| """ |
| |
| if version is None: |
| new_path = f"{output_dir}/skywater-pdk/libraries/{newlib}/cells/{re.sub(r'_[0-9]{1,2}$', '',mod)}" |
| else: |
| new_path = f"{output_dir}/skywater-pdk/libraries/{newlib}/{version}/cells/{re.sub(r'_[0-9]{1,2}$', '',mod)}" |
| return new_path |
| |
| def change_gds_string(source, oldstring, newstring): |
| dest = source |
| |
| sourcedir = os.path.split(source)[0] |
| gdsinfile = os.path.split(source)[1] |
| |
| destdir = os.path.split(dest)[0] |
| gdsoutfile = os.path.split(dest)[1] |
| |
| with open(source, 'rb') as ifile: |
| gdsdata = ifile.read() |
| |
| # To be done: Allow the user to select a specific record type or types |
| # in which to restrict the string substitution. If no restrictions are |
| # specified, then substitue in library name, structure name, and strings. |
| |
| recordtypes = ['libname', 'strname', 'string'] |
| recordfilter = [2, 6, 25] |
| bsearch = bytes(oldstring, 'ascii') |
| brep = bytes(newstring, 'ascii') |
| |
| datalen = len(gdsdata) |
| if debug: |
| 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 |
| if newlen == 0: |
| print('Error: found zero-length record at position ' + str(dataptr)) |
| break |
| |
| rectype = gdsdata[dataptr + 2] |
| datatype = gdsdata[dataptr + 3] |
| |
| if rectype in recordfilter: |
| # Datatype 6 is STRING |
| if datatype == 6: |
| if debug: |
| print('Record type = ' + str(rectype) + ' data type = ' + str(datatype) + ' length = ' + str(reclen)) |
| |
| bstring = gdsdata[dataptr + 4: dataptr + reclen] |
| repstring = bstring.replace(bsearch, brep) |
| if repstring != bstring: |
| before = gdsdata[0:dataptr] |
| after = gdsdata[dataptr + reclen:] |
| newlen = len(repstring) + 4 |
| # Record sizes must be even |
| if newlen % 2 != 0: |
| # Was original string padded with null byte? If so, |
| # remove the null byte and reduce newlen. Otherwise, |
| # add a null byte and increase newlen. |
| if bstring[-1] == 0: |
| newlen -= 1 |
| else: |
| repstring += b'\x00' |
| newlen += 1 |
| |
| bnewlen = newlen.to_bytes(2, byteorder='big') |
| brectype = rectype.to_bytes(1, byteorder='big') |
| bdatatype = datatype.to_bytes(1, byteorder='big') |
| |
| # Assemble the new record |
| newrecord = bnewlen + brectype + bdatatype + repstring |
| # Reassemble the GDS data around the new record |
| gdsdata = before + newrecord[0:newlen] + after |
| # Adjust the data end location |
| datalen += (newlen - reclen) |
| |
| if debug: |
| print('Replaced ' + str(bstring) + ' with ' + str(repstring)) |
| |
| # Advance the pointer past the data |
| dataptr += newlen |
| |
| with open(dest, 'wb') as ofile: |
| ofile.write(gdsdata) |
| |
| |
| def main(input_path, temp_dir, techfile, output): |
| global debug_print |
| ver = version_extract_from_path(input_path) |
| if ver is not None: |
| ver = "V" + ".".join([str(v) for v in ver]) |
| lib = lib_extract_from_path(input_path) |
| new_lib = convert_libname(lib) |
| |
| if input_path.find('s8pir_10r_vcells') != -1: |
| lib = '???' |
| new_lib = 'sky130_fd_pr_base' |
| assert lib is not None and new_lib is not None |
| |
| tmp_list = temp_dir + '/cell.list' |
| tmp_gds = temp_dir + '/input.gds' |
| copyfile(input_path, tmp_gds) |
| # os.system(f"strings {os.path.abspath(tmp_gds)}| grep -v lbcell |sort |uniq > {os.path.abspath(tmp_list)}") |
| os.system(f"strings {os.path.abspath(tmp_gds)}| grep -v libcell |sort |uniq |grep '{lib}' > {os.path.abspath(tmp_list)}") |
| |
| # Load list of all gds cells |
| with open(tmp_list, 'r') as in_f: |
| objects_list_from_gds = set(sorted(in_f.read().split('\n'))) |
| debug_print(" gds objects list") |
| objects_list_from_gds.remove('') |
| debug_print(objects_list_from_gds) |
| |
| #Find all cells in .lef |
| lef_file = sorted(Path("/".join(input_path.split("/")[:-2])).rglob('*.lef'))[0] |
| with open(lef_file, 'r') as in_f: |
| lef_cont = in_f.read() |
| cells_list_from_lef = set(sorted(re.findall(r'(?<=MACRO )\w+', lef_cont))) |
| debug_print(" Lef based list") |
| debug_print(cells_list_from_lef) |
| |
| # Find all verilog files and create celllist from it |
| verilog_list = sorted(Path("/".join(input_path.split("/")[:-2])).rglob('*.v')) |
| cells_list_from_verilog = [] |
| for x in verilog_list: |
| x = str(x) |
| if 'stubs' in x: |
| continue |
| cell_name = re.search(rf'{lib}a?_[^p][0-9a-z_]*[a-z0-9]', x) |
| # should not list primitives |
| """cell_name = re.search(rf'{lib}(_|a_)[A-Za-z]\w*', x) |
| if cell_name is None: |
| # search for primitives |
| cell_name = re.search(r'(?<=/)[A-Z]\w*?(?=\.v)', x) """ |
| |
| if cell_name is None: |
| debug_print(f"Didn't find name in {x}") |
| else: |
| cells_list_from_verilog.append(cell_name[0]) |
| cells_list_from_verilog = [x for x in cells_list_from_verilog if 'pg' not in x and 'lpflow' not in x] |
| cells_list_from_verilog = set(sorted(cells_list_from_verilog)) |
| |
| if debug: |
| # output .csv table |
| print("Cell_name,in .LEF,in .GDS") |
| for x in cells_list_from_verilog: |
| exists_in = lambda x,s: "Y" if x in c else "N" |
| exists_in = lambda cell,s : 'Y' if cell in s else 'N' |
| print(f"{x},{exists_in(x,cells_list_from_lef)},{exists_in(x ,objects_list_from_gds)}") |
| return |
| |
| split_gds_into(tmp_gds, os.path.abspath(techfile), tmp_list, 'gds') |
| os.remove(tmp_list) |
| os.remove(tmp_gds) |
| |
| # remove empty gds files from output |
| os.system("find " + temp_dir + " -type f -size -500c -name *.gds -exec rm {} \\;") |
| # move all files to proper directory |
| for x in Path(temp_dir).rglob('*.mag'): |
| f = str(x) |
| f_gds = f.replace('.mag', '.gds') |
| if not os.path.exists(f_gds): |
| # if .gds does not exists it does mean that magic generates empty file for that cell |
| os.remove(f) |
| continue |
| filename = f.split('/')[-1].split('.')[0] |
| filename = filename |
| mod = filename.replace(f'{new_lib}__', '') |
| |
| # replace 'input.gds` with correct file name |
| # and replace `TECHNAME` with `SKY130A` |
| with open(f,'r') as f_in: |
| mag_contents = f_in.read() |
| mag_contents = Copyright_header + mag_contents |
| mag_contents = mag_contents.replace(' TECHNAME', ' SKY130A') |
| mag_contents = mag_contents.replace(' input.gds', ' ' + filename + '.gds') |
| with open(f, 'w') as f_out: |
| f_out.write(mag_contents) |
| # Skipped for now as we are releasing without .mag files |
| ## try: |
| ## copyfile(f, create_dir_path(output, mod, new_lib, ver) + '/' + filename + '.mag') |
| ## except FileNotFoundError: |
| ## os.remove(f_gds) |
| ## print(f" Warning:File {f} not moved to target_directory, as it does not exist!") |
| ## print(f"{create_dir_path(output, mod, new_lib, ver)}") |
| os.remove(f) |
| for x in Path(temp_dir).rglob('*.lef'): |
| f = str(x) |
| f_gds = f.replace('.lef', '.gds') |
| if not os.path.exists(f_gds): |
| # if .gds does not exists it does mean that magic generates empty file for that cell |
| os.remove(f) |
| continue |
| filename = f.split('/')[-1].split('.')[0] |
| mod = filename.replace(f'{new_lib}__', '') |
| output_path = create_dir_path(output, mod, new_lib, ver) + '/' + filename + '.lef' |
| if os.path.exists(output_path): |
| print(f"Lef already exists, skipping {f}") |
| else: |
| try: |
| copyfile(f, output_path) |
| prepend_copyright(output_path) |
| except FileNotFoundError: |
| os.remove(f_gds) |
| print(f" Warning:File {f} not moved to target_directory, as it does not exist!") |
| print(f"{create_dir_path(output, mod, new_lib, ver)}") |
| os.remove(f) |
| for x in Path(temp_dir).rglob('*.gds'): |
| f = str(x) |
| change_gds_string(f, f'hkscl5hdv1_', new_lib + '__') |
| change_gds_string(f, lib + '_', new_lib + '__') |
| for x in ('scs8hd', 'scs8lp', 'scs8ls', 'scs8ms', 'scs8hd'): |
| change_gds_string(f, f'{x}_', convert_libname(x)+ '__') |
| filename = f.split('/')[-1].split('.')[0] |
| |
| mod = filename.replace(f'{new_lib}__', '') |
| copyfile(f, create_dir_path(output, mod, new_lib, ver) + '/' + filename + '.gds') |
| os.remove(f) |
| |
| def main_vcells(input_path, temp_dir, techfile, output): |
| |
| # TODO - generate list from directory |
| s8_versions = [ |
| 'V2.0.1', |
| 'V2.0.0', |
| 'V1.3.0', |
| 'V1.2.1', |
| 'V1.2.0', |
| 'V1.1.0', |
| 'V1.0.1', |
| 'V1.0.0'] |
| |
| tmp_list = temp_dir + '/cell.list' |
| tmp_gds = temp_dir + '/input.gds' |
| copyfile(input_path, tmp_gds) |
| copyfile(input_path.replace('.gds', '.list'), tmp_list) |
| |
| split_vcells_gds_into(tmp_gds, os.path.abspath(techfile), tmp_list) |
| os.remove(tmp_list) |
| os.remove(tmp_gds) |
| |
| # move all files to proper directory |
| for x in Path(temp_dir).rglob('*.mag'): |
| f = str(x) |
| filename = f.split('/')[-1].split('.')[0] |
| filename = filename |
| new_lib = filename.split('__')[0] |
| if new_lib == 'sky130_fd_pr_base': |
| lib = '' |
| elif new_lib == 'sky130_fd_pr_rf': |
| lib = 's8rf' |
| elif new_lib == 'sky130_fd_pr_rf2': |
| lib = 's8rf2' |
| |
| mod = filename.replace(f'{new_lib}__', '') |
| |
| # replace 'input.gds` with correct file name |
| # and replace `TECHNAME` with `SKY130A` |
| with open(f,'r') as f_in: |
| mag_contents = f_in.read() |
| mag_contents = Copyright_header + mag_contents |
| mag_contents = mag_contents.replace(' TECHNAME', ' SKY130A') |
| mag_contents = mag_contents.replace(' input.gds', ' ' + filename + '.gds') |
| with open(f, 'w') as f_out: |
| f_out.write(mag_contents) |
| for ver in s8_versions: |
| target_dir = create_dir_path(output, mod, new_lib, ver) |
| if not os.path.exists(target_dir): |
| os.makedirs(target_dir) |
| print(f'Moving to {target_dir}') |
| # copyfile(f, target_dir + '/' + filename + '.mag') |
| os.remove(f) |
| |
| for x in Path(temp_dir).rglob('*.lef'): |
| f = str(x) |
| f_gds = f.replace('.lef', '.gds') |
| filename = f.split('/')[-1].split('.')[0] |
| new_lib = filename.split('__')[0] |
| if new_lib == 'sky130_fd_pr_base': |
| lib = '' |
| elif new_lib == 'sky130_fd_pr_rf': |
| lib = 's8rf' |
| elif new_lib == 'sky130_fd_pr_rf2': |
| lib = 's8rf2' |
| mod = filename.replace(f'{new_lib}__', '') |
| for ver in s8_versions: |
| target_dir = create_dir_path(output, mod, new_lib, ver) |
| print(f'Moving to {target_dir}') |
| target_file = target_dir + '/' + filename + '.lef' |
| copyfile(f, target_file) |
| prepend_copyright(target_file) |
| os.remove(f) |
| for x in Path(temp_dir).rglob('*.gds'): |
| f = str(x) |
| filename = f.split('/')[-1].split('.')[0] |
| new_lib = filename.split('__')[0] |
| if new_lib == 'sky130_fd_pr_base': |
| lib = '' |
| elif new_lib == 'sky130_fd_pr_rf': |
| lib = 's8rf' |
| elif new_lib == 'sky130_fd_pr_rf2': |
| lib = 's8rf2' |
| mod = filename.replace(f'{new_lib}__', '') |
| change_gds_string(f, f'hkscl5hdv1_{mod}', new_lib + '__' + mod) |
| for x in ('scs8hd', 'scs8lp', 'scs8ls', 'scs8ms'): |
| change_gds_string(f, f'{x}_', convert_libname(x)+ '__') |
| if lib == '': |
| change_gds_string(f, mod, new_lib + '__' + mod) |
| else: |
| change_gds_string(f, lib + '_', new_lib + '__') |
| for ver in s8_versions: |
| target_dir = create_dir_path(output, mod, new_lib, ver) |
| print(f'Moving to {target_dir}') |
| copyfile(f, target_dir + '/' + filename + '.gds') |
| os.remove(f) |
| |
| |
| |
| 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=str) |
| 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() |
| temp_dir = str(args.temp / 'gds_split') |
| |
| try: |
| os.makedirs(temp_dir) |
| except FileExistsError: |
| pass |
| |
| input_files = [ |
| 'scs8hs/V0.0.0/gds/scs8hs.gds', |
| 'scs8hvl/V0.0.0/gds/scs8hvl.gds', |
| 'scs8hvl/V0.0.1/gds/scs8hvl.gds', |
| 'scs8lp/V0.0.0/gds/scs8lp.gds', |
| 'scs8ls/V0.1.0/gds/scs8ls.gds', |
| 'scs8ms/V0.0.0/gds/scs8ms.gds', |
| 'scs8hd/V0.0.1/gds/scs8hd.gds', |
| 'scs8hdll/V0.1.0/gds/scs8hdll.gds' |
| # 's8iom0s8/V0.2.0/gds/s8iom0s8.gds', |
| # 's8iom0s8/V0.1.0/gds/s8iom0s8.gds', |
| # 's8iom0s8/V0.2.1/gds/s8iom0s8.gds' |
| # 's8iom0s8/V0.0.0/gds/s8iom0s8.gds', |
| ] |
| for x in input_files: |
| path = args.input.rstrip('/') + '/' + x |
| print(path) |
| main(path, temp_dir, args.techfile, str(args.output)) |
| main_vcells(args.input.rstrip('/') + '/' + 'vcells/s8pir_10r_vcells_lvs.gds', temp_dir, args.techfile, str(args.output)) |
| os.rmdir(temp_dir) |
| |