diff --git a/Makefile b/Makefile index 4f37195..0ad4828 100644 --- a/Makefile +++ b/Makefile @@ -59,4 +59,25 @@ check: check-licenses all: README.rst @true + +LIBRARIES = $(sort $(notdir $(wildcard libraries/sky130_*_sc_*))) + +$(LIBRARIES): | $(CONDA_ENV_PYTHON) + @$(IN_CONDA_ENV) for V in libraries/$@/*; do \ + python -m skywater_pdk.liberty $$V; \ + python -m skywater_pdk.liberty $$V all; \ + python -m skywater_pdk.liberty $$V all --ccsnoise; \ + done + +sky130_fd_sc_ms-leakage: | $(CONDA_ENV_PYTHON) + @$(IN_CONDA_ENV) for V in libraries/sky130_fd_sc_ms/*; do \ + python -m skywater_pdk.liberty $$V all --leakage; \ + done + +sky130_fd_sc_ms: sky130_fd_sc_ms-leakage + +timing: $(LIBRARIES) | $(CONDA_ENV_PYTHON) + @true + + .PHONY: all diff --git a/scripts/python-skywater-pdk/skywater_pdk/liberty.py b/scripts/python-skywater-pdk/skywater_pdk/liberty.py new file mode 100755 index 0000000..0e87e61 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/liberty.py @@ -0,0 +1,697 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 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. +# +# SPDX-License-Identifier: Apache-2.0 + + +import argparse +import enum +import json +import os +import pathlib +import pprint +import re +import sys + +from collections import defaultdict + +from typing import Tuple, List, Dict + +from . import sizes + +debug = False + + +class TimingType(enum.IntFlag): + """ + + >>> TimingType.parse("ff_100C_1v65") + ('ff_100C_1v65', ) + + >>> TimingType.parse("ff_100C_1v65_ccsnoise") + ('ff_100C_1v65', ) + + >>> TimingType.basic in TimingType.ccsnoise + True + + >>> TimingType.parse("ff_100C_1v65_pwrlkg") + ('ff_100C_1v65', ) + + >>> (TimingType.basic).describe() + '' + >>> (TimingType.ccsnoise).describe() + '(with ccsnoise)' + >>> (TimingType.leakage).describe() + '(with power leakage)' + >>> (TimingType.leakage | TimingType.ccsnoise).describe() + '(with ccsnoise and power leakage)' + + >>> (TimingType.leakage | TimingType.ccsnoise).names() + 'basic, ccsnoise, leakage' + + >>> TimingType.ccsnoise.names() + 'basic, ccsnoise' + """ + + basic = 1 + + # ccsnoise files are basic files with extra 'ccsn_' values in the timing + # data. + ccsnoise = 2 | basic + + # leakage files are separate from the basic files + leakage = 4 + + def names(self): + o = [] + for t in TimingType: + if t in self: + o.append(t.name) + return ", ".join(o) + + def describe(self): + o = [] + if TimingType.ccsnoise in self: + o.append("ccsnoise") + if TimingType.leakage in self: + o.append("power leakage") + if not o: + return "" + return "(with "+" and ".join(o)+")" + + @property + def file(self): + if self == TimingType.ccsnoise: + return "_ccsnoise" + elif self == TimingType.leakage: + return "_pwrlkg" + return "" + + @classmethod + def parse(cls, name): + ttype = TimingType.basic + if name.endswith("_ccsnoise"): + name = name[:-len("_ccsnoise")] + ttype = TimingType.ccsnoise + elif name.endswith("_pwrlkg"): + name = name[:-len("_pwrlkg")] + ttype = TimingType.leakage + return name, ttype + + @property + def singular(self): + return len(self.types) == 1 + + @property + def types(self): + tt = set(t for t in TimingType if t in self) + if TimingType.ccsnoise in tt: + tt.remove(TimingType.basic) + return list(tt) + + + +def cell_corner_file(lib, cell_with_size, corner, corner_type: TimingType): + """ + + >>> cell_corner_file("sky130_fd_sc_hd", "a2111o", "ff_100C_1v65", TimingType.basic) + 'cells/a2111o/sky130_fd_sc_hd__a2111o__ff_100C_1v65.lib.json' + >>> cell_corner_file("sky130_fd_sc_hd", "a2111o_1", "ff_100C_1v65", TimingType.basic) + 'cells/a2111o/sky130_fd_sc_hd__a2111o_1__ff_100C_1v65.lib.json' + >>> cell_corner_file("sky130_fd_sc_hd", "a2111o_1", "ff_100C_1v65", TimingType.ccsnoise) + 'cells/a2111o/sky130_fd_sc_hd__a2111o_1__ff_100C_1v65_ccsnoise.lib.json' + + """ + assert corner_type.singular, (lib, cell_with_size, corner, corner_type, corner_type.types()) + + sz = sizes.parse_size(cell_with_size) + if sz: + cell = cell_with_size[:-len(sz.suffix)] + else: + cell = cell_with_size + + fname = "cells/{cell}/{lib}__{cell_sz}__{corner}{corner_type}.lib.json".format( + lib=lib, cell=cell, cell_sz=cell_with_size, corner=corner, corner_type=corner_type.file) + return fname + + +def top_corner_file(libname, corner, corner_type: TimingType): + """ + + >>> top_corner_file("sky130_fd_sc_hd", "ff_100C_1v65", TimingType.ccsnoise) + 'timing/sky130_fd_sc_hd__ff_100C_1v65_ccsnoise.lib.json' + >>> top_corner_file("sky130_fd_sc_hd", "ff_100C_1v65", TimingType.basic) + 'timing/sky130_fd_sc_hd__ff_100C_1v65.lib.json' + + """ + assert corner_type.singular, (libname, corner, corner_type, corner_type.types()) + return "timing/{libname}__{corner}{corner_type}.lib.json".format( + libname=libname, + corner=corner, corner_type=corner_type.file) + + +def collect(library_dir) -> Tuple[Dict[str, TimingType], List[str]]: + """Collect the available timing information in corners. + + Parameters + ---------- + library_dir: str + Path to a library. + + Returns + ------- + lib : str + Library name + + corners : {str: TimingType} + corners in the library. + + cells : list of str + cells in the library. + """ + + if not isinstance(library_dir, pathlib.Path): + library_dir = pathlib.Path(library_dir) + + libname0 = None + + corners = {} + all_cells = set() + for p in library_dir.rglob("*.lib.json"): + if not p.is_file(): + continue + if "timing" in str(p): + continue + + fname, fext = str(p.name).split('.', 1) + + libname, cellname, corner = fname.split("__") + if libname0 is None: + libname0 = libname + assert libname0 == libname, (libname0, libname) + + corner_name, corner_type = TimingType.parse(corner) + + if corner_name not in corners: + corners[corner_name] = [corner_type, set()] + + corners[corner_name][0] |= corner_type + corners[corner_name][1].add(cellname) + all_cells.add(cellname) + + for c in corners: + corners[c] = (corners[c][0], list(sorted(corners[c][1]))) + + assert corners, library_dir + assert all_cells, library_dir + assert libname0, library_dir + + all_cells = list(sorted(all_cells)) + + # Sanity check to make sure the corner exists for all cells. + for corner, (corner_types, corner_cells) in sorted(corners.items()): + missing = set() + for cell_with_size in all_cells: + if cell_with_size not in corner_cells: + missing.add(cell_with_size) + + if not missing: + continue + + print("Missing", ", ".join(missing), "from", corner, corner_types) + + return libname0, corners, all_cells + + for corner, (corner_types, corner_cells) in sorted(corners.items()): + for corner_type in corner_types.types: + fname = cell_corner_file(libname0, cell_with_size, corner, corner_type) + fpath = os.path.join(library_dir, fname) + if not os.path.exists(fpath) and debug: + print("Missing", (fpath, corner, corner_type, corner_types)) + + timing_dir = os.path.join(library_dir, "timing") + assert os.path.exists(timing_dir), timing_dir + for corner, (corner_types, corner_cells) in sorted(corners.items()): + for corner_type in corner_types.types: + fname = top_corner_file(libname0, corner, corner_type) + fpath = os.path.join(library_dir, fname) + if not os.path.exists(fpath) and debug: + print("Missing", (fpath, corner, corner_type, corner_types)) + + return libname0, corners, all_cells + + +def remove_ccsnoise(data): + for k, v in list(data.items()): + if "ccsn_" in k: + del data[k] + continue + + if not k.startswith("pin "): + continue + + pin_data = data[k] + + if "input_voltage" in pin_data: + del pin_data["input_voltage"] + + if "timing" not in pin_data: + continue + pin_timing = pin_data["timing"] + + for t in pin_timing: + ccsn_keys = set() + for k in t: + if not k.startswith("ccsn_"): + continue + ccsn_keys.add(k) + + for k in ccsn_keys: + del t[k] + + +def generate(library_dir, lib, corner, ocorner_type, icorner_type, cells): + top_fname = top_corner_file(lib, corner, ocorner_type).replace('.lib.json', '.lib') + top_fpath = os.path.join(library_dir, top_fname) + + top_fout = open(top_fpath, "w") + def top_write(lines): + print("\n".join(lines), file=top_fout) + + otype_str = "({} from {})".format(ocorner_type.name, icorner_type.names()) + print("Starting to write", top_fpath, otype_str, flush=True) + + common_data = {} + + common_data_path = os.path.join(library_dir, "timing", "{}__common.lib.json".format(lib)) + assert os.path.exists(common_data_path), common_data_path + with open(common_data_path) as f: + d = json.load(f) + assert isinstance(d, dict) + for k, v in d.items(): + assert k not in common_data, (k, common_data[k]) + common_data[k] = v + + top_data_path = os.path.join(library_dir, top_corner_file(lib, corner, icorner_type)) + assert os.path.exists(top_data_path), top_data_path + with open(top_data_path) as f: + d = json.load(f) + assert isinstance(d, dict) + for k, v in d.items(): + if k in common_data: + print("Overwriting", k, "with", v, "(existing value of", common_data[k], ")") + common_data[k] = v + + # Remove the ccsnoise if it exists + if ocorner_type != TimingType.ccsnoise: + remove_ccsnoise(common_data) + + output = liberty_dict("library", lib+"__"+corner, common_data) + assert output[-1] == '}', output + top_write(output[:-1]) + + for cell_with_size in cells: + fname = cell_corner_file(lib, cell_with_size, corner, icorner_type) + fpath = os.path.join(library_dir, fname) + assert os.path.exists(fpath), fpath + + with open(fpath) as f: + cell_data = json.load(f) + + # Remove the ccsnoise if it exists + if ocorner_type != TimingType.ccsnoise: + remove_ccsnoise(cell_data) + + top_write(['']) + top_write(liberty_dict("cell", "%s__%s" % (lib, cell_with_size), cell_data, [cell_with_size])) + + top_write(['']) + top_write(['}']) + top_fout.close() + print(" Finish writing", top_fpath, flush=True) + print("") + + +INDENT=" " + +# complex attribute -- (x,b) + +RE_LIBERTY_LIST = re.compile("(.*)_([0-9]+)") + +def liberty_sort(k): + """ + + >>> liberty_sort("variable_1") + (1, 'variable') + >>> liberty_sort("index_3") + (3, 'index') + >>> liberty_sort("values") # doctest: +ELLIPSIS + (inf, 'values') + + """ + m = RE_LIBERTY_LIST.match(k) + if m: + k, n = m.group(1), m.group(2) + n = int(n) + else: + n = float('inf') + return n, k + + +def is_liberty_list(k): + """ + + >>> is_liberty_list("variable_1") + True + >>> is_liberty_list("index_3") + True + >>> is_liberty_list("values") + True + """ + m = RE_LIBERTY_LIST.match(k) + if m: + k, n = m.group(1), m.group(2) + + return k in ('variable', 'index', 'values') + + +def liberty_float(f): + """ + + >>> liberty_float(1.9208818e-02) + '0.0192088180' + + >>> liberty_float(1.5) + '1.5000000000' + + >>> liberty_float(1e20) + '1.000000e+20' + + >>> liberty_float(1) + '1.0000000000' + + """ + WIDTH = len(str(0.0083333333)) + + s = json.dumps(f) + if 'e' in s: + a, b = s.split('e') + if '.' not in a: + a += '.' + while len(a)+len(b)+1 < WIDTH: + a += '0' + s = "%se%s" % (a, b) + elif '.' in s: + while len(s) < WIDTH: + s += '0' + else: + if len(s) < WIDTH: + s += '.' + while len(s) < WIDTH: + s += '0' + return s + + +def liberty_composite(k, v, i=tuple()): + """ + + >>> def pl(l): + ... print("\\n".join(l)) + + >>> pl(liberty_composite("capacitive_load_unit", [1.0, "pf"], [])) + capacitive_load_unit(1.0000000000, "pf"); + + >>> pl(liberty_composite("voltage_map", [("vpwr", 1.95), ("vss", 0.0)], [])) + voltage_map("vpwr", 1.9500000000); + voltage_map("vss", 0.0000000000); + + >>> pl(liberty_composite("library_features", 'report_delay_calculation', [])) + library_features("report_delay_calculation"); + + """ + if isinstance(v, tuple): + v = list(v) + if not isinstance(v, list): + v = [v] + #assert isinstance(v, list), (k, v) + + if isinstance(v[0], (list, tuple)): + o = [] + for j, l in enumerate(v): + o.extend(liberty_composite(k, l, i)) + return o + + o = [] + for l in v: + if isinstance(l, (float, int)): + o.append(liberty_float(l)) + elif isinstance(l, str): + assert '"' not in l, (k, v) + o.append('"%s"' % l) + else: + raise ValueError("%s - %r (%r)" % (k, l, v)) + + return ["%s%s(%s);" % (INDENT*len(i), k, ", ".join(o))] + + +def liberty_join(l): + """ + + >>> l = [5, 1.0, 10] + >>> liberty_join(l)(l) + '5.0000000000, 1.0000000000, 10.000000000' + + >>> l = [1, 5, 8] + >>> liberty_join(l)(l) + '1, 5, 8' + + """ + d = defaultdict(lambda: 0) + + for i in l: + d[type(i)] += 1 + + def types(l): + return [(i, type(i)) for i in l] + + if d[float] > 0: + assert (d[float]+d[int]) == len(l), (d, types(l)) + def join(l): + return ", ".join(liberty_float(f) for f in l) + return join + + elif d[int] > 0: + assert d[int] == len(l), (d, types(l)) + def join(l): + return ", ".join(str(f) for f in l) + return join + + raise ValueError("Invalid value: %r" % types(l)) + + +def liberty_list(k, v, i=tuple()): + o = [] + if isinstance(v[0], list): + o.append('%s%s(' % (INDENT*len(i), k)) + join = liberty_join(v[0]) + for l in v: + o.append('%s"%s", \\' % (INDENT*(len(i)+1), join(l))) + + o[1] = o[0]+o[1] + o.pop(0) + + o[-1] = o[-1][:-3] + ');' + else: + join = liberty_join(v) + o.append('%s%s("%s");' % (INDENT*len(i), k, join(v))) + + return o + + +def liberty_dict(dtype, dvalue, data, i=tuple()): + assert isinstance(data, dict), (dtype, dvalue, data) + o = [] + if dvalue: + dbits = dvalue.split(",") + for j, d in enumerate(dbits): + if '"' in d: + assert d.startswith('"'), (dvalue, dbits, i) + assert d.endswith('"'), (dvalue, dbits, i) + dbits[j] = d[1:-1] + dvalue = ','.join('"%s"' % d.strip() for d in dbits) + o.append('%s%s (%s) {' % (INDENT*len(i), dtype, dvalue)) + + i_n = list(i)+[(dtype, dvalue)] + + # Output the attribute defines first + if 'define' in data: + for d in sorted(data['define'], key=lambda d: d['group_name']+'.'+d['attribute_name']): + o.append('%sdefine(%s,%s,%s);' % (INDENT*len(i_n), d['attribute_name'], d['group_name'], d['attribute_type'])) + o.append('') + + del data['define'] + + # Output all the attributes + def attr_sort_key(a): + k, v = a + if " " in k: + ktype, kvalue = k.split(" ", 1) + else: + ktype = k + kvalue = "" + + if ktype == "comp_attribute": + ktype = kvalue + kvalue = None + + kn, ktype = liberty_sort(ktype) + + return (kn, ktype, kvalue) + + for k, v in sorted(data.items(), key=attr_sort_key): + + if " " in k: + ktype, kvalue = k.split(" ", 1) + else: + ktype = k + kvalue = "" + + if ktype == "comp_attribute": + o.extend(liberty_composite(kvalue, v, i_n)) + + elif isinstance(v, dict): + assert isinstance(v, dict), (dtype, dvalue, k, v) + o.extend(liberty_dict(ktype, kvalue, v, i_n)) + + elif isinstance(v, list): + assert len(v) > 0, (dtype, dvalue, k, v) + if isinstance(v[0], dict): + def k(o): + return o.items() + + for l in sorted(v, key=k): + o.extend(liberty_dict(ktype, kvalue, l, i_n)) + + elif is_liberty_list(k): + o.extend(liberty_list(k, v, i_n)) + + elif "clk_width" == k: + for l in sorted(v): + o.append("%s%s : %s;" % (INDENT*len(i_n), k, l)) + + else: + raise ValueError("Unknown %s: %r\n%s" % (k, v, i_n)) + + else: + if isinstance(v, str): + v = '"%s"' % v + elif isinstance(v, (float,int)): + v = liberty_float(v) + o.append("%s%s : %s;" % (INDENT*len(i_n), k, v)) + + o.append("%s}" % (INDENT*len(i))) + return o + + + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "library_path", + help="Path to the library.", + type=pathlib.Path, + nargs=1) + parser.add_argument( + "corner", + help="Corner to write output for.", + default=None, + nargs='*') + + parser.add_argument( + "--ccsnoise", + help="Include ccsnoise in file output.", + action='store_true', + default=False) + parser.add_argument( + "--leakage", + help="Include power leakage in file output.", + action='store_true', + default=False) + + args = parser.parse_args() + + libdir = args.library_path[0] + + retcode = 0 + + lib, corners, all_cells = collect(libdir) + + if args.ccsnoise: + output_corner_type = TimingType.ccsnoise + elif args.leakage: + output_corner_type = TimingType.leakage + else: + output_corner_type = TimingType.basic + + if args.corner == ['all']: + args.corner = list(sorted(k for k, (v0, v1) in corners.items() if output_corner_type in v0)) + + if args.corner: + for acorner in args.corner: + if acorner in corners: + continue + print() + print("Unknown corner:", acorner) + retcode = 1 + if retcode != 0: + args.corner.clear() + + if not args.corner: + print() + print("Available corners for", lib+":") + for k, v in sorted(corners.items()): + print(" -", k, v[0].describe()) + print() + return retcode + + print("Generating", output_corner_type.name, "liberty timing files for", lib, "at", ", ".join(args.corner)) + print() + for corner in args.corner: + input_corner_type, corner_cells = corners[corner] + if output_corner_type not in input_corner_type: + print("Corner", corner, "doesn't support", output_corner_type, "(only {})".format(input_corner_type)) + return 1 + + if output_corner_type == TimingType.basic and TimingType.ccsnoise in input_corner_type: + input_corner_type = TimingType.ccsnoise + else: + input_corner_type = output_corner_type + + generate( + libdir, lib, + corner, output_corner_type, input_corner_type, + corner_cells, + ) + return 0 + + +if __name__ == "__main__": + import doctest + doctest.testmod() + + sys.exit(main())