# See LICENSE for licensing information. # # Copyright (c) 2016-2021 Regents of the University of California and The Board # of Regents for the Oklahoma Agricultural and Mechanical College # (acting for and on behalf of Oklahoma State University) # All rights reserved. # import os import sys import re from math import sqrt import debug from gdsMill import gdsMill import tech from tech import drc, GDS from tech import layer as tech_layer from tech import layer_indices as tech_layer_indices from tech import preferred_directions from tech import layer_stacks as tech_layer_stacks from tech import active_stack as tech_active_stack from sram_factory import factory from globals import OPTS from .vector import vector from .pin_layout import pin_layout from .utils import round_to_grid from . import geometry try: from tech import special_purposes except ImportError: special_purposes = {} class layout(): """ Class consisting of a set of objs and instances for a module This provides a set of useful generic types for hierarchy management. If a module is a custom designed cell, it will read from the GDS and spice files and perform LVS/DRC. If it is dynamically generated, it should implement a constructor to create the layout/netlist and perform LVS/DRC. """ def __init__(self, name, cell_name): # This gets set in both spice and layout so either can be called first. self.name = name self.cell_name = cell_name self.gds_file = OPTS.openram_tech + "gds_lib/" + cell_name + ".gds" self.is_library_cell = os.path.isfile(self.gds_file) self.width = None self.height = None self.bounding_box = None # The rectangle shape self.bbox = None # The ll, ur coords # Holds module/cell layout instances self.insts = [] # Set of names to check for duplicates self.inst_names = set() # Holds all other objects (labels, geometries, etc) self.objs = [] # This is a mapping of internal pin names to cell pin names # If the key is not found, the internal pin names is assumed self.pin_names = {} # Holds name->pin_layout map for all pins self.pin_map = {} # List of modules we have already visited self.visited = [] self.gds_read() if "contact" not in self.name: if not hasattr(layout, "_drc_constants"): layout._drc_constants = True layout.setup_drc_constants() layout.setup_contacts() layout.setup_layer_constants() @classmethod def setup_drc_constants(layout): """ These are some DRC constants used in many places in the compiler. """ # Make some local rules for convenience for rule in drc.keys(): # Single layer width rules match = re.search(r"minwidth_(.*)", rule) if match: if match.group(1) == "active_contact": setattr(layout, "contact_width", drc(match.group(0))) else: setattr(layout, match.group(1) + "_width", drc(match.group(0))) # Single layer area rules match = re.search(r"minarea_(.*)", rule) if match: setattr(layout, match.group(0), drc(match.group(0))) # Single layer spacing rules match = re.search(r"(.*)_to_(.*)", rule) if match and match.group(1) == match.group(2): setattr(layout, match.group(1) + "_space", drc(match.group(0))) elif match and match.group(1) != match.group(2): if match.group(2) == "poly_active": setattr(layout, match.group(1) + "_to_contact", drc(match.group(0))) else: setattr(layout, match.group(0), drc(match.group(0))) match = re.search(r"(.*)_enclose_(.*)", rule) if match: setattr(layout, match.group(0), drc(match.group(0))) match = re.search(r"(.*)_extend_(.*)", rule) if match: setattr(layout, match.group(0), drc(match.group(0))) # Create the maximum well extend active that gets used # by cells to extend the wells for interaction with other cells layout.well_extend_active = 0 if "nwell" in tech_layer: layout.well_extend_active = max(layout.well_extend_active, layout.nwell_extend_active) if "pwell" in tech_layer: layout.well_extend_active = max(layout.well_extend_active, layout.pwell_extend_active) # The active offset is due to the well extension if "pwell" in tech_layer: layout.pwell_enclose_active = drc("pwell_enclose_active") else: layout.pwell_enclose_active = 0 if "nwell" in tech_layer: layout.nwell_enclose_active = drc("nwell_enclose_active") else: layout.nwell_enclose_active = 0 # Use the max of either so that the poly gates will align properly layout.well_enclose_active = max(layout.pwell_enclose_active, layout.nwell_enclose_active, layout.active_space) # These are for debugging previous manual rules level=99 debug.info(level, "poly_width".format(layout.poly_width)) debug.info(level, "poly_space".format(layout.poly_space)) debug.info(level, "m1_width".format(layout.m1_width)) debug.info(level, "m1_space".format(layout.m1_space)) debug.info(level, "m2_width".format(layout.m2_width)) debug.info(level, "m2_space".format(layout.m2_space)) debug.info(level, "m3_width".format(layout.m3_width)) debug.info(level, "m3_space".format(layout.m3_space)) debug.info(level, "m4_width".format(layout.m4_width)) debug.info(level, "m4_space".format(layout.m4_space)) debug.info(level, "active_width".format(layout.active_width)) debug.info(level, "active_space".format(layout.active_space)) debug.info(level, "contact_width".format(layout.contact_width)) debug.info(level, "poly_to_active".format(layout.poly_to_active)) debug.info(level, "poly_extend_active".format(layout.poly_extend_active)) debug.info(level, "poly_to_contact".format(layout.poly_to_contact)) debug.info(level, "active_contact_to_gate".format(layout.active_contact_to_gate)) debug.info(level, "poly_contact_to_gate".format(layout.poly_contact_to_gate)) debug.info(level, "well_enclose_active".format(layout.well_enclose_active)) debug.info(level, "implant_enclose_active".format(layout.implant_enclose_active)) debug.info(level, "implant_space".format(layout.implant_space)) @classmethod def setup_layer_constants(layout): """ These are some layer constants used in many places in the compiler. """ try: from tech import power_grid layout.pwr_grid_layers = [power_grid[0], power_grid[2]] except ImportError: layout.pwr_grid_layers = ["m3", "m4"] for layer_id in tech_layer_indices: key = "{}_stack".format(layer_id) # Set the stack as a local helper try: layer_stack = getattr(tech, key) setattr(layout, key, layer_stack) except AttributeError: pass # Skip computing the pitch for non-routing layers if layer_id in ["active", "nwell"]: continue # Add the pitch setattr(layout, "{}_pitch".format(layer_id), layout.compute_pitch(layer_id, True)) # Add the non-preferrd pitch (which has vias in the "wrong" way) setattr(layout, "{}_nonpref_pitch".format(layer_id), layout.compute_pitch(layer_id, False)) level=99 for name in tech_layer_indices: if name == "active": continue try: debug.info(level, "{0} width {1} space {2}".format(name, getattr(layout, "{}_width".format(name)), getattr(layout, "{}_space".format(name)))) debug.info(level, "pitch {0} nonpref {1}".format(getattr(layout, "{}_pitch".format(name)), getattr(layout, "{}_nonpref_pitch".format(name)))) except AttributeError: pass @staticmethod def compute_pitch(layer, preferred=True): """ This is the preferred direction pitch i.e. we take the minimum or maximum contact dimension """ # Find the layer stacks this is used in pitches = [] for stack in tech_layer_stacks: # Compute the pitch with both vias above and below (if they exist) if stack[0] == layer: pitches.append(layout.compute_layer_pitch(stack, preferred)) if stack[2] == layer: pitches.append(layout.compute_layer_pitch(stack[::-1], True)) return max(pitches) @staticmethod def get_preferred_direction(layer): return preferred_directions[layer] @staticmethod def compute_layer_pitch(layer_stack, preferred): (layer1, via, layer2) = layer_stack try: if layer1 == "poly" or layer1 == "active": contact1 = getattr(layout, layer1 + "_contact") else: contact1 = getattr(layout, layer1 + "_via") except AttributeError: contact1 = getattr(layout, layer2 + "_via") if preferred: if preferred_directions[layer1] == "V": contact_width = contact1.first_layer_width else: contact_width = contact1.first_layer_height else: if preferred_directions[layer1] == "V": contact_width = contact1.first_layer_height else: contact_width = contact1.first_layer_width layer_space = getattr(layout, layer1 + "_space") #print(layer_stack) #print(contact1) pitch = contact_width + layer_space return round_to_grid(pitch) @classmethod def setup_contacts(layout): # Set up a static for each layer to be used for measurements # unless we are a contact class! for layer_stack in tech_layer_stacks: (layer1, via, layer2) = layer_stack cont = factory.create(module_type="contact", layer_stack=layer_stack) module = sys.modules[__name__] # Also create a contact that is just the first layer if layer1 == "poly" or layer1 == "active": setattr(layout, layer1 + "_contact", cont) else: setattr(layout, layer1 + "_via", cont) # Set up a static for each well contact for measurements if "nwell" in tech_layer: cont = factory.create(module_type="contact", layer_stack=tech_active_stack, implant_type="n", well_type="n") module = sys.modules[__name__] setattr(layout, "nwell_contact", cont) if "pwell" in tech_layer: cont = factory.create(module_type="contact", layer_stack=tech_active_stack, implant_type="p", well_type="p") module = sys.modules[__name__] setattr(layout, "pwell_contact", cont) ############################################################ # GDS layout ############################################################ def offset_all_coordinates(self, offset=None): """ This function is called after everything is placed to shift the origin in the lowest left corner """ if not offset: offset = vector(0, 0) ll = self.find_lowest_coords() real_offset = ll + offset self.translate_all(real_offset) return real_offset def offset_x_coordinates(self, offset=None): """ This function is called after everything is placed to shift the origin to the furthest left point. Y offset is unchanged. """ if not offset: offset = vector(0, 0) ll = self.find_lowest_coords() real_offset = ll.scale(1, 0) + offset self.translate_all(real_offset) return real_offset def get_gate_offset(self, x_offset, height, inv_num): """ Gets the base offset and y orientation of stacked rows of gates assuming a minwidth metal1 vdd/gnd rail. Input is which gate in the stack from 0..n """ if (inv_num % 2 == 0): base_offset = vector(x_offset, inv_num * height) y_dir = 1 else: # we lose a rail after every 2 gates base_offset = vector(x_offset, (inv_num + 1) * height - \ (inv_num % 2) * drc["minwidth_m1"]) y_dir = -1 return (base_offset, y_dir) def find_lowest_coords(self): """ Finds the lowest set of 2d cartesian coordinates within this layout """ lowestx = lowesty = sys.maxsize if len(self.objs) > 0: lowestx = min(min(obj.lx() for obj in self.objs if obj.name != "label"), lowestx) lowesty = min(min(obj.by() for obj in self.objs if obj.name != "label"), lowesty) if len(self.insts) > 0: lowestx = min(min(inst.lx() for inst in self.insts), lowestx) lowesty = min(min(inst.by() for inst in self.insts), lowesty) if len(self.pin_map) > 0: for pin_set in self.pin_map.values(): if len(pin_set) == 0: continue lowestx = min(min(pin.lx() for pin in pin_set), lowestx) lowesty = min(min(pin.by() for pin in pin_set), lowesty) return vector(lowestx, lowesty) def find_highest_coords(self): """ Finds the highest set of 2d cartesian coordinates within this layout """ highestx = highesty = -sys.maxsize - 1 if len(self.objs) > 0: highestx = max(max(obj.rx() for obj in self.objs if obj.name != "label"), highestx) highesty = max(max(obj.uy() for obj in self.objs if obj.name != "label"), highesty) if len(self.insts) > 0: highestx = max(max(inst.rx() for inst in self.insts), highestx) highesty = max(max(inst.uy() for inst in self.insts), highesty) if len(self.pin_map) > 0: for pin_set in self.pin_map.values(): if len(pin_set) == 0: continue highestx = max(max(pin.rx() for pin in pin_set), highestx) highesty = max(max(pin.uy() for pin in pin_set), highesty) return vector(highestx, highesty) def find_highest_layer_coords(self, layer): """ Finds the highest set of 2d cartesian coordinates within this layout on a layer """ # Only consider the layer not the purpose for now layerNumber = tech_layer[layer][0] try: highestx = max(obj.rx() for obj in self.objs if obj.layerNumber == layerNumber) except ValueError: highestx =0 try: highesty = max(obj.uy() for obj in self.objs if obj.layerNumber == layerNumber) except ValueError: highesty = 0 for inst in self.insts: # This really should be rotated/mirrored etc... subcoord = inst.mod.find_highest_layer_coords(layer) + inst.offset highestx = max(highestx, subcoord.x) highesty = max(highesty, subcoord.y) return vector(highestx, highesty) def find_lowest_layer_coords(self, layer): """ Finds the highest set of 2d cartesian coordinates within this layout on a layer """ # Only consider the layer not the purpose for now layerNumber = tech_layer[layer][0] try: lowestx = min(obj.lx() for obj in self.objs if obj.layerNumber == layerNumber) except ValueError: lowestx = 0 try: lowesty = min(obj.by() for obj in self.objs if obj.layerNumber == layerNumber) except ValueError: lowesty = 0 for inst in self.insts: # This really should be rotated/mirrored etc... subcoord = inst.mod.find_lowest_layer_coords(layer) + inst.offset lowestx = min(lowestx, subcoord.x) lowesty = min(lowesty, subcoord.y) return vector(lowestx, lowesty) def translate_all(self, offset): """ Translates all objects, instances, and pins by the given (x,y) offset """ for obj in self.objs: obj.offset = vector(obj.offset - offset) for inst in self.insts: inst.offset = vector(inst.offset - offset) # The instances have a precomputed boundary that we need to update. if inst.__class__.__name__ == "instance": inst.compute_boundary(inst.offset, inst.mirror, inst.rotate) for pin_name in self.pin_map.keys(): # All the pins are absolute coordinates that need to be updated. pin_list = self.pin_map[pin_name] for pin in pin_list: pin.rect = [pin.ll() - offset, pin.ur() - offset] def add_inst(self, name, mod, offset=[0, 0], mirror="R0", rotate=0): """ Adds an instance of a mod to this module """ # Contacts are not really instances, so skip them if "contact" not in mod.name: # Check that the instance name is unique debug.check(name not in self.inst_names, "Duplicate named instance in {0}: {1}".format(self.cell_name, name)) self.mods.add(mod) self.inst_names.add(name) self.insts.append(geometry.instance(name, mod, offset, mirror, rotate)) debug.info(3, "adding instance {}".format(self.insts[-1])) # This is commented out for runtime reasons # debug.info(4, "instance list: " + ",".join(x.name for x in self.insts)) return self.insts[-1] def get_inst(self, name): """ Retrieve an instance by name """ for inst in self.insts: if inst.name == name: return inst return None def add_flat_inst(self, name, mod, offset=[0, 0]): """ Copies all of the items in instance into this module """ for item in mod.objs: item.offset += offset self.objs.append(item) for item in mod.insts: item.offset += offset self.insts.append(item) debug.check(len(item.mod.pins) == 0, "Cannot add flat instance with subinstances.") self.connect_inst([]) debug.info(3, "adding flat instance {}".format(name)) return None def add_rect(self, layer, offset, width=None, height=None): """ Adds a rectangle on a given layer,offset with width and height """ if not width: width = drc["minwidth_{}".format(layer)] if not height: height = drc["minwidth_{}".format(layer)] lpp = tech_layer[layer] self.objs.append(geometry.rectangle(lpp, offset, width, height)) return self.objs[-1] def add_rect_center(self, layer, offset, width=None, height=None): """ Adds a rectangle on a given layer at the center point with width and height """ if not width: width = drc["minwidth_{}".format(layer)] if not height: height = drc["minwidth_{}".format(layer)] lpp = tech_layer[layer] corrected_offset = offset - vector(0.5 * width, 0.5 * height) self.objs.append(geometry.rectangle(lpp, corrected_offset, width, height)) return self.objs[-1] def add_segment_center(self, layer, start, end, width=None): """ Add a min-width rectanglular segment using center line on the start to end point """ if not width: width = drc["minwidth_{}".format(layer)] if start.x != end.x and start.y != end.y: debug.error("Nonrectilinear center rect!", -1) elif start.x != end.x: offset = vector(0, 0.5 * width) return self.add_rect(layer, start - offset, end.x - start.x, width) else: offset = vector(0.5 * width, 0) return self.add_rect(layer, start - offset, width, end.y - start.y) def get_tx_insts(self, tx_type=None): """ Return a list of the instances of given tx type. """ tx_list = [] for i in self.insts: try: if tx_type and i.mod.tx_type == tx_type: tx_list.append(i) elif not tx_type: if i.mod.tx_type == "nmos" or i.mod.tx_type == "pmos": tx_list.append(i) except AttributeError: pass return tx_list def get_pin(self, text): """ Return the pin or list of pins """ name = self.get_pin_name(text) try: if len(self.pin_map[name]) > 1: debug.error("Should use a pin iterator since more than one pin {}".format(text), -1) # If we have one pin, return it and not the list. # Otherwise, should use get_pins() any_pin = next(iter(self.pin_map[name])) return any_pin except Exception: self.gds_write("missing_pin.gds") debug.error("No pin found with name {0} on {1}. Saved as missing_pin.gds.".format(name, self.cell_name), -1) def get_pins(self, text): """ Return a pin list (instead of a single pin) """ name = self.get_pin_name(text) if name in self.pin_map.keys(): return self.pin_map[name] else: return set() def add_pin_names(self, pin_dict): """ Create a mapping from internal pin names to external pin names. """ self.pin_names = pin_dict self.original_pin_names = {y: x for (x, y) in self.pin_names.items()} def get_pin_name(self, text): """ Return the custom cell pin name """ if text in self.pin_names: return self.pin_names[text] else: return text def get_original_pin_names(self): """ Return the internal cell pin name """ # This uses the hierarchy_spice pins (in order) return [self.get_original_pin_name(x) for x in self.pins] def get_original_pin_name(self, text): """ Return the internal cell pin names in custom port order """ if text in self.original_pin_names: return self.original_pin_names[text] else: return text def get_pin_names(self): """ Return a pin list of all pins """ return self.pins def copy_layout_pin(self, instance, pin_name, new_name=""): """ Create a copied version of the layout pin at the current level. You can optionally rename the pin to a new name. """ pins = instance.get_pins(pin_name) if len(pins) == 0: debug.warning("Could not find pin {0} on {1}".format(pin_name, instance.mod.name)) for pin in pins: if new_name == "": new_name = pin_name self.add_layout_pin(new_name, pin.layer, pin.ll(), pin.width(), pin.height()) def connect_row_locs(self, from_layer, to_layer, locs, name=None, full=False): """ Connects left/right rows that are aligned on the given layer. """ bins = {} for loc in locs: y = pin.y try: bins[y].append(loc) except KeyError: bins[y] = [loc] for y, v in bins.items(): # Not enough to route a pin, so just copy them if len(v) < 2: continue if full: left_x = 0 right_x = self.width else: left_x = min([loc.x for loc in v]) right_x = max([loc.x for loc in v]) left_pos = vector(left_x, y) right_pos = vector(right_x, y) # Make sure to add vias to the new route for loc in v: self.add_via_stack_center(from_layer=from_layer, to_layer=to_layer, offset=loc, min_area=True) if name: self.add_layout_pin_segment_center(text=name, layer=to_layer, start=left_pos, end=right_pos) else: self.add_segment_center(layer=to_layer, start=left_pos, end=right_pos) def connect_row_pins(self, layer, pins, name=None, full=False): """ Connects left/right rows that are aligned. """ bins = {} for pin in pins: y = pin.cy() try: bins[y].append(pin) except KeyError: bins[y] = [pin] for y, v in bins.items(): # Not enough to route a pin, so just copy them if len(v) < 2: continue if full: left_x = 0 right_x = self.width else: left_x = min([pin.lx() for pin in v]) right_x = max([pin.rx() for pin in v]) left_pos = vector(left_x, y) right_pos = vector(right_x, y) # Make sure to add vias to the new route for pin in v: self.add_via_stack_center(from_layer=pin.layer, to_layer=layer, offset=pin.center(), min_area=True) if name: self.add_layout_pin_segment_center(text=name, layer=layer, start=left_pos, end=right_pos) else: self.add_segment_center(layer=layer, start=left_pos, end=right_pos) def connect_col_locs(self, from_layer, to_layer, locs, name=None, full=False): """ Connects top/bot columns that are aligned. """ bins = {} for loc in locs: x = loc.x try: bins[x].append(loc) except KeyError: bins[x] = [loc] for x, v in bins.items(): # Not enough to route a pin, so just copy them if len(v) < 2: continue if full: bot_y = 0 top_y = self.height else: bot_y = min([loc.y for loc in v]) top_y = max([loc.y for loc in v]) top_pos = vector(x, top_y) bot_pos = vector(x, bot_y) # Make sure to add vias to the new route for loc in v: self.add_via_stack_center(from_layer=from_layer, to_layer=to_layer, offset=loc, min_area=True) if name: self.add_layout_pin_segment_center(text=name, layer=to_layer, start=top_pos, end=bot_pos) else: self.add_segment_center(layer=to_layer, start=top_pos, end=bot_pos) def connect_col_pins(self, layer, pins, name=None, full=False): """ Connects top/bot columns that are aligned. """ bins = {} for pin in pins: x = pin.cx() try: bins[x].append(pin) except KeyError: bins[x] = [pin] for x, v in bins.items(): # Not enough to route a pin, so just copy them if len(v) < 2: continue if full: bot_y = 0 top_y = self.height else: bot_y = min([pin.by() for pin in v]) top_y = max([pin.uy() for pin in v]) top_pos = vector(x, top_y) bot_pos = vector(x, bot_y) # Make sure to add vias to the new route for pin in v: self.add_via_stack_center(from_layer=pin.layer, to_layer=layer, offset=pin.center(), min_area=True) if name: self.add_layout_pin_segment_center(text=name, layer=layer, start=top_pos, end=bot_pos) else: self.add_segment_center(layer=layer, start=top_pos, end=bot_pos) def get_metal_layers(self, from_layer, to_layer): from_id = tech_layer_indices[from_layer] to_id = tech_layer_indices[to_layer] layer_list = [x for x in tech_layer_indices.keys() if tech_layer_indices[x] >= from_id and tech_layer_indices[x] < to_id] return layer_list def route_vertical_pins(self, name, insts=None, layer=None, xside="cx", yside="cy", full_width=True): """ Route together all of the pins of a given name that vertically align. Uses local_insts if insts not specified. Uses center of pin by default, or right or left if specified. TODO: Add equally spaced option for IR drop min, right now just 2 """ bins = {} if not insts: insts = self.local_insts for inst in insts: for pin in inst.get_pins(name): x = getattr(pin, xside)() try: bins[x].append((inst,pin)) except KeyError: bins[x] = [(inst,pin)] for x, v in bins.items(): # Not enough to route a pin, so just copy them if len(v) < 2: debug.warning("Pins don't align well so copying pins instead of connecting with pin.") for inst,pin in v: self.add_layout_pin(pin.name, pin.layer, pin.ll(), pin.width(), pin.height()) continue last_via = None pin_layer = None for inst,pin in v: if layer: pin_layer = layer else: pin_layer = self.supply_stack[2] y = getattr(pin, yside)() last_via = self.add_via_stack_center(from_layer=pin.layer, to_layer=pin_layer, offset=vector(x, y)) if last_via: via_width=last_via.mod.second_layer_width via_height=last_via.mod.second_layer_height else: via_width=None via_height=0 bot_y = min([pin.by() for (inst,pin) in v]) top_y = max([pin.uy() for (inst,pin) in v]) if full_width: bot_y = min(0, bot_y) top_y = max(self.height, top_y) top_pos = vector(x, top_y + 0.5 * via_height) bot_pos = vector(x, bot_y - 0.5 * via_height) # self.add_layout_pin_rect_ends(name=name, # layer=pin_layer, # start=top_pos, # end=bot_pos, # width=via_width) self.add_layout_pin_segment_center(text=name, layer=pin_layer, start=top_pos, end=bot_pos, width=via_width) def add_layout_pin_rect_ends(self, name, layer, start, end, width=None): # This adds pins on the end connected by a segment top_rect = self.add_layout_pin_rect_center(text=name, layer=layer, offset=start) bot_rect = self.add_layout_pin_rect_center(text=name, layer=layer, offset=end) # This is made to not overlap with the pin above # so that the power router will only select a small pin. # Otherwise it adds big blockages over the rails. if start.y != end.y: self.add_segment_center(layer=layer, start=bot_rect.uc(), end=top_rect.bc()) else: self.add_segment_center(layer=layer, start=bot_rect.rc(), end=top_rect.lc()) return (bot_rect, top_rect) def route_horizontal_pins(self, name, insts=None, layer=None, xside="cx", yside="cy", full_width=True): """ Route together all of the pins of a given name that horizontally align. Uses local_insts if insts not specified. Uses center of pin by default, or top or botom if specified. TODO: Add equally spaced option for IR drop min, right now just 2 """ bins = {} if not insts: insts = self.local_insts for inst in insts: for pin in inst.get_pins(name): y = getattr(pin, yside)() try: bins[y].append((inst,pin)) except KeyError: bins[y] = [(inst,pin)] # Filter the small bins for y, v in bins.items(): if len(v) < 2: debug.warning("Pins don't align well so copying pins instead of connecting with pin.") for inst,pin in v: self.add_layout_pin(pin.name, pin.layer, pin.ll(), pin.width(), pin.height()) continue last_via = None pin_layer = None for inst,pin in v: if layer: pin_layer = layer else: pin_layer = self.supply_stack[0] x = getattr(pin, xside)() last_via = self.add_via_stack_center(from_layer=pin.layer, to_layer=pin_layer, offset=vector(x, y), min_area=True) if last_via: via_height=last_via.mod.second_layer_height via_width=last_via.mod.second_layer_width else: via_height=None via_width=0 left_x = min([pin.lx() for (inst,pin) in v]) right_x = max([pin.rx() for (inst,pin) in v]) if full_width: left_x = min(0, left_x) right_x = max(self.width, right_x) left_pos = vector(left_x + 0.5 * via_width, y) right_pos = vector(right_x + 0.5 * via_width, y) # self.add_layout_pin_rect_ends(name=name, # layer=pin_layer, # start=left_pos, # end=right_pos, # width=via_height) self.add_layout_pin_segment_center(text=name, layer=pin_layer, start=left_pos, end=right_pos, width=via_height) def add_layout_end_pin_segment_center(self, text, layer, start, end): """ Creates a path with two pins on the end that don't overlap. """ start_pin = self.add_layout_pin_rect_center(text=text, layer=layer, offset=start) end_pin = self.add_layout_pin_rect_center(text=text, layer=layer, offset=end) if start.x != end.x and start.y != end.y: file_name = "non_rectilinear.gds" self.gds_write(file_name) debug.error("Cannot have a non-manhatten layout pin: {}".format(file_name), -1) elif start.x != end.x: self.add_segment_center(layer=layer, start=start_pin.rc(), end=end_pin.lc()) elif start.y != end.y: self.add_segment_center(layer=layer, start=start_pin.uc(), end=end_pin.bc()) else: debug.error("Cannot have a point pin.", -1) def add_layout_pin_segment_center(self, text, layer, start, end, width=None): """ Creates a path like pin with center-line convention """ if start.x != end.x and start.y != end.y: file_name = "non_rectilinear.gds" self.gds_write(file_name) debug.error("Cannot have a non-manhatten layout pin: {}".format(file_name), -1) if not width: layer_width = drc["minwidth_{}".format(layer)] else: layer_width = width # one of these will be zero bbox_width = max(start.x, end.x) - min(start.x, end.x) bbox_height = max(start.y, end.y) - min(start.y, end.y) ll_offset = vector(min(start.x, end.x), min(start.y, end.y)) # Shift it down 1/2 a width in the 0 dimension if bbox_height == 0: ll_offset -= vector(0, 0.5 * layer_width) if bbox_width == 0: ll_offset -= vector(0.5 * layer_width, 0) return self.add_layout_pin(text=text, layer=layer, offset=ll_offset, width=max(bbox_width, layer_width), height=max(bbox_height, layer_width)) def add_layout_pin_rect_center(self, text, layer, offset, width=None, height=None): """ Creates a path like pin with center-line convention """ if not width: width = drc["minwidth_{0}".format(layer)] if not height: height = drc["minwidth_{0}".format(layer)] ll_offset = offset - vector(0.5 * width, 0.5 * height) return self.add_layout_pin(text, layer, ll_offset, width, height) def remove_layout_pin(self, text): """ Delete a labeled pin (or all pins of the same name) """ self.pin_map[text] = set() def remove_layout_pins(self): """ Delete all the layout pins """ self.pin_map = {} def copy_layout_pin_shapes(self, text): """ Copy the shapes of the layout pins as objects. """ for s in self.pin_map[text]: self.add_rect(layer=s.layer, offset=s.ll(), width=s.width(), height=s.height()) def replace_layout_pin(self, text, pin): """ Remove the old pin and replace with a new one """ # Keep the shapes as they were used to connect to the router pins self.copy_layout_pin_shapes(text) # Remove the shapes as actual pins self.remove_layout_pin(text) # Add the new pin self.add_layout_pin(text=text, layer=pin.layer, offset=pin.ll(), width=pin.width(), height=pin.height()) def add_layout_pin(self, text, layer, offset, width=None, height=None): """ Create a labeled pin """ if not width: width = drc["minwidth_{0}".format(layer)] if not height: height = drc["minwidth_{0}".format(layer)] new_pin = pin_layout(text, [offset, offset + vector(width, height)], layer) try: # Check if there's a duplicate! # and if so, silently ignore it. # Rounding errors may result in some duplicates. if new_pin not in self.pin_map[text]: self.pin_map[text].add(new_pin) except KeyError: self.pin_map[text] = set() self.pin_map[text].add(new_pin) return new_pin def add_label_pin(self, text, layer, offset, width=None, height=None): """ Create a labeled pin WITHOUT the pin data structure. This is not an actual pin but a named net so that we can add a correspondence point in LVS. """ if not width: width = drc["minwidth_{0}".format(layer)] if not height: height = drc["minwidth_{0}".format(layer)] self.add_rect(layer=layer, offset=offset, width=width, height=height) self.add_label(text=text, layer=layer, offset=offset + vector(0.5 * width, 0.5 * height)) def add_label(self, text, layer, offset=[0, 0], zoom=None): """Adds a text label on the given layer,offset, and zoom level""" debug.info(5, "add label " + str(text) + " " + layer + " " + str(offset)) lpp = tech_layer[layer] self.objs.append(geometry.label(text, lpp, offset, zoom)) return self.objs[-1] def add_path(self, layer, coordinates, width=None): """Connects a routing path on given layer,coordinates,width.""" debug.info(4, "add path " + str(layer) + " " + str(coordinates)) from . import wire_path # NOTE: (UNTESTED) add_path(...) is currently not used # lpp = tech_layer[layer] # self.objs.append(geometry.path(lpp, coordinates, width)) wire_path(obj=self, layer=layer, position_list=coordinates, width=width) def add_route(self, layers, coordinates, layer_widths): """Connects a routing path on given layer,coordinates,width. The layers are the (horizontal, via, vertical). add_wire assumes preferred direction routing whereas this includes layers in the coordinates. """ from . import route debug.info(4, "add route " + str(layers) + " " + str(coordinates)) # add an instance of our path that breaks down into rectangles and contacts route(obj=self, layer_stack=layers, path=coordinates, layer_widths=layer_widths) def add_zjog(self, layer, start, end, first_direction="H", var_offset=0.5, fixed_offset=None): """ Add a simple jog at the halfway point. If layer is a single value, it is a path. If layer is a tuple, it is a wire with preferred directions. """ neg_offset = 1.0 - var_offset # vertical first if first_direction == "V": if fixed_offset: mid1 = vector(start.x, fixed_offset) else: mid1 = vector(start.x, neg_offset * start.y + var_offset * end.y) mid2 = vector(end.x, mid1.y) # horizontal first elif first_direction == "H": if fixed_offset: mid1 = vector(fixed_offset, start.y) else: mid1 = vector(neg_offset * start.x + var_offset * end.x, start.y) mid2 = vector(mid1, end.y) else: debug.error("Invalid direction for jog -- must be H or V.") if layer in tech_layer_stacks: self.add_wire(layer, [start, mid1, mid2, end]) elif layer in tech_layer: self.add_path(layer, [start, mid1, mid2, end]) else: debug.error("Could not find layer {}".format(layer)) def add_horizontal_zjog_path(self, layer, start, end): """ Add a simple jog at the halfway point """ # horizontal first mid1 = vector(0.5 * start.x + 0.5 * end.x, start.y) mid2 = vector(mid1, end.y) self.add_path(layer, [start, mid1, mid2, end]) def add_wire(self, layers, coordinates, widen_short_wires=True): """Connects a routing path on given layer,coordinates,width. The layers are the (horizontal, via, vertical). """ from . import wire # add an instance of our path that breaks down # into rectangles and contacts wire(obj=self, layer_stack=layers, position_list=coordinates, widen_short_wires=widen_short_wires) def add_via(self, layers, offset, size=[1, 1], directions=None, implant_type=None, well_type=None): """ Add a three layer via structure. """ from sram_factory import factory via = factory.create(module_type="contact", layer_stack=layers, dimensions=size, directions=directions, implant_type=implant_type, well_type=well_type) inst = self.add_inst(name=via.name, mod=via, offset=offset) # We don't model the logical connectivity of wires/paths self.connect_inst([]) return inst def add_via_center(self, layers, offset, directions=None, size=[1, 1], implant_type=None, well_type=None): """ Add a three layer via structure by the center coordinate accounting for mirroring and rotation. """ from sram_factory import factory via = factory.create(module_type="contact", layer_stack=layers, dimensions=size, directions=directions, implant_type=implant_type, well_type=well_type) height = via.height width = via.width corrected_offset = offset + vector(-0.5 * width, -0.5 * height) inst = self.add_inst(name=via.name, mod=via, offset=corrected_offset) # We don't model the logical connectivity of wires/paths self.connect_inst([]) return inst def add_via_stack_center(self, offset, from_layer, to_layer, directions=None, size=[1, 1], implant_type=None, well_type=None, min_area=False): """ Punch a stack of vias from a start layer to a target layer by the center. """ if from_layer == to_layer: # In the case where we have no vias added, make sure that there is at least # a metal enclosure. This helps with center-line path routing. self.add_rect_center(layer=from_layer, offset=offset) return None intermediate_layers = self.get_metal_layers(from_layer, to_layer) via = None cur_layer = from_layer while cur_layer != to_layer: from_id = tech_layer_indices[cur_layer] to_id = tech_layer_indices[to_layer] if from_id < to_id: # grow the stack up search_id = 0 next_id = 2 else: # grow the stack down search_id = 2 next_id = 0 curr_stack = next(filter(lambda stack: stack[search_id] == cur_layer, tech_layer_stacks), None) via = self.add_via_center(layers=curr_stack, size=size, offset=offset, directions=directions, implant_type=implant_type, well_type=well_type) # Only add the enclosure if we are in an intermediate layer # or we are forced to if min_area or cur_layer in intermediate_layers: self.add_min_area_rect_center(cur_layer, offset, via.mod.first_layer_width, via.mod.first_layer_height) cur_layer = curr_stack[next_id] return via def add_min_area_rect_center(self, layer, offset, width=None, height=None): """ Add a minimum area retcangle at the given point. Either width or height should be fixed. """ min_area = drc("minarea_{}".format(layer)) if min_area == 0: return min_width = drc("minwidth_{}".format(layer)) if preferred_directions[layer] == "V": new_height = max(min_area / width, min_width) new_width = width else: new_width = max(min_area / height, min_width) new_height = height debug.check(min_area <= round_to_grid(new_height*new_width), "Min area violated.") self.add_rect_center(layer=layer, offset=offset, width=new_width, height=new_height) def add_ptx(self, offset, mirror="R0", rotate=0, width=1, mults=1, tx_type="nmos"): """Adds a ptx module to the design.""" import ptx mos = ptx.ptx(width=width, mults=mults, tx_type=tx_type) inst = self.add_inst(name=mos.name, mod=mos, offset=offset, mirror=mirror, rotate=rotate) return inst def gds_read(self): """Reads a GDSII file in the library and checks if it exists Otherwise, start a new layout for dynamic generation.""" if OPTS.netlist_only: self.gds = None return # open the gds file if it exists or else create a blank layout if os.path.isfile(self.gds_file): debug.info(3, "opening {}".format(self.gds_file)) self.gds = gdsMill.VlsiLayout(units=GDS["unit"]) reader = gdsMill.Gds2reader(self.gds) reader.loadFromFile(self.gds_file, special_purposes) else: debug.info(3, "Creating layout structure {}".format(self.name)) self.gds = gdsMill.VlsiLayout(name=self.name, units=GDS["unit"]) def print_gds(self, gds_file=None): """Print the gds file (not the vlsi class) to the terminal """ if not gds_file: gds_file = self.gds_file debug.info(4, "Printing {}".format(gds_file)) arrayCellLayout = gdsMill.VlsiLayout(units=GDS["unit"]) reader = gdsMill.Gds2reader(arrayCellLayout, debugToTerminal=1) reader.loadFromFile(gds_file, special_purposes) def clear_visited(self): """ Recursively clear the visited flag """ self.visited = [] def gds_write_file(self, gds_layout): """Recursive GDS write function""" # Visited means that we already prepared self.gds for this subtree if self.name in self.visited: return for i in self.insts: i.gds_write_file(gds_layout) for i in self.objs: i.gds_write_file(gds_layout) for pin_name in self.pin_map.keys(): for pin in self.pin_map[pin_name]: pin.gds_write_file(gds_layout) # If it's not a premade cell # and we didn't add our own boundary, # we should add a boundary just for DRC in some technologies if not self.is_library_cell and not self.bounding_box: # If there is a boundary layer, and we didn't create one, add one. boundary_layers = [] if "boundary" in tech_layer.keys(): boundary_layers.append("boundary") if "stdc" in tech_layer.keys(): boundary_layers.append("stdc") boundary = [self.find_lowest_coords(), self.find_highest_coords()] debug.check(boundary[0] and boundary[1], "No shapes to make a boundary.") height = boundary[1][1] - boundary[0][1] width = boundary[1][0] - boundary[0][0] for boundary_layer in boundary_layers: (layer_number, layer_purpose) = tech_layer[boundary_layer] gds_layout.addBox(layerNumber=layer_number, purposeNumber=layer_purpose, offsetInMicrons=boundary[0], width=width, height=height, center=False) debug.info(4, "Adding {0} boundary {1}".format(self.name, boundary)) self.visited.append(self.name) def gds_write(self, gds_name): """Write the entire gds of the object to the file.""" debug.info(3, "Writing to {}".format(gds_name)) # If we already wrote a GDS, we need to reset and traverse it again in # case we made changes. if not self.is_library_cell and self.visited: debug.info(3, "Creating layout structure {}".format(self.name)) self.gds = gdsMill.VlsiLayout(name=self.name, units=GDS["unit"]) writer = gdsMill.Gds2writer(self.gds) # MRG: 3/2/18 We don't want to clear the visited flag since # this would result in duplicates of all instances being placed in self.gds # which may have been previously processed! # MRG: 10/4/18 We need to clear if we make changes and write a second GDS! self.clear_visited() # recursively create all the remaining objects self.gds_write_file(self.gds) # populates the xyTree data structure for gds # self.gds.prepareForWrite() writer.writeToFile(gds_name) debug.info(3, "Done writing to {}".format(gds_name)) def get_boundary(self): """ Return the lower-left and upper-right coordinates of boundary """ # This assumes nothing spans outside of the width and height! return [vector(0, 0), vector(self.width, self.height)] #return [self.find_lowest_coords(), self.find_highest_coords()] def get_blockages(self, layer, top_level=False): """ Write all of the obstacles in the current (and children) modules to the lef file. Do not write the pins since they aren't obstructions. """ if type(layer) == str: lpp = tech_layer[layer] else: lpp = layer blockages = [] for i in self.objs: blockages += i.get_blockages(lpp) for i in self.insts: blockages += i.get_blockages(lpp) # Must add pin blockages to non-top cells if not top_level: blockages += self.get_pin_blockages(lpp) return blockages def get_pin_blockages(self, lpp): """ Return the pin shapes as blockages for non-top-level blocks. """ # FIXME: We don't have a body contact in ptx, so just ignore it for now import copy pin_names = copy.deepcopy(self.pins) if self.name.startswith("pmos") or self.name.startswith("nmos"): pin_names.remove("B") blockages = [] for pin_name in pin_names: pin_list = self.get_pins(pin_name) for pin in pin_list: if pin.same_lpp(pin.lpp, lpp): blockages += [pin.rect] return blockages def create_horizontal_pin_bus(self, layer, offset, names, length, pitch=None): """ Create a horizontal bus of pins. """ return self.create_bus(layer, offset, names, length, vertical=False, make_pins=True, pitch=pitch) def create_vertical_pin_bus(self, layer, offset, names, length, pitch=None): """ Create a horizontal bus of pins. """ return self.create_bus(layer, offset, names, length, vertical=True, make_pins=True, pitch=pitch) def create_vertical_bus(self, layer, offset, names, length, pitch=None): """ Create a horizontal bus. """ return self.create_bus(layer, offset, names, length, vertical=True, make_pins=False, pitch=pitch) def create_horizontal_bus(self, layer, offset, names, length, pitch=None): """ Create a horizontal bus. """ return self.create_bus(layer, offset, names, length, vertical=False, make_pins=False, pitch=pitch) def create_bus(self, layer, offset, names, length, vertical, make_pins, pitch=None): """ Create a horizontal or vertical bus. It can be either just rectangles, or actual layout pins. It returns an map of line center line positions indexed by name. The other coordinate is a 0 since the bus provides a range. TODO: combine with channel router. """ # half minwidth so we can return the center line offsets half_minwidth = 0.5 * drc["minwidth_{}".format(layer)] if not pitch: pitch = getattr(self, "{}_pitch".format(layer)) pins = {} if vertical: for i in range(len(names)): line_offset = offset + vector(i * pitch, 0) if make_pins: new_pin = self.add_layout_pin(text=names[i], layer=layer, offset=line_offset, height=length) else: rect = self.add_rect(layer=layer, offset=line_offset, height=length) new_pin = pin_layout(names[i], [rect.ll(), rect.ur()], layer) pins[names[i]] = new_pin else: for i in range(len(names)): line_offset = offset + vector(0, i * pitch + half_minwidth) if make_pins: new_pin = self.add_layout_pin(text=names[i], layer=layer, offset=line_offset, width=length) else: rect = self.add_rect(layer=layer, offset=line_offset, width=length) new_pin = pin_layout(names[i], [rect.ll(), rect.ur()], layer) pins[names[i]] = new_pin return pins def connect_horizontal_bus(self, mapping, inst, bus_pins, layer_stack=("m1", "via1", "m2")): """ Horizontal version of connect_bus. """ self.connect_bus(mapping, inst, bus_pins, layer_stack, True) def connect_vertical_bus(self, mapping, inst, bus_pins, layer_stack=("m1", "via1", "m2")): """ Vertical version of connect_bus. """ self.connect_bus(mapping, inst, bus_pins, layer_stack, False) def connect_bus(self, mapping, inst, bus_pins, layer_stack, horizontal): """ Connect a mapping of pin -> name for a bus. This could be replaced with a channel router in the future. NOTE: This has only really been tested with point-to-point connections (not multiple pins on a net). """ (horizontal_layer, via_layer, vertical_layer) = layer_stack if horizontal: route_layer = vertical_layer bus_layer = horizontal_layer else: route_layer = horizontal_layer bus_layer = vertical_layer for (pin_name, bus_name) in mapping: pin = inst.get_pin(pin_name) pin_pos = pin.center() bus_pos = bus_pins[bus_name].center() if horizontal: # up/down then left/right mid_pos = vector(pin_pos.x, bus_pos.y) else: # left/right then up/down mid_pos = vector(bus_pos.x, pin_pos.y) # Don't widen short wires because pin_pos and mid_pos could be really close self.add_wire(layer_stack, [bus_pos, mid_pos, pin_pos], widen_short_wires=False) # Connect to the pin on the instances with a via if it is # not on the right layer if pin.layer != route_layer: self.add_via_stack_center(from_layer=pin.layer, to_layer=route_layer, offset=pin_pos) # FIXME: output pins tend to not be rotate, # but supply pins are. Make consistent? # We only need a via if they happened to align perfectly # so the add_wire didn't add a via if (horizontal and bus_pos.y == pin_pos.y) or (not horizontal and bus_pos.x == pin_pos.x): self.add_via_stack_center(from_layer=route_layer, to_layer=bus_layer, offset=bus_pos) def connect_vbus(self, src_pin, dest_pin, hlayer="m3", vlayer="m2"): """ Helper routine to connect an instance to a vertical bus. Routes horizontal then vertical L shape. """ if src_pin.cx()