def place_instances(self): """ Compute the offsets and place the instances. """ row_decoder_offset = vector(0, 0) self.row_decoder_inst.place(row_decoder_offset) wordline_driver_array_offset = vector(self.row_decoder_inst.rx(), 0) self.wordline_driver_array_inst.place(wordline_driver_array_offset) # The wordline driver also had an extra gap on the right, so use this offset well_gap = 2 * drc("pwell_to_nwell") + drc("nwell_enclose_active") x_offset = self.wordline_driver_array_inst.rx( ) - well_gap - self.rbl_driver.width if self.port == 0: rbl_driver_offset = vector(x_offset, 0) self.rbl_driver_inst.place(rbl_driver_offset, "MX") else: rbl_driver_offset = vector(x_offset, self.wordline_driver_array.height) self.rbl_driver_inst.place(rbl_driver_offset) # Pass this up self.predecoder_height = self.row_decoder.predecoder_height self.height = self.row_decoder.height self.width = self.wordline_driver_array_inst.rx()
def setup_layout_constants(self): """ Pre-compute some handy layout parameters. """ # metal spacing to allow contacts on any layer self.input_spacing = max( self.poly_space + contact.poly.first_layer_width, self.m1_space + contact.m1m2.first_layer_width, self.m2_space + contact.m2m3.first_layer_width, self.m3_space + contact.m2m3.second_layer_width) # Compute the other pmos2 location, but determining # offset to overlap the source and drain pins self.overlap_offset = self.pmos.get_pin("D").ll() - self.pmos.get_pin( "S").ll() # Two PMOS devices and a well contact. Separation between each. # Enclosure space on the sides. self.well_width = 2 * self.pmos.active_width \ + self.pmos.active_contact.width \ + 2 * drc("active_to_body_active") \ + 2 * drc("well_enclosure_active") self.width = self.well_width # Height is an input parameter, so it is not recomputed. # This is the extra space needed to ensure DRC rules # to the active contacts extra_contact_space = max(-self.nmos.get_pin("D").by(), 0) # This is a poly-to-poly of a flipped cell self.top_bottom_space = max( 0.5 * self.m1_width + self.m1_space + extra_contact_space, drc("poly_extend_active"), self.poly_space)
def setup_layers(self): (horiz_layer, via_layer, vert_layer) = self.layer_stack self.via_layer_name = via_layer self.vert_layer_name = vert_layer self.vert_layer_width = drc("minwidth_{0}".format(vert_layer)) self.horiz_layer_name = horiz_layer self.horiz_layer_width = drc("minwidth_{0}".format(horiz_layer)) via_connect = factory.create(module_type="contact", layer_stack=self.layer_stack, dimensions=(1, 1)) # This is used for short connections to avoid via-to-via spacing errors self.vert_layer_contact_width = max(via_connect.second_layer_width, via_connect.first_layer_width) self.horiz_layer_contact_width = max(via_connect.second_layer_height, via_connect.first_layer_height) self.node_to_node = [ drc("minwidth_" + str(self.horiz_layer_name)) + via_connect.width, drc("minwidth_" + str(self.horiz_layer_name)) + via_connect.height ] self.pitch = self.compute_pitch(self.layer_stack)
def analytical_delay(self, corner, slew, load): #Delay of the sense amp will depend on the size of the amp and the output load. parasitic_delay = 1 cin = (parameter["sa_inv_pmos_size"] + parameter["sa_inv_nmos_size"])/drc("minwidth_tx") sa_size = parameter["sa_inv_nmos_size"]/drc("minwidth_tx") cc_inv_cin = cin return logical_effort.logical_effort('column_mux', sa_size, cin, load+cc_inv_cin, parasitic_delay, False)
def create_implant_well_enclosures(self): implant_position = self.first_layer_position - [ drc("implant_enclose_active") ] * 2 implant_width = self.first_layer_width + 2 * drc( "implant_enclose_active") implant_height = self.first_layer_height + 2 * drc( "implant_enclose_active") self.add_rect(layer="{}implant".format(self.implant_type), offset=implant_position, width=implant_width, height=implant_height) # Optionally implant well if layer exists well_layer = "{}well".format(self.well_type) if well_layer in tech.layer: well_width_rule = drc("minwidth_" + well_layer) self.well_enclose_active = drc(well_layer + "_enclose_active") self.well_width = max( self.first_layer_width + 2 * self.well_enclose_active, well_width_rule) self.well_height = max( self.first_layer_height + 2 * self.well_enclose_active, well_width_rule) center_pos = vector(0.5 * self.width, 0.5 * self.height) well_position = center_pos - vector(0.5 * self.well_width, 0.5 * self.well_height) self.add_rect(layer=well_layer, offset=well_position, width=self.well_width, height=self.well_height)
def extend_wells(self, middle_position): """ Extend the n/p wells to cover whole cell """ # Add a rail width to extend the well to the top of the rail max_y_offset = self.height + 0.5 * self.m1_width self.nwell_position = middle_position nwell_height = max_y_offset - middle_position.y if drc("has_nwell"): self.add_rect(layer="nwell", offset=middle_position, width=self.well_width, height=nwell_height) self.add_rect(layer="vtg", offset=self.nwell_position, width=self.well_width, height=nwell_height) pwell_position = vector(0, -0.5 * self.m1_width) pwell_height = middle_position.y - pwell_position.y if drc("has_pwell"): self.add_rect(layer="pwell", offset=pwell_position, width=self.well_width, height=pwell_height) self.add_rect(layer="vtg", offset=pwell_position, width=self.well_width, height=pwell_height)
def precompute_constants(self): """ Get some preliminary data ready """ # The central bus is the column address (one hot) and row address (binary) if self.col_addr_size > 0: self.num_col_addr_lines = 2**self.col_addr_size else: self.num_col_addr_lines = 0 # A space for wells or jogging m2 between modules self.m2_gap = max( 2 * drc("pwell_to_nwell") + drc("nwell_enclose_active"), 3 * self.m2_pitch) # create arrays of bitline and bitline_bar names for read, write, or all ports self.bitcell = factory.create(module_type="bitcell") self.bl_names = self.bitcell.get_all_bl_names() self.br_names = self.bitcell.get_all_br_names() self.wl_names = self.bitcell.get_all_wl_names() # used for bl/br names self.precharge = factory.create(module_type="precharge", bitcell_bl=self.bl_names[0], bitcell_br=self.br_names[0]) # We create a dummy here to get bl/br names to add those pins to this # module, which happens before we create the real precharge_array self.precharge_array = factory.create( module_type="precharge_array", columns=self.num_cols + 1, bitcell_bl=self.bl_names[self.port], bitcell_br=self.br_names[self.port])
def add_layout_pins(self): self.add_layout_pin(text="en_bar", layer="metal1", offset=self.pc_cell.get_pin("en_bar").ll(), width=self.width, height=drc("minwidth_metal1")) for inst in self.local_insts: self.copy_layout_pin(inst, "vdd") for i in range(len(self.local_insts)): inst = self.local_insts[i] bl_pin = inst.get_pin("bl") self.add_layout_pin(text="bl_{0}".format(i), layer="metal2", offset=bl_pin.ll(), width=drc("minwidth_metal2"), height=bl_pin.height()) br_pin = inst.get_pin("br") self.add_layout_pin(text="br_{0}".format(i), layer="metal2", offset=br_pin.ll(), width=drc("minwidth_metal2"), height=bl_pin.height())
def setup_layout_constants(self): """ Pre-compute some handy layout parameters. """ # Compute the overlap of the source and drain pins self.overlap_offset = self.pmos.get_pin("D").ll() - self.pmos.get_pin( "S").ll() # Two PMOS devices and a well contact. Separation between each. # Enclosure space on the sides. self.well_width = 3*self.pmos.active_width + self.pmos.active_contact.width \ + 2*drc("active_to_body_active") + 2*drc("well_enclosure_active") \ - self.overlap_offset.x self.width = self.well_width # Height is an input parameter, so it is not recomputed. # This will help with the wells and the input/output placement self.output_pos = vector(0, 0.5 * self.height) # This is the extra space needed to ensure DRC rules to the active contacts nmos = factory.create(module_type="ptx", tx_type="nmos") extra_contact_space = max(-nmos.get_pin("D").by(), 0) # This is a poly-to-poly of a flipped cell self.top_bottom_space = max( 0.5 * self.m1_width + self.m1_space + extra_contact_space, drc("poly_extend_active"), self.poly_space)
def scaled_bins(tx_type, target_width): """ Determine a set of widths and multiples that could be close to the right size sorted by the fewest number of fingers. """ if tx_type == "nmos": bins = nmos_bins[drc("minwidth_poly")] elif tx_type == "pmos": bins = pmos_bins[drc("minwidth_poly")] else: debug.error("invalid tx type") # Prune out bins that are too big, except for one bigger bins = bins[0:bisect_left(bins, target_width) + 1] # Determine multiple of target width for each bin if len(bins) == 1: scaled_bins = [(bins[0], math.ceil(target_width / bins[0]))] else: scaled_bins = [] # Add the biggest size as 1x multiple scaled_bins.append((bins[-1], 1)) # Compute discrete multiple of other sizes for width in reversed(bins[:-1]): multiple = math.ceil(target_width / width) scaled_bins.append((multiple * width, multiple)) return (scaled_bins)
def calculate_module_offsets(self): self.xoffset_nand = self.inv4x.width + 3 * self.m2_pitch + drc("pwell_to_nwell") self.xoffset_nor = self.inv4x.width + 3 * self.m2_pitch + drc("pwell_to_nwell") self.xoffset_bank_sel_inv = 0 self.xoffset_inputs = 0 self.yoffset_maxpoint = self.num_control_lines * self.inv4x.height
def __init__(self, name, size=1, height=None, add_wells=True): """ Creates a cell for a simple 3 input nand """ debug.info(2, "creating pnand4 structure {0} with size of {1}".format(name, size)) self.add_comment("size: {}".format(size)) # We have trouble pitch matching a 3x sizes to the bitcell... # If we relax this, we could size this better. self.size = size self.nmos_size = 2 * size self.pmos_size = parameter["beta"] * size self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx") # FIXME: Allow these to be sized debug.check(size == 1, "Size 1 pnand4 is only supported now.") self.tx_mults = 1 if OPTS.tech_name == "sky130": self.nmos_width = self.nearest_bin("nmos", self.nmos_width) self.pmos_width = self.nearest_bin("pmos", self.pmos_width) # Creates the netlist and layout super().__init__(name, height, add_wells)
def __init__(self, name="nand4_dec", height=None): super().__init__(name, prop=props.nand4_dec) # FIXME: For now... size = 1 self.size = size self.nmos_size = 2 * size self.pmos_size = parameter["beta"] * size self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx")
def setup_layout_constants(self): """ Pre-compute some handy layout parameters. """ # the well width is determined the multi-finger PMOS device width plus # the well contact width and half well enclosure on both sides self.well_width = self.pmos.active_width + self.pmos.active_contact.width \ + drc("active_to_body_active") + 2*drc("well_enclosure_active") self.width = self.well_width
def calculate_module_offsets(self): self.xoffset_nand = self.inv4x.width + 2*self.m2_pitch + drc("pwell_to_nwell") self.xoffset_nor = self.inv4x.width + 2*self.m2_pitch + drc("pwell_to_nwell") self.xoffset_inv = max(self.xoffset_nand + self.nand2.width, self.xoffset_nor + self.nor2.width) self.xoffset_bank_sel_inv = 0 self.xoffset_inputs = 0 self.yoffset_maxpoint = self.num_control_lines * self.inv4x.height # Include the M1 pitches for the supply rails and spacing self.height = self.yoffset_maxpoint + 2*self.m1_pitch self.width = self.xoffset_inv + self.inv4x.width
def create_netlist(self): self.add_pin_list(["D", "G", "S", "B"]) # self.spice.append("\n.SUBCKT {0} {1}".format(self.name, # " ".join(self.pins))) # Just make a guess since these will actually be decided in the layout later. area_sd = 2.5 * drc("minwidth_poly") * self.tx_width perimeter_sd = 2 * drc("minwidth_poly") + 2 * self.tx_width self.spice_device = "M{{0}} {{1}} {0} m={1} w={2}u l={3}u pd={4:.2f}u ps={4:.2f}u as={5:.2f}p ad={5:.2f}p".format( spice[self.tx_type], self.mults, self.tx_width, drc("minwidth_poly"), perimeter_sd, area_sd) self.spice.append("\n* ptx " + self.spice_device)
def create_netlist(self): pin_list = ["D", "G", "S", "B"] if self.tx_type == "nmos": body_dir = "GROUND" else: body_dir = "POWER" dir_list = ["INOUT", "INPUT", "INOUT", body_dir] self.add_pin_list(pin_list, dir_list) # Just make a guess since these will actually # be decided in the layout later. area_sd = 2.5 * self.poly_width * self.tx_width perimeter_sd = 2 * self.poly_width + 2 * self.tx_width if cell_props.ptx.model_is_subckt: # sky130 main_str = "X{{0}} {{1}} {0} m={1} w={2} l={3} ".format( spice[self.tx_type], self.mults, self.tx_width, drc("minwidth_poly")) # Perimeters are in microns # Area is in u since it is microns square area_str = "pd={0:.2f} ps={0:.2f} as={1:.2f}u ad={1:.2f}u".format( perimeter_sd, area_sd) else: main_str = "M{{0}} {{1}} {0} m={1} w={2}u l={3}u ".format( spice[self.tx_type], self.mults, self.tx_width, drc("minwidth_poly")) area_str = "pd={0:.2f}u ps={0:.2f}u as={1:.2f}p ad={1:.2f}p".format( perimeter_sd, area_sd) self.spice_device = main_str + area_str self.spice.append("\n* spice ptx " + self.spice_device) if cell_props.ptx.model_is_subckt and OPTS.lvs_exe and OPTS.lvs_exe[ 0] == "calibre": # sky130 requires mult parameter too. It is not the same as m, but I don't understand it. # self.lvs_device = "X{{0}} {{1}} {0} m={1} w={2} l={3} mult=1".format(spice[self.tx_type], # self.mults, # self.tx_width, # drc("minwidth_poly")) # TEMP FIX: Use old device names if using Calibre. self.lvs_device = "M{{0}} {{1}} {0} m={1} w={2} l={3} mult=1".format( "nshort" if self.tx_type == "nmos" else "pshort", self.mults, self.tx_width, drc("minwidth_poly")) elif cell_props.ptx.model_is_subckt: # sky130 requires mult parameter too self.lvs_device = "X{{0}} {{1}} {0} m={1} w={2}u l={3}u".format( spice[self.tx_type], self.mults, self.tx_width, drc("minwidth_poly")) else: self.lvs_device = "M{{0}} {{1}} {0} m={1} w={2}u l={3}u ".format( spice[self.tx_type], self.mults, self.tx_width, drc("minwidth_poly"))
def determine_tx_mults(self): """ Determines the number of fingers needed to achieve the size within the height constraint. This may fail if the user has a tight height. """ # This is always 1 tx, because we have horizontal transistors. self.tx_mults = 1 self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx") if cell_props.ptx.bin_spice_models: self.nmos_width = self.nearest_bin("nmos", self.nmos_width) self.pmos_width = self.nearest_bin("pmos", self.pmos_width)
def setup_layers(self): (horiz_layer, via_layer, vert_layer) = self.layer_stack self.via_layer_name = via_layer self.vert_layer_name = vert_layer self.vert_layer_width = drc("minwidth_{0}".format(vert_layer)) self.horiz_layer_name = horiz_layer self.horiz_layer_width = drc("minwidth_{0}".format(horiz_layer)) via_connect = factory.create(module_type="contact", layer_stack=self.layer_stack, dimensions=(1, 1)) self.node_to_node = [drc("minwidth_" + str(self.horiz_layer_name)) + via_connect.width, drc("minwidth_" + str(self.horiz_layer_name)) + via_connect.height]
def __init__(self, name="nand3_dec", height=None): design.design.__init__(self, name) self.width = nand3_dec.width self.height = nand3_dec.height self.pin_map = nand3_dec.pin_map self.add_pin_types(self.type_list) # FIXME: For now... size = 1 self.size = size self.nmos_size = 2 * size self.pmos_size = parameter["beta"] * size self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx")
def determine_tx_mults(self): """ Determines the number of fingers needed to achieve the size within the height constraint. This may fail if the user has a tight height. """ # This is always 1 tx, because we have horizontal transistors. self.tx_mults = 1 self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx") if OPTS.tech_name == "sky130": (self.nmos_width, self.tx_mults) = self.bin_width("nmos", self.nmos_width) (self.pmos_width, self.tx_mults) = self.bin_width("pmos", self.pmos_width) return
def route_inputs(self): """ Route the A and B inputs """ # wire space or wire and one contact space metal_spacing = max( self.m1_space + self.m1_width, self.m2_space + self.m2_width, self.m1_space + 0.5 * contact.poly.width + 0.5 * self.m1_width) active_spacing = max( self.m1_space, 0.5 * contact.poly.first_layer_width + drc("poly_to_active")) inputC_yoffset = self.nmos3_pos.y + self.nmos.active_height + active_spacing self.route_input_gate(self.pmos3_inst, self.nmos3_inst, inputC_yoffset, "C", position="center") inputB_yoffset = inputC_yoffset + metal_spacing self.route_input_gate(self.pmos2_inst, self.nmos2_inst, inputB_yoffset, "B", position="center") self.inputA_yoffset = inputB_yoffset + metal_spacing self.route_input_gate(self.pmos1_inst, self.nmos1_inst, self.inputA_yoffset, "A", position="center")
def place_ptx(self): """ """ # center the transistors in the y-dimension (it is rotated, so use the width) y_offset = 0.5 * (self.height - self.nmos.width) + self.nmos.width if OPTS.tech_name == "sky130": # make room for well contacts between cells y_offset = (0.5 * (self.height - self.nmos.width) + self.nmos.width) * 0.9 # offset so that the input contact is over from the left edge by poly spacing x_offset = self.nmos.active_offset.y + contact.poly_contact.width + self.poly_space self.nmos_pos = vector(x_offset, y_offset) self.nmos_inst.place(self.nmos_pos, rotate=270) # place PMOS so it is half a poly spacing down from the top well_offsets = 2 * self.poly_extend_active + 2 * self.well_extend_active + drc( "pwell_to_nwell") # This is to provide spacing for the vdd rails metal_offsets = 2 * self.m3_pitch xoffset = self.nmos_inst.rx() + max(well_offsets, metal_offsets) self.pmos_pos = vector(xoffset, y_offset) self.pmos_inst.place(self.pmos_pos, rotate=270) # Output position will be in between the PMOS and NMOS drains pmos_drain_pos = self.pmos_inst.get_pin("D").center() nmos_drain_pos = self.nmos_inst.get_pin("D").center() self.output_pos = vector(0.5 * (pmos_drain_pos.x + nmos_drain_pos.x), nmos_drain_pos.y) if cell_props.pgate.add_implants: self.extend_implants()
def route_inputs(self): """ Route the A and B inputs """ # Top of NMOS drain nmos_pin = self.nmos2_inst.get_pin("D") bottom_pin_offset = nmos_pin.uy() self.inputB_yoffset = bottom_pin_offset + self.m1_nonpref_pitch self.inputA_yoffset = self.inputB_yoffset + self.m1_nonpref_pitch bpin = self.route_input_gate(self.pmos2_inst, self.nmos2_inst, self.inputB_yoffset, "B", position="right", directions=("V", "V")) # This will help with the wells and the input/output placement apin = self.route_input_gate(self.pmos1_inst, self.nmos1_inst, self.inputA_yoffset, "A", directions=("V", "V")) self.output_yoffset = self.inputA_yoffset + self.m1_nonpref_pitch if cell_props.pgate.add_implants: self.add_enclosure([apin, bpin], "npc", drc("npc_enclose_poly"))
def add_pwell_contact(self, nmos, nmos_pos): """ Add an pwell contact next to the given nmos device. """ layer_stack = ("active", "contact", "metal1") pwell_position = vector(0, -0.5 * self.m1_width) # To the right a spacing away from the nmos right active edge contact_xoffset = nmos_pos.x + nmos.active_width \ + drc("active_to_body_active") # Must be at least an well enclosure of active up # from the bottom of the well contact_yoffset = max(nmos_pos.y, self.well_enclose_active \ - nmos.active_contact.first_layer_height / 2) contact_offset = vector(contact_xoffset, contact_yoffset) # Offset by half a contact contact_offset += vector(0.5 * nmos.active_contact.first_layer_width, 0.5 * nmos.active_contact.first_layer_height) self.pwell_contact = self.add_via_center(layers=layer_stack, offset=contact_offset, directions=("H", "V"), implant_type="p", well_type="p") self.add_rect_center(layer="metal1", offset=contact_offset.scale(1, 0.5), width=self.pwell_contact.mod.second_layer_width, height=contact_offset.y)
def add_nwell_contact(self, pmos, pmos_pos): """ Add an nwell contact next to the given pmos device. """ layer_stack = ("active", "contact", "metal1") # To the right a spacing away from the pmos right active edge contact_xoffset = pmos_pos.x + pmos.active_width \ + drc("active_to_body_active") # Must be at least an well enclosure of active down # from the top of the well # OR align the active with the top of PMOS active. max_y_offset = self.height + 0.5 * self.m1_width contact_yoffset = min( pmos_pos.y + pmos.active_height - pmos.active_contact.first_layer_height, max_y_offset - pmos.active_contact.first_layer_height / 2 - self.well_enclose_active) contact_offset = vector(contact_xoffset, contact_yoffset) # Offset by half a contact in x and y contact_offset += vector(0.5 * pmos.active_contact.first_layer_width, 0.5 * pmos.active_contact.first_layer_height) self.nwell_contact = self.add_via_center(layers=layer_stack, offset=contact_offset, directions=("H", "V"), implant_type="n", well_type="n") self.add_rect_center(layer="metal1", offset=contact_offset + vector(0, 0.5 * (self.height - contact_offset.y)), width=self.nwell_contact.mod.second_layer_width, height=self.height - contact_offset.y)
def create_layout(self): # We increase it by a well enclosure so the precharges don't overlap our wells self.height = self.row_size * self.cell.height + drc( "well_enclosure_active") + self.m1_width self.width = self.column_size * self.cell.width + self.m1_width xoffset = 0.0 for col in range(self.column_size): yoffset = 0.0 for row in range(self.row_size): name = "bit_r{0}_c{1}".format(row, col) if row % 2: tempy = yoffset + self.cell.height dir_key = "MX" else: tempy = yoffset dir_key = "" self.cell_inst[row, col].place(offset=[xoffset, tempy], mirror=dir_key) yoffset += self.cell.height xoffset += self.cell.width self.add_layout_pins() self.DRC_LVS()
def route_vdd_rail(self): """ Adds a vdd rail at the top of the cell """ # Adds the rail across the width of the cell vdd_position = vector(0.5 * self.width, self.height) layer_width = drc("minwidth_" + self.en_layer) self.add_rect_center(layer=self.en_layer, offset=vdd_position, width=self.width, height=layer_width) pmos_pin = self.upper_pmos2_inst.get_pin("S") # center of vdd rail pmos_vdd_pos = vector(pmos_pin.cx(), vdd_position.y) self.add_path(self.en_layer, [pmos_pin.center(), pmos_vdd_pos]) self.add_power_pin("vdd", self.well_contact_pos, directions=("V", "V")) self.add_via_stack_center(from_layer=pmos_pin.layer, to_layer=self.en_layer, offset=pmos_pin.center(), directions=("V", "V"))
def add_modules(self): self.bitcell = factory.create(module_type="bitcell") # Adds nmos_lower,nmos_upper to the module self.ptx_width = self.tx_size * drc("minwidth_tx") self.nmos = factory.create(module_type="ptx", width=self.ptx_width) self.add_mod(self.nmos)
def input_load(self): """ Returns the relative gate cin of the tx """ # FIXME: this will be applied for the loads of the drain/source return self.mults * self.tx_width / drc("minwidth_tx")