diff --git a/compiler/base/channel_route.py b/compiler/base/channel_route.py index cd4bbdc1..00ad6e37 100644 --- a/compiler/base/channel_route.py +++ b/compiler/base/channel_route.py @@ -12,6 +12,69 @@ from vector import vector import design +class channel_net(): + def __init__(self, net_name, pins, vertical): + self.name = net_name + self.pins = pins + self.vertical = vertical + + # Keep track of the internval + if vertical: + self.min_value = min(i.by() for i in pins) + self.max_value = max(i.uy() for i in pins) + else: + self.min_value = min(i.lx() for i in pins) + self.max_value = max(i.rx() for i in pins) + + # Keep track of the conflicts + self.conflicts = [] + + def __str__(self): + return self.name + + def __repr__(self): + return self.name + + def __lt__(self, other): + return self.min_value < other.min_value + + def pin_overlap(self, pin1, pin2, pitch): + """ Check for vertical or horizontal overlap of the two pins """ + + # FIXME: If the pins are not in a row, this may break. + # However, a top pin shouldn't overlap another top pin, + # for example, so the extra comparison *shouldn't* matter. + + # Pin 1 must be in the "BOTTOM" set + x_overlap = pin1.by() < pin2.by() and abs(pin1.center().x - pin2.center().x) < pitch + + # Pin 1 must be in the "LEFT" set + y_overlap = pin1.lx() < pin2.lx() and abs(pin1.center().y - pin2.center().y) < pitch + overlaps = (not self.vertical and x_overlap) or (self.vertical and y_overlap) + return overlaps + + def pins_overlap(self, other, pitch): + """ + Check all the pin pairs on two nets and return a pin + overlap if any pin overlaps. + """ + + for pin1 in self.pins: + for pin2 in other.pins: + if self.pin_overlap(pin1, pin2, pitch): + return True + + return False + + def segment_overlap(self, other): + """ + Check if the horizontal span of the two nets overlaps eachother. + """ + min_overlap = self.min_value >= other.min_value and self.min_value <= other.max_value + max_overlap = self.max_value >= other.min_value and self.max_value <= other.max_value + return min_overlap or max_overlap + + class channel_route(design.design): unique_id = 0 @@ -21,7 +84,8 @@ class channel_route(design.design): offset, layer_stack, directions=None, - vertical=False): + vertical=False, + parent=None): """ The net list is a list of the nets with each net being a list of pins to be connected. The offset is the lower-left of where the @@ -40,6 +104,8 @@ class channel_route(design.design): self.layer_stack = layer_stack self.directions = directions self.vertical = vertical + # For debugging... + self.parent = parent if not directions or directions == "pref": # Use the preferred layer directions @@ -86,114 +152,139 @@ class channel_route(design.design): # FIXME: This is O(n^2), so maybe optimize it. for other_pin, conflicts in g.items(): if pin in conflicts: - conflicts.remove(pin) - g[other_pin]=conflicts + g[other_pin].remove(pin) return g + + def route(self): + # Create names for the nets for the graphs + nets = [] + index = 0 + # print(self.netlist) + for pin_list in self.netlist: + nets.append(channel_net("n{}".format(index), pin_list, self.vertical)) + index += 1 - def vcg_nets_overlap(self, net1, net2): - """ - Check all the pin pairs on two nets and return a pin - overlap if any pin overlaps. - """ + # Create the (undirected) horizontal constraint graph + hcg = collections.OrderedDict() + for net1 in nets: + for net2 in nets: + if net1.name == net2.name: + continue + if net1.segment_overlap(net2): + try: + hcg[net1.name].add(net2.name) + except KeyError: + hcg[net1.name] = set([net2.name]) + try: + hcg[net2.name].add(net1.name) + except KeyError: + hcg[net2.name] = set([net1.name]) + + # Initialize the vertical conflict graph (vcg) + # and make a list of all pins + vcg = collections.OrderedDict() + + # print("Nets:") + # for net_name in nets: + # print(net_name, [x.name for x in nets[net_name]]) + + # Find the vertical pin conflicts + # FIXME: O(n^2) but who cares for now if self.vertical: pitch = self.horizontal_nonpref_pitch else: pitch = self.vertical_nonpref_pitch - for pin1 in net1: - for pin2 in net2: - if self.vcg_pin_overlap(pin1, pin2, pitch): - return True + for net in nets: + vcg[net.name] = set() - return False - - def route(self): - # FIXME: Must extend this to a horizontal conflict graph - # too if we want to minimize the - # number of tracks! - # hcg = {} - - # Initialize the vertical conflict graph (vcg) - # and make a list of all pins - vcg = collections.OrderedDict() - - # Create names for the nets for the graphs - nets = collections.OrderedDict() - index = 0 - # print(netlist) - for pin_list in self.netlist: - net_name = "n{}".format(index) - index += 1 - nets[net_name] = pin_list - - # print("Nets:") - # for net_name in nets: - # print(net_name, [x.name for x in nets[net_name]]) - - # Find the vertical pin conflicts - # FIXME: O(n^2) but who cares for now - for net_name1 in nets: - if net_name1 not in vcg.keys(): - vcg[net_name1] = [] - for net_name2 in nets: - if net_name2 not in vcg.keys(): - vcg[net_name2] = [] + for net1 in nets: + for net2 in nets: # Skip yourself - if net_name1 == net_name2: + if net1.name == net2.name: continue - if self.vcg_nets_overlap(nets[net_name1], - nets[net_name2]): - vcg[net_name2].append(net_name1) + + if net1.pins_overlap(net2, pitch): + vcg[net2.name].add(net1.name) - current_offset = self.offset + # Check if there are any cycles net1 <---> net2 in the VCG - # list of routes to do - while vcg: + + # Some of the pins may be to the left/below the channel offset, + # so adjust if this is the case + self.min_value = min([n.min_value for n in nets]) + self.max_value = min([n.max_value for n in nets]) + if self.vertical: + real_channel_offset = vector(self.offset.x, min(self.min_value, self.offset.y)) + else: + real_channel_offset = vector(min(self.min_value, self.offset.x), self.offset.y) + current_offset = real_channel_offset + + # Sort nets by left edge value + nets.sort() + while len(nets) > 0: + + current_offset_value = current_offset.y if self.vertical else current_offset.x + # from pprint import pformat # print("VCG:\n", pformat(vcg)) + # for name,net in vcg.items(): + # print(name, net.min_value, net.max_value, net.conflicts) + # print(current_offset) # get a route from conflict graph with empty fanout set - net_name = None - for net_name, conflicts in vcg.items(): - if len(conflicts) == 0: - vcg = self.remove_net_from_graph(net_name, vcg) + for net in nets: + # If it has no conflicts and the interval is to the right of the current offset in the track + if net.min_value >= current_offset_value and len(vcg[net.name]) == 0: + # print("Routing {}".format(net.name)) + # Add the trunk routes from the bottom up for + # horizontal or the left to right for vertical + if self.vertical: + self.add_vertical_trunk_route(net.pins, + current_offset, + self.vertical_nonpref_pitch) + current_offset = vector(current_offset.x, net.max_value + self.horizontal_nonpref_pitch) + else: + self.add_horizontal_trunk_route(net.pins, + current_offset, + self.horizontal_nonpref_pitch) + current_offset = vector(net.max_value + self.vertical_nonpref_pitch, current_offset.y) + + # Remove the net from other constriants in the VCG + vcg = self.remove_net_from_graph(net.name, vcg) + nets.remove(net) + break else: - # FIXME: We don't support cyclic VCGs right now. - debug.error("Cyclic VCG in channel router.", -1) - - # These are the pins we'll have to connect - pin_list = nets[net_name] - # print("Routing:", net_name, [x.name for x in pin_list]) - - # Remove the net from other constriants in the VCG - vcg = self.remove_net_from_graph(net_name, vcg) - - # Add the trunk routes from the bottom up for - # horizontal or the left to right for vertical - if self.vertical: - self.add_vertical_trunk_route(pin_list, - current_offset, - self.vertical_nonpref_pitch) - # This accounts for the via-to-via spacings - current_offset += vector(self.horizontal_nonpref_pitch, 0) - else: - self.add_horizontal_trunk_route(pin_list, - current_offset, - self.horizontal_nonpref_pitch) - # This accounts for the via-to-via spacings - current_offset += vector(0, self.vertical_nonpref_pitch) + # If we made a full pass and the offset didn't change... + current_offset_value = current_offset.y if self.vertical else current_offset.x + initial_offset_value = real_channel_offset.y if self.vertical else real_channel_offset.x + if current_offset_value == initial_offset_value: + debug.info(0, "Channel offset: {}".format(real_channel_offset)) + debug.info(0, "Current offset: {}".format(current_offset)) + debug.info(0, "VCG {}".format(str(vcg))) + debug.info(0, "HCG {}".format(str(hcg))) + for net in nets: + debug.info(0, "{0} pin: {1}".format(net.name, str(net.pins))) + if self.parent: + debug.info(0, "Saving vcg.gds") + self.parent.gds_write("vcg.gds") + debug.error("Cyclic VCG in channel router.", -1) + # Increment the track and reset the offset to the start (like a typewriter) + if self.vertical: + current_offset = vector(current_offset.x + self.horizontal_nonpref_pitch, real_channel_offset.y) + else: + current_offset = vector(real_channel_offset.x, current_offset.y + self.vertical_nonpref_pitch) + # Return the size of the channel if self.vertical: - self.width = 0 - self.height = current_offset.y - return current_offset.y + self.vertical_nonpref_pitch - self.offset.y + self.width = current_offset.x + self.horizontal_nonpref_pitch - self.offset.x + self.height = self.max_value + self.vertical_nonpref_pitch - self.offset.y else: - self.width = current_offset.x - self.height = 0 - return current_offset.x + self.horizontal_nonpref_pitch - self.offset.x - + self.width = self.max_value + self.horizontal_nonpref_pitch - self.offset.x + self.height = current_offset.y + self.vertical_nonpref_pitch - self.offset.y + def get_layer_pitch(self, layer): """ Return the track pitch on a given layer """ try: @@ -226,6 +317,17 @@ class channel_route(design.design): self.add_path(self.vertical_layer, [vector(min_x - half_layer_width, trunk_offset.y), vector(max_x + half_layer_width, trunk_offset.y)]) + + # Route each pin to the trunk + for pin in pins: + if pin.cy() < trunk_offset.y: + pin_pos = pin.uc() + else: + pin_pos = pin.bc() + + # No bend needed here + mid = vector(pin_pos.x, trunk_offset.y) + self.add_path(self.vertical_layer, [pin_pos, mid]) else: # Add the horizontal trunk self.add_path(self.horizontal_layer, @@ -269,6 +371,17 @@ class channel_route(design.design): self.add_path(self.horizontal_layer, [vector(trunk_offset.x, min_y - half_layer_width), vector(trunk_offset.x, max_y + half_layer_width)]) + + # Route each pin to the trunk + for pin in pins: + # Find the correct side of the pin + if pin.cx() < trunk_offset.x: + pin_pos = pin.rc() + else: + pin_pos = pin.lc() + # No bend needed here + mid = vector(trunk_offset.x, pin_pos.y) + self.add_path(self.horizontal_layer, [pin_pos, mid]) else: # Add the vertical trunk self.add_path(self.vertical_layer, @@ -292,18 +405,4 @@ class channel_route(design.design): to_layer=self.horizontal_layer, offset=pin_pos) - def vcg_pin_overlap(self, pin1, pin2, pitch): - """ Check for vertical or horizontal overlap of the two pins """ - - # FIXME: If the pins are not in a row, this may break. - # However, a top pin shouldn't overlap another top pin, - # for example, so the extra comparison *shouldn't* matter. - - # Pin 1 must be in the "BOTTOM" set - x_overlap = pin1.by() < pin2.by() and abs(pin1.center().x - pin2.center().x) < pitch - - # Pin 1 must be in the "LEFT" set - y_overlap = pin1.lx() < pin2.lx() and abs(pin1.center().y - pin2.center().y) < pitch - overlaps = (not self.vertical and x_overlap) or (self.vertical and y_overlap) - return overlaps diff --git a/compiler/base/hierarchy_layout.py b/compiler/base/hierarchy_layout.py index 8995488a..22826054 100644 --- a/compiler/base/hierarchy_layout.py +++ b/compiler/base/hierarchy_layout.py @@ -1018,7 +1018,7 @@ class layout(): Wrapper to create a vertical channel route """ import channel_route - cr = channel_route.channel_route(netlist, offset, layer_stack, directions, vertical=True) + cr = channel_route.channel_route(netlist, offset, layer_stack, directions, vertical=True, parent=self) self.add_inst("vc", cr) self.connect_inst([]) @@ -1027,7 +1027,7 @@ class layout(): Wrapper to create a horizontal channel route """ import channel_route - cr = channel_route.channel_route(netlist, offset, layer_stack, directions, vertical=False) + cr = channel_route.channel_route(netlist, offset, layer_stack, directions, vertical=False, parent=self) self.add_inst("hc", cr) self.connect_inst([]) diff --git a/compiler/characterizer/delay.py b/compiler/characterizer/delay.py index b7faaec3..3f5d61af 100644 --- a/compiler/characterizer/delay.py +++ b/compiler/characterizer/delay.py @@ -185,9 +185,8 @@ class delay(simulation): self.sen_meas = delay_measure("delay_sen", self.clk_frmt, self.sen_name+"{}", "FALL", "RISE", measure_scale=1e9) self.sen_meas.meta_str = sram_op.READ_ZERO self.sen_meas.meta_add_delay = True - self.dout_volt_meas.append(self.sen_meas) - - return self.dout_volt_meas + + return self.dout_volt_meas + [self.sen_meas] def create_read_bit_measures(self): """ Adds bit measurements for read0 and read1 cycles """ @@ -1351,7 +1350,7 @@ class delay(simulation): Return the analytical model results for the SRAM. """ if OPTS.num_rw_ports > 1 or OPTS.num_w_ports > 0 and OPTS.num_r_ports > 0: - debug.warning("Analytical characterization results are not supported for multiport.") + debug.warning("In analytical mode, all ports have the timing of the first read port.") # Probe set to 0th bit, does not matter for analytical delay. self.set_probe('0'*self.addr_size, 0) diff --git a/compiler/example_configs/example_config_1w_1r_scn4m_subm.py b/compiler/example_configs/example_config_1w_1r_scn4m_subm.py index 55ac4016..7698f1a3 100644 --- a/compiler/example_configs/example_config_1w_1r_scn4m_subm.py +++ b/compiler/example_configs/example_config_1w_1r_scn4m_subm.py @@ -1,9 +1,9 @@ word_size = 2 num_words = 16 -num_rw_ports = 1 +num_rw_ports = 0 num_r_ports = 1 -num_w_ports = 0 +num_w_ports = 1 tech_name = "scn4m_subm" nominal_corners_only = False diff --git a/compiler/modules/bank.py b/compiler/modules/bank.py index 158ac37b..b0707edb 100644 --- a/compiler/modules/bank.py +++ b/compiler/modules/bank.py @@ -211,10 +211,11 @@ class bank(design.design): self.port_data_offsets[port] = vector(self.main_bitcell_array_left - self.bitcell_array.cell.width, 0) # UPPER LEFT QUADRANT - # To the left of the bitcell array + # To the left of the bitcell array above the predecoders and control logic x_offset = self.m2_gap + self.port_address.width self.port_address_offsets[port] = vector(-x_offset, self.main_bitcell_array_bottom) + self.predecoder_height = self.port_address.predecoder_height + self.port_address_offsets[port].y # LOWER LEFT QUADRANT # Place the col decoder left aligned with wordline driver diff --git a/compiler/modules/hierarchical_decoder.py b/compiler/modules/hierarchical_decoder.py index 3233bdc8..dbacd051 100644 --- a/compiler/modules/hierarchical_decoder.py +++ b/compiler/modules/hierarchical_decoder.py @@ -314,24 +314,23 @@ class hierarchical_decoder(design.design): for i in range(self.no_of_pre3x8): self.place_pre3x8(i) + self.predecode_height = 0 + if self.no_of_pre2x4 > 0: + self.predecode_height = self.pre2x4_inst[-1].uy() + if self.no_of_pre3x8 > 0: + self.predecode_height = self.pre3x8_inst[-1].uy() + def place_pre2x4(self, num): """ Place 2x4 predecoder to the left of the origin """ - if (self.num_inputs == 2): - base = vector(-self.pre2_4.width, 0) - else: - base= vector(-self.pre2_4.width, num * (self.pre2_4.height + self.predecoder_spacing)) - + base= vector(-self.pre2_4.width, num * (self.pre2_4.height + self.predecoder_spacing)) self.pre2x4_inst[num].place(base) def place_pre3x8(self, num): """ Place 3x8 predecoder to the left of the origin and above any 2x4 decoders """ - if (self.num_inputs == 3): - offset = vector(-self.pre_3_8.width, 0) - else: - height = self.no_of_pre2x4 * (self.pre2_4.height + self.predecoder_spacing) + num * (self.pre3_8.height + self.predecoder_spacing) - offset = vector(-self.pre3_8.width, height) - + height = self.no_of_pre2x4 * (self.pre2_4.height + self.predecoder_spacing) \ + + num * (self.pre3_8.height + self.predecoder_spacing) + offset = vector(-self.pre3_8.width, height) self.pre3x8_inst[num].place(offset) def create_row_decoder(self): diff --git a/compiler/modules/port_address.py b/compiler/modules/port_address.py index a73e536c..980c9d96 100644 --- a/compiler/modules/port_address.py +++ b/compiler/modules/port_address.py @@ -153,6 +153,8 @@ class port_address(design.design): wordline_driver_offset = vector(self.row_decoder.width, 0) self.wordline_driver_inst.place(wordline_driver_offset) self.row_decoder_inst.place(row_decoder_offset) + # Pass this up + self.predecoder_height = self.row_decoder.predecoder_height self.height = self.row_decoder.height self.width = self.wordline_driver_inst.rx() diff --git a/compiler/pgates/pgate.py b/compiler/pgates/pgate.py index 985c8a86..1e55d5fb 100644 --- a/compiler/pgates/pgate.py +++ b/compiler/pgates/pgate.py @@ -372,66 +372,78 @@ class pgate(design.design): self.width = width @staticmethod - def bin_width(tx_type, target_width): + def best_bin(tx_type, target_width): + """ + Determine the width transistor that meets the accuracy requirement and is larger than target_width. + """ + + # Find all of the relavent scaled bins and multiples + scaled_bins = pgate.scaled_bins(tx_type, target_width) + for (scaled_width, multiple) in scaled_bins: + if abs(target_width - scaled_width) / target_width <= 1 - OPTS.accuracy_requirement: + break + else: + debug.error("failed to bin tx size {}, try reducing accuracy requirement".format(target_width), 1) + + debug.info(2, "binning {0} tx, target: {4}, found {1} x {2} = {3}".format(tx_type, + multiple, + scaled_width / multiple, + scaled_width, + target_width)) + + return(scaled_width / multiple, multiple) + + @staticmethod + 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: - selected_bin = bins[0] - scaling_factor = math.ceil(target_width / selected_bin) - scaled_bin = bins[0] * scaling_factor - + scaled_bins = [(bins[0], math.ceil(target_width / bins[0]))] else: - base_bins = [] scaled_bins = [] - scaling_factors = [] + # 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)) - for width in bins: - m = math.ceil(target_width / width) - base_bins.append(width) - scaling_factors.append(m) - scaled_bins.append(m * width) - - select = -1 - for i in reversed(range(0, len(scaled_bins))): - if abs(target_width - scaled_bins[i])/target_width <= 1-OPTS.accuracy_requirement: - select = i - break - if select == -1: - debug.error("failed to bin tx size {}, try reducing accuracy requirement".format(target_width), 1) - scaling_factor = scaling_factors[select] - scaled_bin = scaled_bins[select] - selected_bin = base_bins[select] - - debug.info(2, "binning {0} tx, target: {4}, found {1} x {2} = {3}".format(tx_type, selected_bin, scaling_factor, selected_bin * scaling_factor, target_width)) - - return(selected_bin, scaling_factor) - - def permute_widths(self, tx_type, target_width): + return(scaled_bins) + @staticmethod + def nearest_bin(tx_type, target_width): + """ + Determine the nearest width to the given target_width + while assuming a single multiple. + """ 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") - bins = bins[0:bisect_left(bins, target_width) + 1] - if len(bins) == 1: - scaled_bins = [(bins[0], math.ceil(target_width / bins[0]))] - else: - scaled_bins = [] - scaled_bins.append((bins[-1], 1)) - for width in bins[:-1]: - m = math.ceil(target_width / width) - scaled_bins.append((m * width, m)) + debug.error("invalid tx type") - return(scaled_bins) - - def bin_accuracy(self, ideal_width, width): - return 1-abs((ideal_width - width)/ideal_width) + # Find the next larger bin + bin_loc = bisect_left(bins, target_width) + if bin_loc < len(bins): + return bins[bin_loc] + else: + return bins[-1] + + @staticmethod + def bin_accuracy(ideal_width, width): + return 1 - abs((ideal_width - width) / ideal_width) diff --git a/compiler/pgates/pinv.py b/compiler/pgates/pinv.py index aa0f7d9a..4caf2a18 100644 --- a/compiler/pgates/pinv.py +++ b/compiler/pgates/pinv.py @@ -14,15 +14,11 @@ from vector import vector from math import ceil from globals import OPTS from utils import round_to_grid -from bisect import bisect_left import logical_effort from sram_factory import factory from errors import drc_error -if(OPTS.tech_name == "sky130"): - from tech import nmos_bins, pmos_bins - class pinv(pgate.pgate): """ Pinv generates gds of a parametrically sized inverter. The @@ -164,8 +160,8 @@ class pinv(pgate.pgate): else: self.nmos_width = self.nmos_size * drc("minwidth_tx") self.pmos_width = self.pmos_size * drc("minwidth_tx") - nmos_bins = self.permute_widths("nmos", self.nmos_width) - pmos_bins = self.permute_widths("pmos", self.pmos_width) + nmos_bins = self.scaled_bins("nmos", self.nmos_width) + pmos_bins = self.scaled_bins("pmos", self.pmos_width) valid_pmos = [] for bin in pmos_bins: diff --git a/compiler/pgates/pinv_dec.py b/compiler/pgates/pinv_dec.py index 4d4f2229..672bde2d 100644 --- a/compiler/pgates/pinv_dec.py +++ b/compiler/pgates/pinv_dec.py @@ -13,6 +13,7 @@ from vector import vector from globals import OPTS from sram_factory import factory + class pinv_dec(pinv.pinv): """ This is another version of pinv but with layout for the decoder. @@ -50,9 +51,8 @@ class pinv_dec(pinv.pinv): 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 + self.nmos_width = self.nearest_bin("nmos", self.nmos_width) + self.pmos_width = self.nearest_bin("pmos", self.pmos_width) # Over-ride the route input gate to call the horizontal version. # Other top-level netlist and layout functions are not changed. diff --git a/compiler/pgates/pnand2.py b/compiler/pgates/pnand2.py index 3974d95b..fb6bb210 100644 --- a/compiler/pgates/pnand2.py +++ b/compiler/pgates/pnand2.py @@ -39,8 +39,8 @@ class pnand2(pgate.pgate): self.tx_mults = 1 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) + 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 pgate.pgate.__init__(self, name, height, add_wells) diff --git a/compiler/pgates/pnand3.py b/compiler/pgates/pnand3.py index ff988d88..e4e71e61 100644 --- a/compiler/pgates/pnand3.py +++ b/compiler/pgates/pnand3.py @@ -42,8 +42,8 @@ class pnand3(pgate.pgate): self.tx_mults = 1 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) + 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 pgate.pgate.__init__(self, name, height, add_wells) diff --git a/compiler/pgates/pnor2.py b/compiler/pgates/pnor2.py index 908bba82..aad405e8 100644 --- a/compiler/pgates/pnor2.py +++ b/compiler/pgates/pnor2.py @@ -38,8 +38,8 @@ class pnor2(pgate.pgate): self.tx_mults = 1 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) + 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 pgate.pgate.__init__(self, name, height, add_wells) diff --git a/compiler/pgates/precharge.py b/compiler/pgates/precharge.py index b3d25865..52d24390 100644 --- a/compiler/pgates/precharge.py +++ b/compiler/pgates/precharge.py @@ -9,11 +9,10 @@ import contact import design import debug from pgate import pgate -from tech import parameter +from tech import parameter, drc from vector import vector from globals import OPTS from sram_factory import factory -from tech import drc, layer class precharge(design.design): @@ -81,7 +80,7 @@ class precharge(design.design): Initializes the upper and lower pmos """ if(OPTS.tech_name == "sky130"): - (self.ptx_width, self.ptx_mults) = pgate.bin_width("pmos", self.ptx_width) + self.ptx_width = pgate.nearest_bin("pmos", self.ptx_width) self.pmos = factory.create(module_type="ptx", width=self.ptx_width, mults=self.ptx_mults, diff --git a/compiler/pgates/ptx.py b/compiler/pgates/ptx.py index ce8591fd..b6f839f3 100644 --- a/compiler/pgates/ptx.py +++ b/compiler/pgates/ptx.py @@ -131,7 +131,7 @@ class ptx(design.design): perimeter_sd = 2 * self.poly_width + 2 * self.tx_width if OPTS.tech_name == "sky130" and OPTS.lvs_exe and OPTS.lvs_exe[0] == "calibre": # sky130 simulation cannot use the mult parameter in simulation - (self.tx_width, self.mults) = pgate.bin_width(self.tx_type, self.tx_width) + (self.tx_width, self.mults) = pgate.best_bin(self.tx_type, self.tx_width) main_str = "M{{0}} {{1}} {0} m={1} w={2} l={3} ".format(spice[self.tx_type], self.mults, self.tx_width, diff --git a/compiler/sram/sram.py b/compiler/sram/sram.py index b2ab1124..686877da 100644 --- a/compiler/sram/sram.py +++ b/compiler/sram/sram.py @@ -100,9 +100,9 @@ class sram(): import verify start_time = datetime.datetime.now() # Output the extracted design if requested - sp_file = OPTS.output_path + "temp_pex.sp" + pexname = OPTS.output_path + self.s.name + ".pex.sp" spname = OPTS.output_path + self.s.name + ".sp" - verify.run_pex(self.s.name, gdsname, spname, output=sp_file) + verify.run_pex(self.s.name, gdsname, spname, output=pexname) print_time("Extraction", datetime.datetime.now(), start_time) else: # Use generated spice file for characterization diff --git a/compiler/sram/sram_1bank.py b/compiler/sram/sram_1bank.py index a9731ed5..fc390f37 100644 --- a/compiler/sram/sram_1bank.py +++ b/compiler/sram/sram_1bank.py @@ -10,6 +10,8 @@ from vector import vector from sram_base import sram_base from contact import m2_via from globals import OPTS +import channel_route + class sram_1bank(sram_base): """ @@ -58,12 +60,14 @@ class sram_1bank(sram_base): # the sense amps/column mux and cell array) # The x-coordinate is placed to allow a single clock wire (plus an extra pitch) # up to the row address DFFs. - control_pos = [None] * len(self.all_ports) - row_addr_pos = [None] * len(self.all_ports) - col_addr_pos = [None] * len(self.all_ports) - wmask_pos = [None] * len(self.all_ports) - spare_wen_pos = [None] * len(self.all_ports) - data_pos = [None] * len(self.all_ports) + self.control_pos = [None] * len(self.all_ports) + self.row_addr_pos = [None] * len(self.all_ports) + + # DFFs are placd on their own + self.col_addr_pos = [None] * len(self.all_ports) + self.wmask_pos = [None] * len(self.all_ports) + self.spare_wen_pos = [None] * len(self.all_ports) + self.data_pos = [None] * len(self.all_ports) # These positions utilize the channel route sizes. # FIXME: Auto-compute these rather than manual computation. @@ -75,9 +79,11 @@ class sram_1bank(sram_base): # Spare wen are on a separate layer so not included # Start with 1 track minimum self.data_bus_size = [1] * len(self.all_ports) + self.col_addr_bus_size = [1] * len(self.all_ports) for port in self.all_ports: + # The column address wires are routed separately from the data bus and will always be smaller. # All ports need the col addr flops - self.data_bus_size[port] += self.col_addr_size + self.col_addr_bus_size[port] = self.col_addr_size * self.m4_nonpref_pitch # Write ports need the data input flops and write mask flops if port in self.write_ports: self.data_bus_size[port] += self.num_wmasks + self.word_size @@ -89,119 +95,152 @@ class sram_1bank(sram_base): self.data_bus_size[port] *= self.m4_nonpref_pitch # Add the gap in unit length self.data_bus_size[port] += self.data_bus_gap - - # Port 0 - port = 0 - # This includes 2 M2 pitches for the row addr clock line. - # The delay line is aligned with the bitcell array while the control logic is aligned with the port_data - # using the control_logic_center value. - control_pos[port] = vector(-self.control_logic_insts[port].width - 2 * self.m2_pitch, - self.bank.bank_array_ll.y - self.control_logic_insts[port].mod.control_logic_center.y) - self.control_logic_insts[port].place(control_pos[port]) - + # The control and row addr flops are independent of any bus widths. + self.place_control() + self.place_row_addr_dffs() + + # Place with an initial wide channel (from above) + self.place_dffs() + # Route the channel and set to the new data bus size + self.route_dffs(add_routes=False) + # Re-place with the new channel size + self.place_dffs() + # Now route the channel + self.route_dffs() + + def place_row_addr_dffs(self): + """ + Must be run after place control logic. + """ + port = 0 # The row address bits are placed above the control logic aligned on the right. x_offset = self.control_logic_insts[port].rx() - self.row_addr_dff_insts[port].width # It is above the control logic but below the top of the bitcell array - y_offset = max(self.control_logic_insts[port].uy(), self.bank_inst.uy() - self.row_addr_dff_insts[port].height) - row_addr_pos[port] = vector(x_offset, y_offset) - self.row_addr_dff_insts[port].place(row_addr_pos[port]) + y_offset = max(self.control_logic_insts[port].uy(), self.bank.predecoder_height) + self.row_addr_pos[port] = vector(x_offset, y_offset) + self.row_addr_dff_insts[port].place(self.row_addr_pos[port]) + if len(self.all_ports)>1: + port = 1 + # The row address bits are placed above the control logic aligned on the left. + x_offset = self.control_pos[port].x - self.control_logic_insts[port].width + self.row_addr_dff_insts[port].width + # If it can be placed above the predecoder and below the control logic, do it + y_offset = self.bank.bank_array_ll.y + self.row_addr_pos[port] = vector(x_offset, y_offset) + self.row_addr_dff_insts[port].place(self.row_addr_pos[port], mirror="XY") + + def place_control(self): + port = 0 + + # This includes 2 M2 pitches for the row addr clock line. + # The delay line is aligned with the bitcell array while the control logic is aligned with the port_data + # using the control_logic_center value. + self.control_pos[port] = vector(-self.control_logic_insts[port].width - 2 * self.m2_pitch, + self.bank.bank_array_ll.y - self.control_logic_insts[port].mod.control_logic_center.y) + self.control_logic_insts[port].place(self.control_pos[port]) + if len(self.all_ports) > 1: + port = 1 + # This includes 2 M2 pitches for the row addr clock line + # The delay line is aligned with the bitcell array while the control logic is aligned with the port_data + # using the control_logic_center value. + self.control_pos[port] = vector(self.bank_inst.rx() + self.control_logic_insts[port].width + 2 * self.m2_pitch, + self.bank.bank_array_ur.y + + self.control_logic_insts[port].height + - self.control_logic_insts[port].height + + self.control_logic_insts[port].mod.control_logic_center.y) + self.control_logic_insts[port].place(self.control_pos[port], mirror="XY") + + def place_dffs(self): + """ + Place the col addr, data, wmask, and spare data DFFs. + This can be run more than once after we recompute the channel width. + """ + + port = 0 # Add the col address flops below the bank to the right of the control logic x_offset = self.control_logic_insts[port].rx() + self.dff.width - y_offset = - self.data_bus_size[port] - self.dff.height + # Place it a data bus below the x-axis, but at least as low as the control logic to not block + # the control logic signals + y_offset = min(-self.data_bus_size[port] - self.dff.height, + self.control_logic_insts[port].by()) if self.col_addr_dff: - col_addr_pos[port] = vector(x_offset, - y_offset) - self.col_addr_dff_insts[port].place(col_addr_pos[port]) + self.col_addr_pos[port] = vector(x_offset, + y_offset) + self.col_addr_dff_insts[port].place(self.col_addr_pos[port]) x_offset = self.col_addr_dff_insts[port].rx() else: - col_addr_pos[port] = vector(x_offset, 0) + self.col_addr_pos[port] = vector(x_offset, 0) if port in self.write_ports: if self.write_size: # Add the write mask flops below the write mask AND array. - wmask_pos[port] = vector(x_offset, - y_offset) - self.wmask_dff_insts[port].place(wmask_pos[port]) + self.wmask_pos[port] = vector(x_offset, + y_offset) + self.wmask_dff_insts[port].place(self.wmask_pos[port]) x_offset = self.wmask_dff_insts[port].rx() # Add the data flops below the write mask flops. - data_pos[port] = vector(x_offset, - y_offset) - self.data_dff_insts[port].place(data_pos[port]) + self.data_pos[port] = vector(x_offset, + y_offset) + self.data_dff_insts[port].place(self.data_pos[port]) x_offset = self.data_dff_insts[port].rx() # Add spare write enable flops to the right of data flops since the spare columns # will be on the right if self.num_spare_cols: - spare_wen_pos[port] = vector(x_offset, - y_offset) - self.spare_wen_dff_insts[port].place(spare_wen_pos[port]) + self.spare_wen_pos[port] = vector(x_offset, + y_offset) + self.spare_wen_dff_insts[port].place(self.spare_wen_pos[port]) x_offset = self.spare_wen_dff_insts[port].rx() else: - wmask_pos[port] = vector(x_offset, y_offset) - data_pos[port] = vector(x_offset, y_offset) - spare_wen_pos[port] = vector(x_offset, y_offset) + self.wmask_pos[port] = vector(x_offset, y_offset) + self.data_pos[port] = vector(x_offset, y_offset) + self.spare_wen_pos[port] = vector(x_offset, y_offset) - if len(self.all_ports)>1: - # Port 1 + if len(self.all_ports) > 1: port = 1 - - # This includes 2 M2 pitches for the row addr clock line - # The delay line is aligned with the bitcell array while the control logic is aligned with the port_data - # using the control_logic_center value. - control_pos[port] = vector(self.bank_inst.rx() + self.control_logic_insts[port].width + 2 * self.m2_pitch, - self.bank.bank_array_ur.y - + self.control_logic_insts[port].height - - self.control_logic_insts[port].height - + self.control_logic_insts[port].mod.control_logic_center.y) - self.control_logic_insts[port].place(control_pos[port], mirror="XY") - - # The row address bits are placed above the control logic aligned on the left. - x_offset = control_pos[port].x - self.control_logic_insts[port].width + self.row_addr_dff_insts[port].width - # It is below the control logic but below the bottom of the bitcell array - y_offset = min(self.control_logic_insts[port].by(), self.bank_inst.by() + self.row_addr_dff_insts[port].height) - row_addr_pos[port] = vector(x_offset, y_offset) - self.row_addr_dff_insts[port].place(row_addr_pos[port], mirror="XY") - + # Add the col address flops below the bank to the right of the control logic x_offset = self.control_logic_insts[port].lx() - 2 * self.dff.width - y_offset = self.bank.height + self.data_bus_size[port] + self.dff.height + # Place it a data bus below the x-axis, but at least as high as the control logic to not block + # the control logic signals + y_offset = max(self.bank.height + self.data_bus_size[port] + self.dff.height, + self.control_logic_insts[port].uy() - self.dff.height) if self.col_addr_dff: - col_addr_pos[port] = vector(x_offset - self.col_addr_dff_insts[port].width, - y_offset) - self.col_addr_dff_insts[port].place(col_addr_pos[port], mirror="MX") + self.col_addr_pos[port] = vector(x_offset, + y_offset) + self.col_addr_dff_insts[port].place(self.col_addr_pos[port], mirror="XY") x_offset = self.col_addr_dff_insts[port].lx() else: - col_addr_pos[port] = vector(x_offset, y_offset) + self.col_addr_pos[port] = vector(x_offset, y_offset) if port in self.write_ports: # Add spare write enable flops to the right of the data flops since the spare # columns will be on the left if self.num_spare_cols: - spare_wen_pos[port] = vector(x_offset - self.spare_wen_dff_insts[port].width, - y_offset) - self.spare_wen_dff_insts[port].place(spare_wen_pos[port], mirror="MX") + self.spare_wen_pos[port] = vector(x_offset - self.spare_wen_dff_insts[port].width, + y_offset) + self.spare_wen_dff_insts[port].place(self.spare_wen_pos[port], mirror="MX") x_offset = self.spare_wen_dff_insts[port].lx() if self.write_size: # Add the write mask flops below the write mask AND array. - wmask_pos[port] = vector(x_offset - self.wmask_dff_insts[port].width, - y_offset) - self.wmask_dff_insts[port].place(wmask_pos[port], mirror="MX") + self.wmask_pos[port] = vector(x_offset - self.wmask_dff_insts[port].width, + y_offset) + self.wmask_dff_insts[port].place(self.wmask_pos[port], mirror="MX") x_offset = self.wmask_dff_insts[port].lx() # Add the data flops below the write mask flops. - data_pos[port] = vector(x_offset - self.data_dff_insts[port].width, - y_offset) - self.data_dff_insts[port].place(data_pos[port], mirror="MX") + self.data_pos[port] = vector(x_offset - self.data_dff_insts[port].width, + y_offset) + self.data_dff_insts[port].place(self.data_pos[port], mirror="MX") else: - wmask_pos[port] = vector(x_offset, y_offset) - data_pos[port] = vector(x_offset, y_offset) - spare_wen_pos[port] = vector(x_offset, y_offset) - + self.wmask_pos[port] = vector(x_offset, y_offset) + self.data_pos[port] = vector(x_offset, y_offset) + self.spare_wen_pos[port] = vector(x_offset, y_offset) + def add_layout_pins(self): """ Add the top-level pins for a single bank SRAM with control. @@ -343,20 +382,42 @@ class sram_1bank(sram_base): self.route_row_addr_dff() + def route_dffs(self, add_routes=True): + for port in self.all_ports: - self.route_dff(port) + self.route_dff(port, add_routes) - def route_dff(self, port): + def route_dff(self, port, add_routes): route_map = [] - # column mux dff + # column mux dff is routed on it's own since it is to the far end + # decoder inputs are min pitch M2, so need to use lower layer stack if self.col_addr_size > 0: dff_names = ["dout_{}".format(x) for x in range(self.col_addr_size)] dff_pins = [self.col_addr_dff_insts[port].get_pin(x) for x in dff_names] bank_names = ["addr{0}_{1}".format(port, x) for x in range(self.col_addr_size)] bank_pins = [self.bank_inst.get_pin(x) for x in bank_names] route_map.extend(list(zip(bank_pins, dff_pins))) + + if port == 0: + offset = vector(self.control_logic_insts[port].rx() + self.dff.width, + - self.data_bus_size[port] + 2 * self.m1_pitch) + else: + offset = vector(0, + self.bank.height + 2 * self.m1_space) + + cr = channel_route.channel_route(netlist=route_map, + offset=offset, + layer_stack=self.m1_stack, + parent=self) + if add_routes: + self.add_inst("hc", cr) + self.connect_inst([]) + else: + self.col_addr_bus_size[port] = cr.height + + route_map = [] # wmask dff if self.num_wmasks > 0 and port in self.write_ports: @@ -384,35 +445,46 @@ class sram_1bank(sram_base): bank_pins = [self.bank_inst.get_pin(x) for x in bank_names] route_map.extend(list(zip(bank_pins, sram_pins))) - if self.num_wmasks > 0 and port in self.write_ports: - layer_stack = self.m3_stack - else: - layer_stack = self.m1_stack - - if port == 0: - offset = vector(self.control_logic_insts[port].rx() + self.dff.width, - - self.data_bus_size[port] + 2 * self.m1_pitch) - else: - offset = vector(0, - self.bank.height + 2 * self.m1_space) - - if len(route_map) > 0: - self.create_horizontal_channel_route(netlist=route_map, - offset=offset, - layer_stack=layer_stack) - - # Route these separately because sometimes the pin pitch on the write driver is too narrow for M3 (FreePDK45) # spare wen dff if self.num_spare_cols > 0 and port in self.write_ports: dff_names = ["dout_{}".format(x) for x in range(self.num_spare_cols)] dff_pins = [self.spare_wen_dff_insts[port].get_pin(x) for x in dff_names] bank_names = ["bank_spare_wen{0}_{1}".format(port, x) for x in range(self.num_spare_cols)] bank_pins = [self.bank_inst.get_pin(x) for x in bank_names] - route_map = zip(bank_pins, dff_pins) - self.create_horizontal_channel_route(netlist=route_map, + route_map.extend(list(zip(bank_pins, dff_pins))) + + if len(route_map) > 0: + + if self.num_wmasks > 0 and port in self.write_ports: + layer_stack = self.m3_stack + else: + layer_stack = self.m1_stack + + if port == 0: + offset = vector(self.control_logic_insts[port].rx() + self.dff.width, + - self.data_bus_size[port] + 2 * self.m1_pitch) + cr = channel_route.channel_route(netlist=route_map, offset=offset, - layer_stack=self.m1_stack) - + layer_stack=layer_stack, + parent=self) + if add_routes: + self.add_inst("hc", cr) + self.connect_inst([]) + else: + self.data_bus_size[port] = max(cr.height, self.col_addr_bus_size[port]) + self.data_bus_gap + else: + offset = vector(0, + self.bank.height + 2 * self.m1_space) + cr = channel_route.channel_route(netlist=route_map, + offset=offset, + layer_stack=layer_stack, + parent=self) + if add_routes: + self.add_inst("hc", cr) + self.connect_inst([]) + else: + self.data_bus_size[port] = max(cr.height, self.col_addr_bus_size[port]) + self.data_bus_gap + def route_clk(self): """ Route the clock network """ diff --git a/compiler/sram/sram_base.py b/compiler/sram/sram_base.py index b16c28fa..12af7cf6 100644 --- a/compiler/sram/sram_base.py +++ b/compiler/sram/sram_base.py @@ -129,7 +129,7 @@ class sram_base(design, verilog, lef): start_time = datetime.datetime.now() # We only enable final verification if we have routed the design - self.DRC_LVS(final_verification=OPTS.route_supplies, force_check=True) + self.DRC_LVS(final_verification=OPTS.route_supplies, force_check=OPTS.check_lvsdrc) if not OPTS.is_unit_test: print_time("Verification", datetime.datetime.now(), start_time) diff --git a/compiler/verify/calibre.py b/compiler/verify/calibre.py index 6ee41e5f..965e3557 100644 --- a/compiler/verify/calibre.py +++ b/compiler/verify/calibre.py @@ -135,7 +135,7 @@ def write_calibre_lvs_script(cell_name, final_verification, gds_name, sp_name): def write_calibre_pex_script(cell_name, extract, output, final_verification): """ Write a pex script that can either just extract the netlist or the netlist+parasitics """ if output == None: - output = cell_name + ".pex.netlist" + output = cell_name + ".pex.sp" # check if lvs report has been done # if not run drc and lvs @@ -195,11 +195,14 @@ def run_drc(cell_name, gds_name, extract=False, final_verification=False): filter_gds(cell_name, OPTS.openram_temp + "temp.gds", OPTS.openram_temp + cell_name + ".gds") else: # Copy file to local dir if it isn't already - if not os.path.isfile(gds_name): - shutil.copy(OPTS.output_path+os.path.basename(gds_name),gds_name) + if not os.path.isfile(OPTS.openram_temp + os.path.basename(gds_name)): + shutil.copy(gds_name, OPTS.openram_temp) drc_runset = write_calibre_drc_script(cell_name, extract, final_verification, gds_name) + if not os.path.isfile(OPTS.openram_temp + os.path.basename(gds_name)): + shutil.copy(gds_name, OPTS.openram_temp + os.path.basename(gds_name)) + (outfile, errfile, resultsfile) = run_script(cell_name, "drc") # check the result for these lines in the summary: @@ -241,14 +244,10 @@ def run_lvs(cell_name, gds_name, sp_name, final_verification=False): lvs_runset = write_calibre_lvs_script(cell_name, final_verification, gds_name, sp_name) # Copy file to local dir if it isn't already -# if os.path.dirname(gds_name)!=OPTS.openram_temp.rstrip('/'): -# shutil.copy(gds_name, OPTS.openram_temp) -# if os.path.dirname(sp_name)!=OPTS.openram_temp.rstrip('/'): -# shutil.copy(sp_name, OPTS.openram_temp) - if not os.path.isfile(gds_name): - shutil.copy(OPTS.output_path+os.path.basename(gds_name), gds_name) - if not os.path.isfile(sp_name): - shutil.copy(OPTS.output_path+os.path.basename(sp_name), sp_name) + if not os.path.isfile(OPTS.openram_temp + os.path.basename(gds_name)): + shutil.copy(gds_name, OPTS.openram_temp) + if not os.path.isfile(OPTS.openram_temp + os.path.basename(sp_name)): + shutil.copy(sp_name, OPTS.openram_temp) (outfile, errfile, resultsfile) = run_script(cell_name, "lvs") @@ -329,13 +328,14 @@ def run_pex(cell_name, gds_name, sp_name, output=None, final_verification=False) global num_pex_runs num_pex_runs += 1 - write_calibre_pex_script(cell_name,True,output,final_verification) + write_calibre_pex_script(cell_name, True, output, final_verification) + # Copy file to local dir if it isn't already if not os.path.isfile(OPTS.openram_temp + os.path.basename(gds_name)): - shutil.copy(gds_name, OPTS.openram_temp + os.path.basename(gds_name)) + shutil.copy(gds_name, OPTS.openram_temp) if not os.path.isfile(OPTS.openram_temp + os.path.basename(sp_name)): - shutil.copy(sp_name, OPTS.openram_temp + os.path.basename(sp_name)) - + shutil.copy(sp_name, OPTS.openram_temp) + (outfile, errfile, resultsfile) = run_script(cell_name, "pex") diff --git a/compiler/verify/none.py b/compiler/verify/none.py index 41e5780e..f82d59ae 100644 --- a/compiler/verify/none.py +++ b/compiler/verify/none.py @@ -20,7 +20,7 @@ pex_warned = False def run_drc(cell_name, gds_name, extract=False, final_verification=False): global drc_warned if not drc_warned: - debug.warning("DRC unable to run.") + debug.error("DRC unable to run.", -1) drc_warned=True # Since we warned, return a failing test. return 1 @@ -29,7 +29,7 @@ def run_drc(cell_name, gds_name, extract=False, final_verification=False): def run_lvs(cell_name, gds_name, sp_name, final_verification=False): global lvs_warned if not lvs_warned: - debug.warning("LVS unable to run.") + debug.error("LVS unable to run.", -1) lvs_warned=True # Since we warned, return a failing test. return 1 @@ -38,7 +38,7 @@ def run_lvs(cell_name, gds_name, sp_name, final_verification=False): def run_pex(name, gds_name, sp_name, output=None, final_verification=False): global pex_warned if not pex_warned: - debug.warning("PEX unable to run.") + debug.error("PEX unable to run.", -1) pex_warned=True # Since we warned, return a failing test. return 1 diff --git a/technology/freepdk45/gds_lib/write_driver.gds b/technology/freepdk45/gds_lib/write_driver.gds index 86015e7a..5e4fb15a 100644 Binary files a/technology/freepdk45/gds_lib/write_driver.gds and b/technology/freepdk45/gds_lib/write_driver.gds differ