Merge branch 'dev' into automated_analytical_model

This commit is contained in:
Hunter Nichols 2021-06-09 15:45:41 -07:00
commit 4ec2e1240f
34 changed files with 423 additions and 190 deletions

View File

@ -176,7 +176,7 @@ class cell_properties():
self.names["col_cap_bitcell_2port"] = "col_cap_cell_2rw"
self.names["row_cap_bitcell_1port"] = "row_cap_cell_1rw"
self.names["row_cap_bitcell_2port"] = "row_cap_cell_2rw"
self.use_strap = False
self._ptx = _ptx(model_is_subckt=False,
bin_spice_models=False)

View File

@ -48,7 +48,8 @@ class design(hierarchy_design):
self.add_pin_indices(prop.port_indices)
self.add_pin_names(prop.port_map)
self.add_pin_types(prop.port_types)
(width, height) = utils.get_libcell_size(self.cell_name,
GDS["unit"],
layer[prop.boundary_layer])

View File

@ -41,7 +41,8 @@ class layout():
self.width = None
self.height = None
self.bounding_box = 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
@ -1163,6 +1164,57 @@ class layout():
self.bbox = [self.bounding_box.ll(), self.bounding_box.ur()]
def get_bbox(self, side="all", big_margin=0, little_margin=0):
"""
Get the bounding box from the GDS
"""
gds_filename = OPTS.openram_temp + "temp.gds"
# If didn't specify a gds blockage file, write it out to read the gds
# This isn't efficient, but easy for now
# Load the gds file and read in all the shapes
self.gds_write(gds_filename)
layout = gdsMill.VlsiLayout(units=GDS["unit"])
reader = gdsMill.Gds2reader(layout)
reader.loadFromFile(gds_filename)
top_name = layout.rootStructureName
if not self.bbox:
# The boundary will determine the limits to the size
# of the routing grid
boundary = layout.measureBoundary(top_name)
# These must be un-indexed to get rid of the matrix type
ll = vector(boundary[0][0], boundary[0][1])
ur = vector(boundary[1][0], boundary[1][1])
else:
ll, ur = self.bbox
ll_offset = vector(0, 0)
ur_offset = vector(0, 0)
if side in ["ring", "top", "all"]:
ur_offset += vector(0, big_margin)
else:
ur_offset += vector(0, little_margin)
if side in ["ring", "bottom", "all"]:
ll_offset += vector(0, big_margin)
else:
ll_offset += vector(0, little_margin)
if side in ["ring", "left", "all"]:
ll_offset += vector(big_margin, 0)
else:
ll_offset += vector(little_margin, 0)
if side in ["ring", "right", "all"]:
ur_offset += vector(big_margin, 0)
else:
ur_offset += vector(little_margin, 0)
bbox = (ll - ll_offset, ur + ur_offset)
size = ur - ll
debug.info(1, "Size: {0} x {1} with perimeter big margin {2} little margin {3}".format(size.x,
size.y,
big_margin,
little_margin))
return bbox
def add_enclosure(self, insts, layer="nwell", extend=0, leftx=None, rightx=None, topy=None, boty=None):
"""
Add a layer that surrounds the given instances. Useful
@ -1205,22 +1257,24 @@ class layout():
height=ymax - ymin)
return rect
def copy_power_pins(self, inst, name, add_vias=True):
def copy_power_pins(self, inst, name, add_vias=True, new_name=""):
"""
This will copy a power pin if it is on the lowest power_grid layer.
If it is on M1, it will add a power via too.
"""
pins = inst.get_pins(name)
for pin in pins:
if new_name == "":
new_name = pin.name
if pin.layer == self.pwr_grid_layer:
self.add_layout_pin(name,
self.add_layout_pin(new_name,
pin.layer,
pin.ll(),
pin.width(),
pin.height())
elif add_vias:
self.copy_power_pin(pin)
self.copy_power_pin(pin, new_name=new_name)
def add_io_pin(self, instance, pin_name, new_name, start_layer=None):
"""
@ -1266,13 +1320,15 @@ class layout():
width=width,
height=height)
def copy_power_pin(self, pin, loc=None, directions=None):
def copy_power_pin(self, pin, loc=None, directions=None, new_name=""):
"""
Add a single power pin from the lowest power_grid layer down to M1 (or li) at
the given center location. The starting layer is specified to determine
which vias are needed.
"""
if new_name == "":
new_name = pin.name
if not loc:
loc = pin.center()
@ -1286,7 +1342,7 @@ class layout():
height = None
if pin.layer == self.pwr_grid_layer:
self.add_layout_pin_rect_center(text=pin.name,
self.add_layout_pin_rect_center(text=new_name,
layer=self.pwr_grid_layer,
offset=loc,
width=width,
@ -1301,7 +1357,7 @@ class layout():
width = via.width
if not height:
height = via.height
self.add_layout_pin_rect_center(text=pin.name,
self.add_layout_pin_rect_center(text=new_name,
layer=self.pwr_grid_layer,
offset=loc,
width=width,
@ -1357,7 +1413,7 @@ class layout():
[ll, ur] = bbox
# Possibly inflate the bbox
nwell_offset = vector(self.nwell_width, self.nwell_width)
nwell_offset = vector(2 * self.nwell_width, 2 * self.nwell_width)
ll -= nwell_offset.scale(inflate, inflate)
ur += nwell_offset.scale(inflate, inflate)
@ -1396,7 +1452,7 @@ class layout():
to_layer="m1",
offset=loc)
else:
self.add_power_pin(name="gnd",
self.add_power_pin(name="vdd",
loc=loc,
start_layer="li")
count += 1
@ -1416,7 +1472,7 @@ class layout():
to_layer="m1",
offset=loc)
else:
self.add_power_pin(name="gnd",
self.add_power_pin(name="vdd",
loc=loc,
start_layer="li")
count += 1
@ -1436,7 +1492,7 @@ class layout():
to_layer="m2",
offset=loc)
else:
self.add_power_pin(name="gnd",
self.add_power_pin(name="vdd",
loc=loc,
start_layer="li")
count += 1
@ -1456,7 +1512,7 @@ class layout():
to_layer="m2",
offset=loc)
else:
self.add_power_pin(name="gnd",
self.add_power_pin(name="vdd",
loc=loc,
start_layer="li")
count += 1

View File

@ -112,23 +112,25 @@ class lef:
for pin_name in self.pins:
pins = self.get_pins(pin_name)
for pin in pins:
inflated_pin = pin.inflated_pin(multiple=1)
another_iteration_needed = True
while another_iteration_needed:
another_iteration_needed = False
inflated_pin = pin.inflated_pin(multiple=2)
continue_fragmenting = True
while continue_fragmenting:
continue_fragmenting = False
old_blockages = list(self.blockages[pin.layer])
for blockage in old_blockages:
if blockage.overlaps(inflated_pin):
intersection_shape = blockage.intersection(inflated_pin)
# If it is zero area, don't add the pin
# If it is zero area, don't split the blockage
if intersection_shape[0][0]==intersection_shape[1][0] or intersection_shape[0][1]==intersection_shape[1][1]:
continue
another_iteration_needed = True
# Remove the old blockage and add the new ones
self.blockages[pin.layer].remove(blockage)
intersection_pin = pin_layout("", intersection_shape, inflated_pin.layer)
new_blockages = blockage.cut(intersection_pin)
self.blockages[pin.layer].extend(new_blockages)
# We split something so make another pass
continue_fragmenting = True
def lef_write_header(self):
""" Header of LEF file """

View File

@ -606,7 +606,9 @@ class pin_layout:
# Don't add the existing shape in if it overlaps the pin shape
if new_shape.contains(shape):
continue
new_shapes.append(new_shape)
# Only add non-zero shapes
if new_shape.area() > 0:
new_shapes.append(new_shape)
return new_shapes

View File

@ -148,12 +148,15 @@ def get_gds_pins(pin_names, name, gds_filename, units):
cell[str(pin_name)] = []
pin_list = cell_vlsi.getPinShape(str(pin_name))
for pin_shape in pin_list:
(lpp, boundary) = pin_shape
rect = [vector(boundary[0], boundary[1]),
vector(boundary[2], boundary[3])]
# this is a list because other cells/designs
# may have must-connect pins
cell[str(pin_name)].append(pin_layout(pin_name, rect, lpp))
if pin_shape != None:
(lpp, boundary) = pin_shape
rect = [vector(boundary[0], boundary[1]),
vector(boundary[2], boundary[3])]
# this is a list because other cells/designs
# may have must-connect pins
if isinstance(lpp[1], list):
lpp = (lpp[0], None)
cell[str(pin_name)].append(pin_layout(pin_name, rect, lpp))
_GDS_PINS_CACHE[k] = cell
return dict(cell)

View File

@ -24,7 +24,7 @@ debug.info(1, "Initializing characterizer...")
OPTS.spice_exe = ""
if not OPTS.analytical_delay:
if OPTS.spice_name != "":
if OPTS.spice_name:
# Capitalize Xyce
if OPTS.spice_name == "xyce":
OPTS.spice_name = "Xyce"
@ -45,7 +45,7 @@ if not OPTS.analytical_delay:
if OPTS.spice_name == "ngspice":
os.environ["NGSPICE_INPUT_DIR"] = "{0}".format(OPTS.openram_temp)
if OPTS.spice_exe == "":
if not OPTS.spice_exe:
debug.error("No recognizable spice version found. Unable to perform characterization.", 1)
else:
debug.info(1, "Finding spice simulator: {} ({})".format(OPTS.spice_name, OPTS.spice_exe))

View File

@ -276,8 +276,8 @@ class stimuli():
self.sf.write(".OPTIONS MEASURE MEASFAIL=1\n")
self.sf.write(".OPTIONS LINSOL type=klu\n")
self.sf.write(".TRAN {0}p {1}n\n".format(timestep, end_time))
else:
debug.error("Unkown spice simulator {}".format(OPTS.spice_name))
elif OPTS.spice_name:
debug.error("Unkown spice simulator {}".format(OPTS.spice_name), -1)
# create plots for all signals
if not OPTS.use_pex: # Don't save all for extracted simulations

View File

@ -9,8 +9,8 @@ nominal_corner_only = True
# Local wordlines have issues with met3 power routing for now
#local_array_size = 16
#route_supplies = "ring"
route_supplies = "left"
route_supplies = "ring"
#route_supplies = "left"
check_lvsdrc = True
#perimeter_pins = False
#netlist_only = True

View File

@ -3,7 +3,7 @@ from datetime import *
import numpy as np
import math
import debug
from tech import use_purpose
class VlsiLayout:
"""Class represent a hierarchical layout"""
@ -215,9 +215,13 @@ class VlsiLayout:
self.deduceHierarchy()
# self.traverseTheHierarchy()
self.populateCoordinateMap()
#only ones with text
for layerNumber in self.layerNumbersInUse:
self.processLabelPins((layerNumber, None))
#if layerNumber not in no_pin_shape:
if layerNumber in use_purpose:
self.processLabelPins((layerNumber, use_purpose[layerNumber]))
else:
self.processLabelPins((layerNumber, None))
def populateCoordinateMap(self):
def addToXyTree(startingStructureName = None,transformPath = None):
@ -903,6 +907,16 @@ def sameLPP(lpp1, lpp2):
if lpp1[1] == None or lpp2[1] == None:
return lpp1[0] == lpp2[0]
if isinstance(lpp1[1], list):
for i in range(len(lpp1[1])):
if lpp1[0] == lpp2[0] and lpp1[1][i] == lpp2[1]:
return True
if isinstance(lpp2[1], list):
for i in range(len(lpp2[1])):
if lpp1[0] == lpp2[0] and lpp1[1] == lpp2[1][i]:
return True
return lpp1[0] == lpp2[0] and lpp1[1] == lpp2[1]

View File

@ -238,8 +238,8 @@ def setup_bitcell():
OPTS.dummy_bitcell = "dummy_pbitcell"
OPTS.replica_bitcell = "replica_pbitcell"
else:
num_ports = OPTS.num_rw_ports + OPTS.num_w_ports + OPTS.num_r_ports
OPTS.bitcell = "bitcell_{}port".format(num_ports)
OPTS.num_ports = OPTS.num_rw_ports + OPTS.num_w_ports + OPTS.num_r_ports
OPTS.bitcell = "bitcell_{}port".format(OPTS.num_ports)
OPTS.dummy_bitcell = "dummy_" + OPTS.bitcell
OPTS.replica_bitcell = "replica_" + OPTS.bitcell
@ -607,7 +607,7 @@ def report_status():
# If a write mask is specified by the user, the mask write size should be the same as
# the word size so that an entire word is written at once.
if OPTS.write_size is not None:
if OPTS.write_size is not None and OPTS.write_size != OPTS.word_size:
if (OPTS.word_size % OPTS.write_size != 0):
debug.error("Write size needs to be an integer multiple of word size.")
# If write size is more than half of the word size,

View File

@ -366,18 +366,13 @@ class bank(design.design):
# A space for wells or jogging m2
self.m2_gap = max(2 * drc("pwell_to_nwell") + drc("nwell_enclose_active"),
3 * self.m2_pitch)
3 * self.m2_pitch,
drc("nwell_to_nwell"))
def add_modules(self):
""" Add all the modules using the class loader """
self.port_address = []
for port in self.all_ports:
self.port_address.append(factory.create(module_type="port_address",
cols=self.num_cols + self.num_spare_cols,
rows=self.num_rows,
port=port))
self.add_mod(self.port_address[port])
local_array_size = OPTS.local_array_size
@ -397,12 +392,26 @@ class bank(design.design):
cols=self.num_cols + self.num_spare_cols,
rows=self.num_rows)
self.add_mod(self.bitcell_array)
if self.num_spare_cols == 0:
self.num_spare_cols = (self.bitcell_array.column_size % (self.word_size *self.words_per_row))
self.port_address = []
for port in self.all_ports:
self.port_address.append(factory.create(module_type="port_address",
cols=self.bitcell_array.column_size,
rows=self.bitcell_array.row_size,
port=port))
self.add_mod(self.port_address[port])
self.port_data = []
self.bit_offsets = self.get_column_offsets()
for port in self.all_ports:
temp_pre = factory.create(module_type="port_data",
sram_config=self.sram_config,
dimension_override=True,
cols=self.bitcell_array.column_size - self.num_spare_cols,
rows=self.bitcell_array.row_size,
num_spare_cols=self.num_spare_cols,
port=port,
bit_offsets=self.bit_offsets)
self.port_data.append(temp_pre)
@ -430,7 +439,9 @@ class bank(design.design):
temp.append("vdd")
temp.append("gnd")
if 'vpb' in self.bitcell_array_inst.mod.pins and 'vnb' in self.bitcell_array_inst.mod.pins:
temp.append('vpb')
temp.append('vnb')
self.connect_inst(temp)
def place_bitcell_array(self, offset):
@ -489,7 +500,7 @@ class bank(design.design):
mod=self.port_address[port])
temp = []
for bit in range(self.row_addr_size):
for bit in range(ceil(log(self.bitcell_array.row_size, 2))):
temp.append("addr{0}_{1}".format(port, bit + self.col_addr_size))
temp.append("wl_en{}".format(port))
wordline_names = self.bitcell_array.get_wordline_names(port)
@ -614,6 +625,10 @@ class bank(design.design):
self.copy_power_pins(inst, "vdd", add_vias=False)
self.copy_power_pins(inst, "gnd", add_vias=False)
if 'vpb' in self.bitcell_array_inst.mod.pins and 'vnb' in self.bitcell_array_inst.mod.pins:
for pin_name, supply_name in zip(['vpb','vnb'],['vdd','gnd']):
self.copy_power_pins(self.bitcell_array_inst, pin_name, new_name=supply_name)
# If we use the pinvbuf as the decoder, we need to add power pins.
# Other decoders already have them.
if self.col_addr_size == 1:
@ -1062,7 +1077,6 @@ class bank(design.design):
to_layer="m2",
offset=control_pos)
def graph_exclude_precharge(self):
"""
Precharge adds a loop between bitlines, can be excluded to reduce complexity

View File

@ -109,7 +109,7 @@ class dff_buf(design.design):
except AttributeError:
pass
well_spacing += self.well_extend_active
well_spacing += 2 * self.well_extend_active
self.inv1_inst.place(vector(self.dff_inst.rx() + well_spacing, 0))

View File

@ -12,6 +12,10 @@ from vector import vector
from sram_factory import factory
from globals import OPTS
from tech import layer_properties as layer_props
from tech import layer_indices
from tech import layer_stacks
from tech import preferred_directions
from tech import drc
class hierarchical_predecode(design.design):
@ -29,7 +33,7 @@ class hierarchical_predecode(design.design):
self.cell_height = height
self.column_decoder = column_decoder
self.input_and_rail_pos = []
self.number_of_outputs = int(math.pow(2, self.number_of_inputs))
super().__init__(name)
@ -183,9 +187,9 @@ class hierarchical_predecode(design.design):
def route(self):
self.route_input_inverters()
self.route_output_inverters()
self.route_inputs_to_rails()
self.route_input_ands()
self.route_output_inverters()
self.route_inputs_to_rails()
self.route_output_ands()
self.route_vdd_gnd()
@ -274,8 +278,46 @@ class hierarchical_predecode(design.design):
# pins in the and gates.
inv_out_pos = inv_out_pin.rc()
y_offset = (inv_num + 1) * self.inv.height - self.output_layer_pitch
right_pos = inv_out_pos + vector(self.inv.width - self.inv.get_pin("Z").rx(), 0)
rail_pos = vector(self.decode_rails[out_pin].cx(), y_offset)
# create via for dimensions
from_layer = self.output_layer
to_layer = self.bus_layer
cur_layer = from_layer
from_id = layer_indices[cur_layer]
to_id = 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, layer_stacks), None)
via = factory.create(module_type="contact",
layer_stack=curr_stack,
dimensions=[1, 1],
directions=self.bus_directions)
overlapping_pin_space = drc["{0}_to_{0}".format(self.output_layer)]
total_buffer_space = (overlapping_pin_space + via.height)
#FIXME: compute rail locations instead of just guessing and nudging
while(True):
drc_error = 0
for and_input in self.input_and_rail_pos:
if and_input.x == rail_pos.x:
if (abs(y_offset - and_input.y) < total_buffer_space) and (abs(y_offset - and_input.y) > via.height):
drc_error = 1
if drc_error == 0:
break
else:
y_offset += drc["grid"]
rail_pos.y = y_offset
right_pos = inv_out_pos + vector(self.inv.width - self.inv.get_pin("Z").rx(), 0)
self.add_path(self.output_layer, [inv_out_pos, right_pos, vector(right_pos.x, y_offset), rail_pos])
self.add_via_stack_center(from_layer=inv_out_pin.layer,
@ -316,6 +358,7 @@ class hierarchical_predecode(design.design):
to_layer=self.bus_layer,
offset=rail_pos,
directions=self.bus_directions)
self.input_and_rail_pos.append(rail_pos)
if gate_pin == "A":
direction = None
else:

View File

@ -11,6 +11,7 @@ from sram_factory import factory
from collections import namedtuple
from vector import vector
from globals import OPTS
from tech import cell_properties
from tech import layer_properties as layer_props
@ -20,26 +21,36 @@ class port_data(design.design):
Port 0 always has the RBL on the left while port 1 is on the right.
"""
def __init__(self, sram_config, port, bit_offsets=None, name=""):
def __init__(self, sram_config, port, num_spare_cols=None, bit_offsets=None, name="", rows=None, cols=None, dimension_override=False):
sram_config.set_local_config(self)
if dimension_override:
self.num_rows = rows
self.num_cols = cols
self.word_size = sram_config.word_size
self.port = port
if self.write_size is not None:
self.num_wmasks = int(math.ceil(self.word_size / self.write_size))
else:
self.num_wmasks = 0
if self.num_spare_cols is None:
if num_spare_cols:
self.num_spare_cols = num_spare_cols
elif self.num_spare_cols is None:
self.num_spare_cols = 0
if not bit_offsets:
bitcell = factory.create(module_type=OPTS.bitcell)
if(cell_properties.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_properties.strap_module, version=cell_properties.strap_version)
precharge_width = bitcell.width + strap.width
else:
precharge_width = bitcell.width
self.bit_offsets = []
for i in range(self.num_cols + self.num_spare_cols):
self.bit_offsets.append(i * bitcell.width)
self.bit_offsets.append(i * precharge_width)
else:
self.bit_offsets = bit_offsets
if name == "":
name = "port_data_{0}".format(self.port)
super().__init__(name)
@ -117,7 +128,6 @@ class port_data(design.design):
for bit in range(self.num_spare_cols):
self.add_pin("sparebl_{0}".format(bit), "INOUT")
self.add_pin("sparebr_{0}".format(bit), "INOUT")
if self.port in self.read_ports:
for bit in range(self.word_size + self.num_spare_cols):
self.add_pin("dout_{}".format(bit), "OUTPUT")
@ -191,14 +201,19 @@ class port_data(design.design):
# and mirroring happens correctly
# Used for names/dimensions only
self.cell = factory.create(module_type=OPTS.bitcell)
cell = factory.create(module_type=OPTS.bitcell)
if(cell_properties.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_properties.strap_module, version=cell_properties.strap_version)
precharge_width = cell.width + strap.width
else:
precharge_width = cell.width
if self.port == 0:
# Append an offset on the left
precharge_bit_offsets = [self.bit_offsets[0] - self.cell.width] + self.bit_offsets
precharge_bit_offsets = [self.bit_offsets[0] - precharge_width] + self.bit_offsets
else:
# Append an offset on the right
precharge_bit_offsets = self.bit_offsets + [self.bit_offsets[-1] + self.cell.width]
precharge_bit_offsets = self.bit_offsets + [self.bit_offsets[-1] + precharge_width]
self.precharge_array = factory.create(module_type="precharge_array",
columns=self.num_cols + self.num_spare_cols + 1,
offsets=precharge_bit_offsets,
@ -567,19 +582,32 @@ class port_data(design.design):
off = 1
else:
off = 0
if OPTS.num_ports > 1:
self.channel_route_bitlines(inst1=self.column_mux_array_inst,
inst1_bls_template="{inst}_out_{bit}",
inst2=inst2,
num_bits=self.word_size,
inst1_start_bit=start_bit)
self.channel_route_bitlines(inst1=self.column_mux_array_inst,
inst1_bls_template="{inst}_out_{bit}",
inst2=inst2,
num_bits=self.word_size,
inst1_start_bit=start_bit)
self.channel_route_bitlines(inst1=self.precharge_array_inst,
inst1_bls_template="{inst}_{bit}",
inst2=inst2,
num_bits=self.num_spare_cols,
inst1_start_bit=self.num_cols + off,
inst2_start_bit=self.word_size)
else:
self.connect_bitlines(inst1=self.column_mux_array_inst,
inst1_bls_template="{inst}_out_{bit}",
inst2=inst2,
num_bits=self.word_size,
inst1_start_bit=start_bit)
self.channel_route_bitlines(inst1=self.precharge_array_inst,
inst1_bls_template="{inst}_{bit}",
inst2=inst2,
num_bits=self.num_spare_cols,
inst1_start_bit=self.num_cols + off,
inst2_start_bit=self.word_size)
self.connect_bitlines(inst1=self.precharge_array_inst,
inst1_bls_template="{inst}_{bit}",
inst2=inst2,
num_bits=self.num_spare_cols,
inst1_start_bit=self.num_cols + off,
inst2_start_bit=self.word_size)
elif layer_props.port_data.channel_route_bitlines:
self.channel_route_bitlines(inst1=inst1,

View File

@ -76,8 +76,8 @@ class precharge_array(design.design):
size=self.size,
bitcell_bl=self.bitcell_bl,
bitcell_br=self.bitcell_br)
self.add_mod(self.pc_cell)
self.cell = factory.create(module_type=OPTS.bitcell)
def add_layout_pins(self):

View File

@ -6,7 +6,7 @@
import debug
from bitcell_base_array import bitcell_base_array
from tech import drc, spice
from tech import drc, spice, cell_properties
from vector import vector
from globals import OPTS
from sram_factory import factory

View File

@ -8,6 +8,7 @@
import design
from vector import vector
from sram_factory import factory
from tech import cell_properties
import debug
from globals import OPTS
@ -41,7 +42,6 @@ class sense_amp_array(design.design):
self.en_layer = "m3"
else:
self.en_layer = "m1"
self.create_netlist()
if not OPTS.netlist_only:
self.create_layout()
@ -109,15 +109,22 @@ class sense_amp_array(design.design):
self.en_name, "vdd", "gnd"])
def place_sense_amp_array(self):
if self.bitcell.width > self.amp.width:
self.amp_spacing = self.bitcell.width
cell = factory.create(module_type=OPTS.bitcell)
if(cell_properties.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_properties.strap_module, version=cell_properties.strap_version)
precharge_width = cell.width + strap.width
else:
precharge_width = cell.width
if precharge_width > self.amp.width:
self.amp_spacing = precharge_width
else:
self.amp_spacing = self.amp.width
if not self.offsets:
self.offsets = []
for i in range(self.num_cols + self.num_spare_cols):
self.offsets.append(i * self.bitcell.width)
self.offsets.append(i * self.amp_spacing)
for i, xoffset in enumerate(self.offsets[0:self.num_cols:self.words_per_row]):
if self.bitcell.mirror.y and (i * self.words_per_row + self.column_offset) % 2:
@ -128,13 +135,12 @@ class sense_amp_array(design.design):
amp_position = vector(xoffset, 0)
self.local_insts[i].place(offset=amp_position, mirror=mirror)
# place spare sense amps (will share the same enable as regular sense amps)
for i, xoffset in enumerate(self.offsets[self.num_cols:]):
index = self.word_size + i
if self.bitcell.mirror.y and (index + self.column_offset) % 2:
mirror = "MY"
xoffset = xoffset + self.amp_width
xoffset = xoffset + self.amp_spacing
else:
mirror = ""

View File

@ -72,7 +72,9 @@ class options(optparse.Values):
# This is the temp directory where all intermediate results are stored.
try:
# If user defined the temporary location in their environment, use it
openram_temp = os.path.abspath(os.environ.get("OPENRAM_TMP"))
except:
openram_temp = "/tmp"
@ -99,6 +101,7 @@ class options(optparse.Values):
netlist_only = False
# Whether we should do the final power routing
route_supplies = "tree"
supply_pin_type = "ring"
# This determines whether LVS and DRC is checked at all.
check_lvsdrc = False
# This determines whether LVS and DRC is checked for every submodule.
@ -119,13 +122,13 @@ class options(optparse.Values):
# Tool options
###################
# Variable to select the variant of spice
spice_name = ""
spice_name = None
# The spice executable being used which is derived from the user PATH.
spice_exe = ""
spice_exe = None
# Variable to select the variant of drc, lvs, pex
drc_name = ""
lvs_name = ""
pex_name = ""
drc_name = None
lvs_name = None
pex_name = None
# The DRC/LVS/PEX executable being used
# which is derived from the user PATH.
drc_exe = None

View File

@ -56,7 +56,13 @@ class column_mux(pgate.pgate):
self.place_ptx()
self.width = self.bitcell.width
cell = factory.create(module_type=OPTS.bitcell)
if(cell_props.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_props.strap_module, version=cell_props.strap_version)
precharge_width = cell.width + strap.width
else:
precharge_width = cell.width
self.width = precharge_width
self.height = self.nmos_upper.uy() + self.pin_height
self.connect_poly()
@ -217,10 +223,15 @@ class column_mux(pgate.pgate):
Add a well and implant over the whole cell. Also, add the
pwell contact (if it exists)
"""
if(cell_props.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_props.strap_module, version=cell_props.strap_version)
rbc_width = self.bitcell.width + strap.width
else:
rbc_width = self.bitcell.width
# Add it to the right, aligned in between the two tx
active_pos = vector(self.bitcell.width,
active_pos = vector(rbc_width,
self.nmos_upper.by() - 0.5 * self.poly_space)
self.add_via_center(layers=self.active_stack,
offset=active_pos,
implant_type="p",
@ -239,5 +250,5 @@ class column_mux(pgate.pgate):
if "pwell" in layer:
self.add_rect(layer="pwell",
offset=vector(0, 0),
width=self.bitcell.width,
width=rbc_width,
height=self.height)

View File

@ -30,7 +30,11 @@ class precharge(design.design):
self.beta = parameter["beta"]
self.ptx_width = self.beta * parameter["min_tx_size"]
self.ptx_mults = 1
self.width = self.bitcell.width
if(cell_props.use_strap == True and OPTS.num_ports == 1):
strap = factory.create(module_type=cell_props.strap_module, version=cell_props.strap_version)
self.width = self.bitcell.width + strap.width
else:
self.width = self.bitcell.width
self.bitcell_bl = bitcell_bl
self.bitcell_br = bitcell_br
self.bitcell_bl_pin =self.bitcell.get_pin(self.bitcell_bl)

View File

@ -37,6 +37,8 @@ class grid:
# This is really lower left bottom layer and upper right top layer in 3D.
self.ll = vector3d(ll.x, ll.y, 0).scale(self.track_factor).round()
self.ur = vector3d(ur.x, ur.y, 0).scale(self.track_factor).round()
debug.info(1, "BBOX coords: ll=" + str(ll) + " ur=" + str(ur))
debug.info(1, "BBOX grids: ll=" + str(self.ll) + " ur=" + str(self.ur))
# let's leave the map sparse, cells are created on demand to reduce memory
self.map={}
@ -127,33 +129,47 @@ class grid:
Side specifies which side.
Layer specifies horizontal (0) or vertical (1)
Width specifies how wide the perimter "stripe" should be.
Works from the inside out from the bbox (ll, ur)
"""
if "ring" in side:
ring_width = width
else:
ring_width = 0
if "ring" in side:
ring_offset = offset
else:
ring_offset = 0
perimeter_list = []
# Add the left/right columns
if side=="all" or side=="left":
for x in range(self.ll.x + offset, self.ll.x + width + offset, 1):
for y in range(self.ll.y + offset + margin, self.ur.y - offset - margin, 1):
if side=="all" or "left" in side:
for x in range(self.ll.x - offset, self.ll.x - width - offset, -1):
for y in range(self.ll.y - ring_offset - margin - ring_width + 1, self.ur.y + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or side=="right":
for x in range(self.ur.x - width - offset, self.ur.x - offset, 1):
for y in range(self.ll.y + offset + margin, self.ur.y - offset - margin, 1):
if side=="all" or "right" in side:
for x in range(self.ur.x + offset, self.ur.x + width + offset, 1):
for y in range(self.ll.y - ring_offset - margin - ring_width + 1, self.ur.y + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or side=="bottom":
for y in range(self.ll.y + offset, self.ll.y + width + offset, 1):
for x in range(self.ll.x + offset + margin, self.ur.x - offset - margin, 1):
if side=="all" or "bottom" in side:
for y in range(self.ll.y - offset, self.ll.y - width - offset, -1):
for x in range(self.ll.x - ring_offset - margin - ring_width + 1, self.ur.x + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
if side=="all" or side=="top":
for y in range(self.ur.y - width - offset, self.ur.y - offset, 1):
for x in range(self.ll.x + offset + margin, self.ur.x - offset - margin, 1):
if side=="all" or "top" in side:
for y in range(self.ur.y + offset, self.ur.y + width + offset, 1):
for x in range(self.ll.x - ring_offset - margin - ring_width + 1, self.ur.x + ring_offset + margin + ring_width, 1):
for layer in layers:
perimeter_list.append(vector3d(x, y, layer))
# Add them all to the map
self.add_map(perimeter_list)
return perimeter_list
def add_perimeter_target(self, side="all", layers=[0, 1]):

View File

@ -82,31 +82,13 @@ class router(router_tech):
"""
Initialize the ll,ur values with the paramter or using the layout boundary.
"""
# If didn't specify a gds blockage file, write it out to read the gds
# This isn't efficient, but easy for now
# Load the gds file and read in all the shapes
self.cell.gds_write(self.gds_filename)
self.layout = gdsMill.VlsiLayout(units=GDS["unit"])
self.reader = gdsMill.Gds2reader(self.layout)
self.reader.loadFromFile(self.gds_filename)
self.top_name = self.layout.rootStructureName
if not bbox:
# The boundary will determine the limits to the size
# of the routing grid
self.boundary = self.layout.measureBoundary(self.top_name)
# These must be un-indexed to get rid of the matrix type
self.ll = vector(self.boundary[0][0], self.boundary[0][1])
self.ur = vector(self.boundary[1][0], self.boundary[1][1])
self.bbox = self.cell.get_bbox(margin)
else:
self.ll, self.ur = bbox
self.bbox = bbox
(self.ll, self.ur) = self.bbox
margin_offset = vector(margin, margin)
self.bbox = (self.ll - margin_offset, self.ur + margin_offset)
size = self.ur - self.ll
debug.info(1, "Size: {0} x {1} with perimeter margin {2}".format(size.x, size.y, margin))
def get_bbox(self):
return self.bbox
@ -893,19 +875,21 @@ class router(router_tech):
# Clearing the blockage of this pin requires the inflated pins
self.clear_blockages(pin_name)
def add_side_supply_pin(self, name, side="left", width=2):
def add_side_supply_pin(self, name, side="left", width=3, space=2):
"""
Adds a supply pin to the perimeter and resizes the bounding box.
"""
pg = pin_group(name, [], self)
# Offset two spaces inside and one between the rings
if name == "gnd":
offset = width + 1
offset = width + 2 * space
else:
offset = 1
offset = space
if side in ["left", "right"]:
layers = [1]
else:
layers = [0]
pg.grids = set(self.rg.get_perimeter_list(side=side,
width=width,
margin=self.margin,
@ -920,39 +904,39 @@ class router(router_tech):
self.new_pins[name] = pg.pins
def add_ring_supply_pin(self, name, width=2):
def add_ring_supply_pin(self, name, width=3, space=2):
"""
Adds a ring supply pin that goes inside the given bbox.
"""
pg = pin_group(name, [], self)
# Offset the vdd inside one ring width
# Offset two spaces inside and one between the rings
# Units are in routing grids
if name == "gnd":
offset = width + 1
offset = width + 2 * space
else:
offset = 1
offset = space
# LEFT
left_grids = set(self.rg.get_perimeter_list(side="left",
left_grids = set(self.rg.get_perimeter_list(side="left_ring",
width=width,
margin=self.margin,
offset=offset,
layers=[1]))
# RIGHT
right_grids = set(self.rg.get_perimeter_list(side="right",
right_grids = set(self.rg.get_perimeter_list(side="right_ring",
width=width,
margin=self.margin,
offset=offset,
layers=[1]))
# TOP
top_grids = set(self.rg.get_perimeter_list(side="top",
top_grids = set(self.rg.get_perimeter_list(side="top_ring",
width=width,
margin=self.margin,
offset=offset,
layers=[0]))
# BOTTOM
bottom_grids = set(self.rg.get_perimeter_list(side="bottom",
bottom_grids = set(self.rg.get_perimeter_list(side="bottom_ring",
width=width,
margin=self.margin,
offset=offset,

View File

@ -21,7 +21,7 @@ class supply_grid_router(router):
routes a grid to connect the supply on the two layers.
"""
def __init__(self, layers, design, margin=0, bbox=None):
def __init__(self, layers, design, bbox=None, pin_type=None):
"""
This will route on layers in design. It will get the blockages from
either the gds file name or the design itself (by saving to a gds file).

View File

@ -34,7 +34,7 @@ class supply_tree_router(router):
# The pin escape router already made the bounding box big enough,
# so we can use the regular bbox here.
if pin_type:
debug.check(pin_type in ["left", "right", "top", "bottom", "tree", "ring"],
debug.check(pin_type in ["left", "right", "top", "bottom", "single", "ring"],
"Invalid pin type {}".format(pin_type))
self.pin_type = pin_type
router.__init__(self,

View File

@ -9,6 +9,7 @@ from vector import vector
from sram_base import sram_base
from contact import m2_via
from channel_route import channel_route
from router_tech import router_tech
from globals import OPTS
@ -329,13 +330,31 @@ class sram_1bank(sram_base):
# Some technologies have an isolation
self.add_dnwell(inflate=2)
# We need the initial bbox for the supply rings later
# because the perimeter pins will change the bbox
# Route the pins to the perimeter
pre_bbox = None
if OPTS.perimeter_pins:
self.route_escape_pins()
rt = router_tech(self.supply_stack, 1)
if OPTS.supply_pin_type in ["ring", "left", "right", "top", "bottom"]:
big_margin = 12 * rt.track_width
little_margin = 2 * rt.track_width
else:
big_margin = 6 * rt.track_width
little_margin = 0
pre_bbox = self.get_bbox(side="ring",
big_margin=rt.track_width)
bbox = self.get_bbox(side=OPTS.supply_pin_type,
big_margin=big_margin,
little_margin=little_margin)
self.route_escape_pins(bbox)
# Route the supplies first since the MST is not blockage aware
# and signals can route to anywhere on sides (it is flexible)
self.route_supplies()
self.route_supplies(pre_bbox)
def route_dffs(self, add_routes=True):
@ -366,6 +385,7 @@ class sram_1bank(sram_base):
if len(route_map) > 0:
# This layer stack must be different than the data dff layer stack
layer_stack = self.m1_stack
if port == 0:
@ -375,11 +395,11 @@ class sram_1bank(sram_base):
offset=offset,
layer_stack=layer_stack,
parent=self)
# This causes problem in magic since it sometimes cannot extract connectivity of isntances
# This causes problem in magic since it sometimes cannot extract connectivity of instances
# with no active devices.
self.add_inst(cr.name, cr)
self.connect_inst([])
#self.add_flat_inst(cr.name, cr)
# self.add_flat_inst(cr.name, cr)
else:
offset = vector(0,
self.bank.height + self.m3_pitch)
@ -387,11 +407,11 @@ class sram_1bank(sram_base):
offset=offset,
layer_stack=layer_stack,
parent=self)
# This causes problem in magic since it sometimes cannot extract connectivity of isntances
# This causes problem in magic since it sometimes cannot extract connectivity of instances
# with no active devices.
self.add_inst(cr.name, cr)
self.connect_inst([])
#self.add_flat_inst(cr.name, cr)
# self.add_flat_inst(cr.name, cr)
def route_data_dffs(self, port, add_routes):
route_map = []
@ -422,40 +442,49 @@ class sram_1bank(sram_base):
if len(route_map) > 0:
# The write masks will have blockages on M1
# if self.num_wmasks > 0 and port in self.write_ports:
# layer_stack = self.m3_stack
# else:
# layer_stack = self.m1_stack
# This layer stack must be different than the column addr dff layer stack
layer_stack = self.m3_stack
if port == 0:
# This is relative to the bank at 0,0 or the s_en which is routed on M3 also
if "s_en" in self.control_logic_insts[port].mod.pin_map:
y_bottom = min(0, self.control_logic_insts[port].get_pin("s_en").by())
else:
y_bottom = 0
y_offset = y_bottom - self.data_bus_size[port] + 2 * self.m3_pitch
offset = vector(self.control_logic_insts[port].rx() + self.dff.width,
- self.data_bus_size[port] + 2 * self.m3_pitch)
y_offset)
cr = channel_route(netlist=route_map,
offset=offset,
layer_stack=layer_stack,
parent=self)
if add_routes:
# This causes problem in magic since it sometimes cannot extract connectivity of isntances
# This causes problem in magic since it sometimes cannot extract connectivity of instances
# with no active devices.
self.add_inst(cr.name, cr)
self.connect_inst([])
#self.add_flat_inst(cr.name, cr)
# self.add_flat_inst(cr.name, cr)
else:
self.data_bus_size[port] = max(cr.height, self.col_addr_bus_size[port]) + self.data_bus_gap
else:
if "s_en" in self.control_logic_insts[port].mod.pin_map:
y_top = max(self.bank.height, self.control_logic_insts[port].get_pin("s_en").uy())
else:
y_top = self.bank.height
y_offset = y_top + self.m3_pitch
offset = vector(0,
self.bank.height + self.m3_pitch)
y_offset)
cr = channel_route(netlist=route_map,
offset=offset,
layer_stack=layer_stack,
parent=self)
if add_routes:
# This causes problem in magic since it sometimes cannot extract connectivity of isntances
# This causes problem in magic since it sometimes cannot extract connectivity of instances
# with no active devices.
self.add_inst(cr.name, cr)
self.connect_inst([])
#self.add_flat_inst(cr.name, cr)
# self.add_flat_inst(cr.name, cr)
else:
self.data_bus_size[port] = max(cr.height, self.col_addr_bus_size[port]) + self.data_bus_gap

View File

@ -41,6 +41,14 @@ class sram_base(design, verilog, lef):
if not self.num_spare_cols:
self.num_spare_cols = 0
try:
from tech import power_grid
self.supply_stack = power_grid
except ImportError:
# if no power_grid is specified by tech we use sensible defaults
# Route a M3/M4 grid
self.supply_stack = self.m3_stack
def add_pins(self):
""" Add pins for entire SRAM. """
@ -230,7 +238,7 @@ class sram_base(design, verilog, lef):
def create_modules(self):
debug.error("Must override pure virtual function.", -1)
def route_supplies(self):
def route_supplies(self, bbox=None):
""" Route the supply grid and connect the pins to them. """
# Copy the pins to the top level
@ -239,29 +247,21 @@ class sram_base(design, verilog, lef):
for inst in self.insts:
self.copy_power_pins(inst, pin_name, self.ext_supply[pin_name])
try:
from tech import power_grid
grid_stack = power_grid
except ImportError:
# if no power_grid is specified by tech we use sensible defaults
# Route a M3/M4 grid
grid_stack = self.m3_stack
if not OPTS.route_supplies:
# Do not route the power supply (leave as must-connect pins)
return
elif OPTS.route_supplies == "grid":
from supply_grid_router import supply_grid_router as router
rtr=router(grid_stack, self)
else:
from supply_tree_router import supply_tree_router as router
rtr=router(grid_stack,
self,
pin_type=OPTS.route_supplies)
rtr=router(layers=self.supply_stack,
design=self,
bbox=bbox,
pin_type=OPTS.supply_pin_type)
rtr.route()
if OPTS.route_supplies in ["left", "right", "top", "bottom", "ring"]:
if OPTS.supply_pin_type in ["left", "right", "top", "bottom", "ring"]:
# Find the lowest leftest pin for vdd and gnd
for pin_name in ["vdd", "gnd"]:
# Copy the pin shape(s) to rectangles
@ -283,7 +283,7 @@ class sram_base(design, verilog, lef):
pin.width(),
pin.height())
elif OPTS.route_supplies:
elif OPTS.route_supplies and OPTS.supply_pin_type == "single":
# Update these as we may have routed outside the region (perimeter pins)
lowest_coord = self.find_lowest_coords()
@ -321,7 +321,7 @@ class sram_base(design, verilog, lef):
# Grid is left with many top level pins
pass
def route_escape_pins(self):
def route_escape_pins(self, bbox):
"""
Add the top-level pins for a single bank SRAM with control.
"""
@ -364,7 +364,7 @@ class sram_base(design, verilog, lef):
from signal_escape_router import signal_escape_router as router
rtr=router(layers=self.m3_stack,
design=self,
margin=8 * self.m3_pitch)
bbox=bbox)
rtr.escape_route(pins_to_route)
def compute_bus_sizes(self):
@ -472,6 +472,12 @@ class sram_base(design, verilog, lef):
self.bitcell = factory.create(module_type=OPTS.bitcell)
self.dff = factory.create(module_type="dff")
# Create the bank module (up to four are instantiated)
self.bank = factory.create("bank", sram_config=self.sram_config, module_name="bank")
self.add_mod(self.bank)
self.num_spare_cols = self.bank.num_spare_cols
# Create the address and control flops (but not the clk)
self.row_addr_dff = factory.create("dff_array", module_name="row_addr_dff", rows=self.row_addr_size, columns=1)
self.add_mod(self.row_addr_dff)
@ -493,10 +499,6 @@ class sram_base(design, verilog, lef):
self.spare_wen_dff = factory.create("dff_array", module_name="spare_wen_dff", rows=1, columns=self.num_spare_cols)
self.add_mod(self.spare_wen_dff)
# Create the bank module (up to four are instantiated)
self.bank = factory.create("bank", sram_config=self.sram_config, module_name="bank")
self.add_mod(self.bank)
# Create bank decoder
if(self.num_banks > 1):
self.add_multi_bank_modules()

View File

@ -45,8 +45,8 @@ class timing_setup_test(openram_test):
'setup_times_HL': [0.16357419999999998],
'setup_times_LH': [0.1757812]}
elif OPTS.tech_name == "sky130":
golden_data = {'hold_times_HL': [-0.05615234],
'hold_times_LH': [-0.03173828],
golden_data = {'hold_times_HL': [-0.03173828],
'hold_times_LH': [-0.05615234],
'setup_times_HL': [0.078125],
'setup_times_LH': [0.1025391]}
else:

View File

@ -45,8 +45,8 @@ class timing_setup_test(openram_test):
'setup_times_HL': [0.1757812],
'setup_times_LH': [0.1879883]}
elif OPTS.tech_name == "sky130":
golden_data = {'hold_times_HL': [-0.05615234],
'hold_times_LH': [-0.03173828],
golden_data = {'hold_times_HL': [-0.03173828],
'hold_times_LH': [-0.05615234],
'setup_times_HL': [0.078125],
'setup_times_LH': [0.1025391]}
else:

View File

@ -45,8 +45,8 @@ class timing_setup_test(openram_test):
'setup_times_HL': [0.16357419999999998],
'setup_times_LH': [0.1757812]}
elif OPTS.tech_name == "sky130":
golden_data = {'hold_times_HL': [-0.05615234],
'hold_times_LH': [-0.03173828],
golden_data = {'hold_times_HL': [-0.03173828],
'hold_times_LH': [-0.05615234],
'setup_times_HL': [0.078125],
'setup_times_LH': [0.1025391]}
else:

View File

@ -89,7 +89,10 @@ def write_drc_script(cell_name, gds_name, extract, final_verification, output_pa
f.write("{} -dnull -noconsole << EOF\n".format(OPTS.drc_exe[1]))
# Do not run DRC for extraction/conversion
f.write("drc off\n")
f.write("gds polygon subcell true\n")
f.write("set VDD vdd\n")
f.write("set GND gnd\n")
f.write("set SUB gnd\n")
#f.write("gds polygon subcell true\n")
f.write("gds warning default\n")
# These two options are temporarily disabled until Tim fixes a bug in magic related
# to flattening channel routes and vias (hierarchy with no devices in it). Otherwise,
@ -177,6 +180,10 @@ def write_drc_script(cell_name, gds_name, extract, final_verification, output_pa
f.write('puts "Finished drc check"\n')
f.write("drc catchup\n")
f.write('puts "Finished drc catchup"\n')
# This is needed instead of drc count total because it displays
# some errors that are not "DRC" errors.
# f.write("puts -nonewline \"Total DRC errors found: \"\n")
# f.write("puts stdout [drc listall count total]\n")
f.write("drc count total\n")
f.write("quit -noprompt\n")
f.write("EOF\n")
@ -244,11 +251,14 @@ def write_lvs_script(cell_name, gds_name, sp_name, final_verification=False, out
if not output_path:
output_path = OPTS.openram_temp
setup_file = "setup.tcl"
full_setup_file = OPTS.openram_tech + "tech/" + setup_file
if os.path.exists(full_setup_file):
# Copy .magicrc file into the output directory
setup_file = os.environ.get('OPENRAM_NETGENRC', None)
if not setup_file:
setup_file = OPTS.openram_tech + "tech/setup.tcl"
if os.path.exists(setup_file):
# Copy setup.tcl file into temp dir
shutil.copy(full_setup_file, output_path)
shutil.copy(setup_file, output_path)
else:
setup_file = 'nosetup'

View File

@ -135,6 +135,8 @@ layer["m10"] = (29, 0)
layer["text"] = (239, 0)
layer["boundary"]= (239, 0)
use_purpose = {}
# Layer names for external PDKs
layer_names = {}
layer_names["active"] = "active"

View File

@ -63,6 +63,7 @@ layer["text"] = (63, 0)
layer["boundary"] = (63, 0)
layer["blockage"] = (83, 0)
use_purpose = {}
###################################################
##END GDS Layer Map
###################################################

View File

@ -119,6 +119,8 @@ layer["m4"] = (31, 0)
layer["text"] = (63, 0)
layer["boundary"] = (63, 0)
use_purpose = {}
# Layer names for external PDKs
layer_names = {}
layer_names["active"] = "active"