OpenRAM/compiler/characterizer/delay.py

1562 lines
74 KiB
Python

# See LICENSE for licensing information.
#
# Copyright (c) 2016-2024 Regents of the University of California and The Board
# of Regents for the Oklahoma Agricultural and Mechanical College
# (acting for and on behalf of Oklahoma State University)
# All rights reserved.
#
import math
import shutil
from openram import debug
from openram import tech
from openram import OPTS
from .stimuli import *
from .trim_spice import *
from .charutils import *
from .simulation import simulation
from .measurements import *
from os import path
import re
class delay(simulation):
"""
Functions to measure the delay and power of an SRAM at a given address and
data bit.
In general, this will perform the following actions:
1) Trim the netlist to remove unnecessary logic.
2) Find a feasible clock period using max load/slew on the trimmed netlist.
3) Characterize all loads/slews and consider fail when delay is greater than 5% of feasible delay using trimmed netlist.
4) Measure the leakage during the last cycle of the trimmed netlist when there is no operation.
5) Measure the leakage of the whole netlist (untrimmed) in each corner.
6) Subtract the trimmed leakage and add the untrimmed leakage to the power.
Netlist trimming can be removed by setting OPTS.trim_netlist to
False, but this is VERY slow.
"""
def __init__(self, sram, spfile, corner, output_path=None):
super().__init__(sram, spfile, corner)
self.targ_read_ports = []
self.targ_write_ports = []
self.period = 0
if self.write_size != self.word_size:
self.num_wmasks = int(math.ceil(self.word_size / self.write_size))
else:
self.num_wmasks = 0
if output_path is None:
self.output_path = OPTS.openram_temp
else:
self.output_path = output_path
self.set_load_slew(0, 0)
self.set_corner(corner)
self.create_signal_names()
self.add_graph_exclusions()
self.meas_id = 0
def create_measurement_objects(self):
""" Create the measurements used for read and write ports """
self.read_meas_lists = self.create_read_port_measurement_objects()
self.write_meas_lists = self.create_write_port_measurement_objects()
self.check_meas_names(self.read_meas_lists + self.write_meas_lists)
def check_meas_names(self, measures_lists):
"""
Given measurements (in 2d list), checks that their names are unique.
Spice sim will fail otherwise.
"""
name_set = set()
for meas_list in measures_lists:
for meas in meas_list:
name = meas.name.lower()
debug.check(name not in name_set, ("SPICE measurements must have unique names. "
"Duplicate name={0}").format(name))
name_set.add(name)
def create_read_port_measurement_objects(self):
"""Create the measurements used for read ports: delays, slews, powers"""
self.read_lib_meas = []
self.clk_frmt = "clk{0}" # Unformatted clock name
targ_name = "{0}{{}}_{1}".format(self.dout_name, self.probe_data) # Empty values are the port and probe data bit
self.delay_meas = []
self.delay_meas.append(delay_measure("delay_lh", self.clk_frmt, targ_name, "FALL", "RISE", measure_scale=1e9))
self.delay_meas[-1].meta_str = sram_op.READ_ONE # Used to index time delay values when measurements written to spice file.
self.delay_meas[-1].meta_add_delay = False
self.delay_meas.append(delay_measure("delay_hl", self.clk_frmt, targ_name, "FALL", "FALL", measure_scale=1e9))
self.delay_meas[-1].meta_str = sram_op.READ_ZERO
self.delay_meas[-1].meta_add_delay = False
self.read_lib_meas+=self.delay_meas
self.slew_meas = []
self.slew_meas.append(slew_measure("slew_lh", targ_name, "RISE", measure_scale=1e9))
self.slew_meas[-1].meta_str = sram_op.READ_ONE
self.slew_meas.append(slew_measure("slew_hl", targ_name, "FALL", measure_scale=1e9))
self.slew_meas[-1].meta_str = sram_op.READ_ZERO
self.read_lib_meas+=self.slew_meas
self.read_lib_meas.append(power_measure("read1_power", "RISE", measure_scale=1e3))
self.read_lib_meas[-1].meta_str = sram_op.READ_ONE
self.read_lib_meas.append(power_measure("read0_power", "FALL", measure_scale=1e3))
self.read_lib_meas[-1].meta_str = sram_op.READ_ZERO
self.read_lib_meas.append(power_measure("disabled_read1_power", "RISE", measure_scale=1e3))
self.read_lib_meas[-1].meta_str = "disabled_read1"
self.read_lib_meas.append(power_measure("disabled_read0_power", "FALL", measure_scale=1e3))
self.read_lib_meas[-1].meta_str = "disabled_read0"
# This will later add a half-period to the spice time delay. Only for reading 0.
# FIXME: Removed this to check, see if it affects anything
#for obj in self.read_lib_meas:
# if obj.meta_str is sram_op.READ_ZERO:
# obj.meta_add_delay = True
read_measures = []
read_measures.append(self.read_lib_meas)
# Other measurements associated with the read port not included in the liberty file
read_measures.append(self.create_bitline_measurement_objects())
read_measures.append(self.create_debug_measurement_objects())
read_measures.append(self.create_read_bit_measures())
# TODO: Maybe don't do this here (?)
if OPTS.top_process != "memchar":
read_measures.append(self.create_sen_and_bitline_path_measures())
return read_measures
def create_bitline_measurement_objects(self):
"""
Create the measurements used for bitline delay values. Due to
unique error checking, these are separated from other measurements.
These measurements are only associated with read values.
"""
self.bitline_volt_meas = []
self.bitline_volt_meas.append(voltage_at_measure("v_bl_READ_ZERO",
self.bl_name))
self.bitline_volt_meas[-1].meta_str = sram_op.READ_ZERO
self.bitline_volt_meas.append(voltage_at_measure("v_br_READ_ZERO",
self.br_name))
self.bitline_volt_meas[-1].meta_str = sram_op.READ_ZERO
self.bitline_volt_meas.append(voltage_at_measure("v_bl_READ_ONE",
self.bl_name))
self.bitline_volt_meas[-1].meta_str = sram_op.READ_ONE
self.bitline_volt_meas.append(voltage_at_measure("v_br_READ_ONE",
self.br_name))
self.bitline_volt_meas[-1].meta_str = sram_op.READ_ONE
return self.bitline_volt_meas
def create_write_port_measurement_objects(self):
"""Create the measurements used for read ports: delays, slews, powers"""
self.write_lib_meas = []
self.write_lib_meas.append(power_measure("write1_power", "RISE", measure_scale=1e3))
self.write_lib_meas[-1].meta_str = sram_op.WRITE_ONE
self.write_lib_meas.append(power_measure("write0_power", "FALL", measure_scale=1e3))
self.write_lib_meas[-1].meta_str = sram_op.WRITE_ZERO
self.write_lib_meas.append(power_measure("disabled_write1_power", "RISE", measure_scale=1e3))
self.write_lib_meas[-1].meta_str = "disabled_write1"
self.write_lib_meas.append(power_measure("disabled_write0_power", "FALL", measure_scale=1e3))
self.write_lib_meas[-1].meta_str = "disabled_write0"
write_measures = []
write_measures.append(self.write_lib_meas)
write_measures.append(self.create_write_bit_measures())
return write_measures
def create_debug_measurement_objects(self):
"""Create debug measurement to help identify failures."""
self.dout_volt_meas = []
for meas in self.delay_meas:
# Output voltage measures
self.dout_volt_meas.append(voltage_at_measure("v_{0}".format(meas.name),
meas.targ_name_no_port))
self.dout_volt_meas[-1].meta_str = meas.meta_str
if OPTS.use_pex and OPTS.pex_exe[0] != 'calibre':
self.sen_meas = delay_measure("delay_sen", self.clk_frmt, self.sen_name, "FALL", "RISE", measure_scale=1e9)
else:
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
return self.dout_volt_meas + [self.sen_meas]
def create_read_bit_measures(self):
""" Adds bit measurements for read0 and read1 cycles """
self.read_bit_meas = {bit_polarity.NONINVERTING: [], bit_polarity.INVERTING: []}
meas_cycles = (sram_op.READ_ZERO, sram_op.READ_ONE)
for cycle in meas_cycles:
meas_tag = "a{0}_b{1}_{2}".format(self.probe_address, self.probe_data, cycle.name)
single_bit_meas = self.get_bit_measures(meas_tag, self.probe_address, self.probe_data)
for polarity, meas in single_bit_meas.items():
meas.meta_str = cycle
self.read_bit_meas[polarity].append(meas)
# Dictionary values are lists, reduce to a single list of measurements
return [meas for meas_list in self.read_bit_meas.values() for meas in meas_list]
def create_write_bit_measures(self):
""" Adds bit measurements for write0 and write1 cycles """
self.write_bit_meas = {bit_polarity.NONINVERTING: [], bit_polarity.INVERTING: []}
meas_cycles = (sram_op.WRITE_ZERO, sram_op.WRITE_ONE)
for cycle in meas_cycles:
meas_tag = "a{0}_b{1}_{2}".format(self.probe_address, self.probe_data, cycle.name)
single_bit_meas = self.get_bit_measures(meas_tag, self.probe_address, self.probe_data)
for polarity, meas in single_bit_meas.items():
meas.meta_str = cycle
self.write_bit_meas[polarity].append(meas)
# Dictionary values are lists, reduce to a single list of measurements
return [meas for meas_list in self.write_bit_meas.values() for meas in meas_list]
def get_bit_measures(self, meas_tag, probe_address, probe_data):
"""
Creates measurements for the q/qbar of input bit position.
meas_tag is a unique identifier for the measurement.
"""
bit_col = self.get_data_bit_column_number(probe_address, probe_data)
bit_row = self.get_address_row_number(probe_address)
if OPTS.top_process == "memchar":
cell_name = self.cell_name.format(bit_row, bit_col)
storage_names = ("Q", "Q_bar")
else:
(cell_name, cell_inst) = self.sram.get_cell_name(self.sram.name, bit_row, bit_col)
storage_names = cell_inst.mod.get_storage_net_names()
debug.check(len(storage_names) == 2, ("Only inverting/non-inverting storage nodes"
"supported for characterization. Storage nets={0}").format(storage_names))
if OPTS.use_pex and OPTS.pex_exe[0] != "calibre":
bank_num = self.sram.get_bank_num(self.sram.name, bit_row, bit_col)
q_name = "bitcell_Q_b{0}_r{1}_c{2}".format(bank_num, bit_row, bit_col)
qbar_name = "bitcell_Q_bar_b{0}_r{1}_c{2}".format(bank_num, bit_row, bit_col)
else:
q_name = cell_name + OPTS.hier_seperator + str(storage_names[0])
qbar_name = cell_name + OPTS.hier_seperator + str(storage_names[1])
# Bit measures, measurements times to be defined later. The measurement names must be unique
# but they is enforced externally. {} added to names to differentiate between ports allow the
# measurements are independent of the ports
q_meas = voltage_at_measure("v_q_{0}".format(meas_tag), q_name)
qbar_meas = voltage_at_measure("v_qbar_{0}".format(meas_tag), qbar_name)
return {bit_polarity.NONINVERTING: q_meas, bit_polarity.INVERTING: qbar_meas}
def create_sen_and_bitline_path_measures(self):
"""Create measurements for the s_en and bitline paths for individual delays per stage."""
# # FIXME: There should be a default_read_port variable in this case, pathing is done with this
# # but is never mentioned otherwise
port = self.read_ports[0]
sen_and_port = self.sen_name + str(port)
bl_and_port = self.bl_name.format(port) # bl_name contains a '{}' for the port
# Isolate the s_en and bitline paths
debug.info(1, "self.bl_name = {0}".format(self.bl_name))
debug.info(2, "self.graph.all_paths = {0}".format(self.graph.all_paths))
sen_paths = [path for path in self.graph.all_paths if sen_and_port in path]
bl_paths = [path for path in self.graph.all_paths if bl_and_port in path]
debug.check(len(sen_paths)==1, 'Found {0} paths which contain the s_en net.'.format(len(sen_paths)))
debug.check(len(bl_paths)==1, 'Found {0} paths which contain the bitline net.'.format(len(bl_paths)))
sen_path = sen_paths[0]
bitline_path = bl_paths[0]
# Get the measures
self.sen_path_meas = self.create_delay_path_measures(sen_path, "sen")
self.bl_path_meas = self.create_delay_path_measures(bitline_path, "bl")
all_meas = self.sen_path_meas + self.bl_path_meas
# Paths could have duplicate measurements, remove them before they go to the stim file
all_meas = self.remove_duplicate_meas_names(all_meas)
# FIXME: duplicate measurements still exist in the member variables, since they have the same
# name it will still work, but this could cause an issue in the future.
return all_meas
def remove_duplicate_meas_names(self, measures):
"""Returns new list of measurements without duplicate names"""
name_set = set()
unique_measures = []
for meas in measures:
if meas.name not in name_set:
name_set.add(meas.name)
unique_measures.append(meas)
return unique_measures
def create_delay_path_measures(self, path, process):
"""Creates measurements for each net along given path."""
# Determine the directions (RISE/FALL) of signals
path_dirs = self.get_meas_directions(path)
# Create the measurements
path_meas = []
for i in range(len(path) - 1):
cur_net, next_net = path[i], path[i + 1]
cur_dir, next_dir = path_dirs[i], path_dirs[i + 1]
meas_name = "delay_{0}_to_{1}".format(cur_net, next_net)
meas_name += "_" + process + "_id" + str(self.meas_id)
self.meas_id += 1
if i + 1 != len(path) - 1:
path_meas.append(delay_measure(meas_name, cur_net, next_net, cur_dir, next_dir, measure_scale=1e9, has_port=False))
else: # Make the last measurement always measure on FALL because is a read 0
path_meas.append(delay_measure(meas_name, cur_net, next_net, cur_dir, "FALL", measure_scale=1e9, has_port=False))
# Some bitcell logic is hardcoded for only read zeroes, force that here as well.
path_meas[-1].meta_str = sram_op.READ_ZERO
path_meas[-1].meta_add_delay = True
return path_meas
def get_meas_directions(self, path):
"""Returns SPICE measurements directions based on path."""
# Get the edges modules which define the path
edge_mods = self.graph.get_edge_mods(path)
# Convert to booleans based on function of modules (inverting/non-inverting)
mod_type_bools = [mod.is_non_inverting() for mod in edge_mods]
# FIXME: obtuse hack to differentiate s_en input from bitline in sense amps
if self.sen_name in path:
# Force the sense amp to be inverting for s_en->DOUT.
# bitline->DOUT is non-inverting, but the module cannot differentiate inputs.
s_en_index = path.index(self.sen_name)
mod_type_bools[s_en_index] = False
debug.info(2, 'Forcing sen->dout to be inverting.')
# Use these to determine direction list assuming delay start on neg. edge of clock (FALL)
# Also, use shorthand that 'FALL' == False, 'RISE' == True to simplify logic
bool_dirs = [False]
cur_dir = False # All Paths start on FALL edge of clock
for mod_bool in mod_type_bools:
cur_dir = (cur_dir == mod_bool)
bool_dirs.append(cur_dir)
# Convert from boolean to string
return ['RISE' if dbool else 'FALL' for dbool in bool_dirs]
def set_load_slew(self, load, slew):
""" Set the load and slew """
self.load = load
self.slew = slew
def check_arguments(self):
"""Checks if arguments given for write_stimulus() meets requirements"""
try:
int(self.probe_address, 2)
except ValueError:
debug.error("Probe Address is not of binary form: {0}".format(self.probe_address), 1)
if len(self.probe_address) != self.bank_addr_size:
debug.error("Probe Address's number of bits does not correspond to given SRAM", 1)
if not isinstance(self.probe_data, int) or self.probe_data>self.word_size or self.probe_data<0:
debug.error("Given probe_data is not an integer to specify a data bit", 1)
# Adding port options here which the characterizer cannot handle. Some may be added later like ROM
if len(self.read_ports) == 0:
debug.error("Characterizer does not currently support SRAMs without read ports.", 1)
if len(self.write_ports) == 0:
debug.error("Characterizer does not currently support SRAMs without write ports.", 1)
def write_generic_stimulus(self):
""" Create the instance, supplies, loads, and access transistors. """
# add vdd/gnd statements
self.sf.write("\n* Global Power Supplies\n")
self.stim.write_supply()
# instantiate the sram
self.sf.write("\n* Instantiation of the SRAM\n")
self.stim.inst_model(pins=self.pins,
model_name=self.sram.name)
self.sf.write("\n* SRAM output loads\n")
for port in self.read_ports:
for i in range(self.word_size + self.num_spare_cols):
self.sf.write("CD{0}{1} {2}{0}_{1} 0 {3}f\n".format(port, i, self.dout_name, self.load))
def write_delay_stimulus(self):
"""
Creates a stimulus file for simulations to probe a bitcell at a given clock period.
Address and bit were previously set with set_probe().
Input slew (in ns) and output capacitive load (in fF) are required for charaterization.
"""
self.check_arguments()
# obtains list of time-points for each rising clk edge
self.create_test_cycles()
# creates and opens stimulus file for writing
self.delay_stim_sp = "delay_stim.sp"
temp_stim = path.join(self.output_path, self.delay_stim_sp)
self.sf = open(temp_stim, "w")
# creates and opens measure file for writing
self.delay_meas_sp = "delay_meas.sp"
temp_meas = path.join(self.output_path, self.delay_meas_sp)
self.mf = open(temp_meas, "w")
if OPTS.spice_name == "spectre":
self.sf.write("simulator lang=spice\n")
self.sf.write("* Delay stimulus for period of {0}n load={1}fF slew={2}ns\n\n".format(self.period,
self.load,
self.slew))
self.stim = stimuli(self.sf, self.mf, self.corner)
# include files in stimulus file
self.stim.write_include(self.trim_sp_file)
self.write_generic_stimulus()
# generate data and addr signals
self.sf.write("\n* Generation of data and address signals\n")
self.gen_data()
self.gen_addr()
# generate control signals
self.sf.write("\n* Generation of control signals\n")
self.gen_control()
self.sf.write("\n* Generation of Port clock signal\n")
for port in self.all_ports:
self.stim.gen_pulse(sig_name="CLK{0}".format(port),
v1=0,
v2=self.vdd_voltage,
offset=self.period,
period=self.period,
t_rise=self.slew,
t_fall=self.slew)
self.sf.write(".include {0}".format(temp_meas))
# self.load_all_measure_nets()
self.write_delay_measures()
# self.write_simulation_saves()
# run until the end of the cycle time
self.stim.write_control(self.cycle_times[-1] + self.period)
self.sf.close()
self.mf.close()
def write_power_stimulus(self, trim):
""" Creates a stimulus file to measure leakage power only.
This works on the *untrimmed netlist*.
"""
self.check_arguments()
# creates and opens stimulus file for writing
self.power_stim_sp = "power_stim.sp"
temp_stim = path.join(self.output_path, self.power_stim_sp)
self.sf = open(temp_stim, "w")
self.sf.write("* Power stimulus for period of {0}n\n\n".format(self.period))
# creates and opens measure file for writing
self.power_meas_sp = "power_meas.sp"
temp_meas = path.join(self.output_path, self.power_meas_sp)
self.mf = open(temp_meas, "w")
self.stim = stimuli(self.sf, self.mf, self.corner)
# include UNTRIMMED files in stimulus file
if trim:
self.stim.write_include(self.trim_sp_file)
else:
self.stim.write_include(self.sim_sp_file)
self.write_generic_stimulus()
# generate data and addr signals
self.sf.write("\n* Generation of data and address signals\n")
for write_port in self.write_ports:
for i in range(self.word_size + self.num_spare_cols):
self.stim.gen_constant(sig_name="{0}{1}_{2} ".format(self.din_name, write_port, i),
v_val=0)
for port in self.all_ports:
for i in range(self.bank_addr_size):
self.stim.gen_constant(sig_name="{0}{1}_{2}".format(self.addr_name, port, i),
v_val=0)
# generate control signals
self.sf.write("\n* Generation of control signals\n")
for port in self.all_ports:
self.stim.gen_constant(sig_name="CSB{0}".format(port), v_val=self.vdd_voltage)
if port in self.readwrite_ports:
self.stim.gen_constant(sig_name="WEB{0}".format(port), v_val=self.vdd_voltage)
self.sf.write("\n* Generation of global clock signal\n")
for port in self.all_ports:
self.stim.gen_constant(sig_name="CLK{0}".format(port), v_val=0)
self.sf.write(".include {}".format(temp_meas))
self.write_power_measures()
# run until the end of the cycle time
self.stim.write_control(2 * self.period)
self.sf.close()
self.mf.close()
def get_measure_variants(self, port, measure_obj, measure_type=None):
"""
Checks the measurement object and calls respective function for
related measurement inputs.
"""
meas_type = type(measure_obj)
if meas_type is delay_measure or meas_type is slew_measure:
variant_tuple = self.get_delay_measure_variants(port, measure_obj)
elif meas_type is power_measure:
variant_tuple = self.get_power_measure_variants(port, measure_obj, measure_type)
elif meas_type is voltage_when_measure:
variant_tuple = self.get_volt_when_measure_variants(port, measure_obj)
elif meas_type is voltage_at_measure:
variant_tuple = self.get_volt_at_measure_variants(port, measure_obj)
else:
debug.error("Input function not defined for measurement type={0}".format(meas_type))
# Removes port input from any object which does not use it. This shorthand only works if
# the measurement has port as the last input. Could be implemented by measurement type or
# remove entirely from measurement classes.
if not measure_obj.has_port:
variant_tuple = variant_tuple[:-1]
return variant_tuple
def get_delay_measure_variants(self, port, delay_obj):
"""
Get the measurement values that can either vary from simulation to
simulation (vdd, address) or port to port (time delays)
"""
# Return value is intended to match the delay measure format: trig_td, targ_td, vdd, port
# vdd is arguably constant as that is true for a single lib file.
if delay_obj.meta_str == sram_op.READ_ZERO:
# Falling delay are measured starting from neg. clk edge. Delay adjusted to that.
meas_cycle_delay = self.cycle_times[self.measure_cycles[port][delay_obj.meta_str]]
elif delay_obj.meta_str == sram_op.READ_ONE:
meas_cycle_delay = self.cycle_times[self.measure_cycles[port][delay_obj.meta_str]]
else:
debug.error("Unrecognized delay Index={0}".format(delay_obj.meta_str), 1)
# These measurements have there time further delayed to the neg. edge of the clock.
if delay_obj.meta_add_delay:
meas_cycle_delay += self.period / 2
return (meas_cycle_delay, meas_cycle_delay, self.vdd_voltage, port)
def get_power_measure_variants(self, port, power_obj, operation):
"""Get the measurement values that can either vary port to port (time delays)"""
# Return value is intended to match the power measure format: t_initial, t_final, port
t_initial = self.cycle_times[self.measure_cycles[port][power_obj.meta_str]]
t_final = self.cycle_times[self.measure_cycles[port][power_obj.meta_str] + 1]
return (t_initial, t_final, port)
def get_volt_at_measure_variants(self, port, volt_meas):
"""
Get the measurement values that can either vary port to port (time delays)
"""
meas_cycle = self.cycle_times[self.measure_cycles[port][volt_meas.meta_str]]
# Measurement occurs slightly into the next period so we know that the value
# "stuck" after the end of the period -> current period start + 1.25*period
at_time = meas_cycle + 1.25 * self.period
return (at_time, port)
def get_volt_when_measure_variants(self, port, volt_meas):
"""
Get the measurement values that can either vary port to port (time delays)
"""
# Only checking 0 value reads for now.
t_trig = self.cycle_times[self.measure_cycles[port][sram_op.READ_ZERO]]
return (t_trig, self.vdd_voltage, port)
def write_delay_measures_read_port(self, port):
"""
Write the measure statements to quantify the delay and power results for a read port.
"""
# add measure statements for delays/slews
for meas_list in self.read_meas_lists:
for measure in meas_list:
measure_variant_inp_tuple = self.get_measure_variants(port, measure, "read")
measure.write_measure(self.stim, measure_variant_inp_tuple)
def write_delay_measures_write_port(self, port):
"""
Write the measure statements to quantify the power results for a write port.
"""
# add measure statements for power
for meas_list in self.write_meas_lists:
for measure in meas_list:
measure_variant_inp_tuple = self.get_measure_variants(port, measure, "write")
measure.write_measure(self.stim, measure_variant_inp_tuple)
def write_delay_measures(self):
"""
Write the measure statements to quantify the delay and power results for all targeted ports.
"""
self.sf.write("\n* Measure statements for delay and power\n")
# Output some comments to aid where cycles start and
# what is happening
for comment in self.cycle_comments:
self.mf.write("* {0}\n".format(comment))
self.sf.write("\n")
for read_port in self.targ_read_ports:
self.mf.write("* Read ports {0}\n".format(read_port))
self.write_delay_measures_read_port(read_port)
for write_port in self.targ_write_ports:
self.mf.write("* Write ports {0}\n".format(write_port))
self.write_delay_measures_write_port(write_port)
def load_pex_net(self, net: str):
from subprocess import check_output, CalledProcessError
prefix = (self.sram_instance_name + OPTS.hier_seperator).lower()
if not net.lower().startswith(prefix) or not OPTS.use_pex or not OPTS.calibre_pex:
return net
original_net = net
net = net[len(prefix):]
net = net.replace(".", "_").replace("[", r"\[").replace("]", r"\]")
for pattern in [r"\sN_{}_[MXmx]\S+_[gsd]".format(net), net]:
try:
match = check_output(["grep", "-m1", "-o", "-iE", pattern, self.sp_file])
return prefix + match.decode().strip()
except CalledProcessError:
pass
return original_net
def load_all_measure_nets(self):
measurement_nets = set()
for port, meas in zip(self.targ_read_ports * len(self.read_meas_lists)
+ self.targ_write_ports * len(self.write_meas_lists),
self.read_meas_lists + self.write_meas_lists):
for measurement in meas:
visited = getattr(measurement, 'pex_visited', False)
for prop in ["trig_name_no_port", "targ_name_no_port"]:
if hasattr(measurement, prop):
net = getattr(measurement, prop).format(port)
if not visited:
net = self.load_pex_net(net)
setattr(measurement, prop, net)
measurement_nets.add(net)
measurement.pex_visited = True
self.measurement_nets = measurement_nets
return measurement_nets
def write_simulation_saves(self):
for net in self.measurement_nets:
self.sf.write(".plot V({0}) \n".format(net))
probe_nets = set()
sram_name = self.sram_instance_name
col = self.bitline_column
row = self.wordline_row
for port in set(self.targ_read_ports + self.targ_write_ports):
probe_nets.add("WEB{0}".format(port))
probe_nets.add("{0}{2}w_en{1}".format(self.sram_instance_name, port, OPTS.hier_seperator))
probe_nets.add("{0}{3}Xbank0{3}Xport_data{1}{3}Xwrite_driver_array{1}{3}Xwrite_driver{2}{3}en_bar".format(self.sram_instance_name,
port,
self.bitline_column,
OPTS.hier_seperator))
probe_nets.add("{0}{3}Xbank0{3}br_{1}_{2}".format(self.sram_instance_name,
port,
self.bitline_column,
OPTS.hier_seperator))
if not OPTS.use_pex:
continue
probe_nets.add(
"{0}{3}vdd_Xbank0_Xbitcell_array_xbitcell_array_xbit_r{1}_c{2}".format(sram_name,
row,
col - 1,
OPTS.hier_seperator))
probe_nets.add(
"{0}{3}p_en_bar{1}_Xbank0_Xport_data{1}_Xprecharge_array{1}_Xpre_column_{2}".format(sram_name,
port,
col,
OPTS.hier_seperator))
probe_nets.add(
"{0}{3}vdd_Xbank0_Xport_data{1}_Xprecharge_array{1}_xpre_column_{2}".format(sram_name,
port,
col,
OPTS.hier_seperator))
probe_nets.add("{0}{3}vdd_Xbank0_Xport_data{1}_Xwrite_driver_array{1}_xwrite_driver{2}".format(sram_name,
port,
col,
OPTS.hier_seperator))
probe_nets.update(self.measurement_nets)
for net in probe_nets:
debug.info(2, "Probe: {0}".format(net))
self.sf.write(".plot V({0}) \n".format(self.load_pex_net(net)))
def write_power_measures(self):
"""
Write the measure statements to quantify the leakage power only.
"""
self.sf.write("\n* Measure statements for idle leakage power\n")
# add measure statements for power
# TODO: Convert to measure statement insted of using stimuli
# measure = power_measure('leakage_power',
t_initial = self.period
t_final = 2 * self.period
self.stim.gen_meas_power(meas_name="leakage_power",
t_initial=t_initial,
t_final=t_final)
def find_feasible_period_one_port(self, port):
"""
Uses an initial period and finds a feasible period before we
run the binary search algorithm to find min period. We check if
the given clock period is valid and if it's not, we continue to
double the period until we find a valid period to use as a
starting point.
"""
debug.check(port in self.read_ports, "Characterizer requires a read port to determine a period.")
feasible_period = float(tech.spice["feasible_period"])
time_out = 9
while True:
time_out -= 1
if (time_out <= 0):
debug.error("Timed out, could not find a feasible period.", 2)
# Write ports are assumed non-critical to timing, so the first available is used
self.targ_write_ports = [self.write_ports[0]]
# Set target read port for simulation
self.targ_read_ports = [port]
debug.info(1, "Trying feasible period: {0}ns on Port {1}".format(feasible_period, port))
self.period = feasible_period
(success, results)=self.run_delay_simulation()
# Clear these target ports after simulation
self.targ_write_ports = []
self.targ_read_ports = []
if not success:
feasible_period = 2 * feasible_period
continue
# Positions of measurements currently hardcoded. First 2 are delays, next 2 are slews
feasible_delays = [results[port][mname] for mname in self.delay_meas_names if "delay" in mname]
feasible_slews = [results[port][mname] for mname in self.delay_meas_names if "slew" in mname]
delay_str = "feasible_delay {0:.4f}ns/{1:.4f}ns".format(*feasible_delays)
slew_str = "slew {0:.4f}ns/{1:.4f}ns".format(*feasible_slews)
debug.info(2, "feasible_period passed for Port {3}: {0}ns {1} {2} ".format(feasible_period,
delay_str,
slew_str,
port))
if success:
debug.info(2, "Found feasible_period for port {0}: {1}ns".format(port, feasible_period))
self.period = feasible_period
# Only return results related to input port.
return results[port]
def find_feasible_period(self):
"""
Loops through all read ports determining the feasible period and collecting
delay information from each port.
"""
feasible_delays = [{} for i in self.all_ports]
# Get initial feasible delays from first port
feasible_delays[self.read_ports[0]] = self.find_feasible_period_one_port(self.read_ports[0])
previous_period = self.period
# Loops through all the ports checks if the feasible period works. Everything restarts it if does not.
# Write ports do not produce delays which is why they are not included here.
i = 1
while i < len(self.read_ports):
port = self.read_ports[i]
# Only extract port values from the specified port, not the entire results.
feasible_delays[port].update(self.find_feasible_period_one_port(port))
# Function sets the period. Restart the entire process if period changes to collect accurate delays
if self.period > previous_period:
i = 0
else:
i+=1
previous_period = self.period
debug.info(1, "Found feasible_period: {0}ns".format(self.period))
return feasible_delays
def run_delay_simulation(self):
"""
This tries to simulate a period and checks if the result works. If
so, it returns True and the delays, slews, and powers. It
works on the trimmed netlist by default, so powers do not
include leakage of all cells.
"""
debug.check(self.period > 0, "Target simulation period non-positive")
self.write_delay_stimulus()
self.stim.run_sim(self.delay_stim_sp)
return self.check_measurements()
def check_measurements(self):
""" Check the write and read measurements """
# Loop through all targeted ports and collect delays and powers.
result = [{} for i in self.all_ports]
for port in self.targ_write_ports:
if not self.check_bit_measures(self.write_bit_meas, port):
return (False, {})
debug.info(2, "Checking write values for port {0}".format(port))
write_port_dict = {}
for measure in self.write_lib_meas:
write_port_dict[measure.name] = measure.retrieve_measure(port=port)
if not check_dict_values_is_float(write_port_dict):
debug.error("Failed to Measure Write Port Values:\n\t\t{0}".format(write_port_dict), 1)
result[port].update(write_port_dict)
for port in self.targ_read_ports:
# First, check that the memory has the right values at the right times
if not self.check_bit_measures(self.read_bit_meas, port):
return (False, {})
debug.info(2, "Checking read delay values for port {0}".format(port))
# Check sen timing, then bitlines, then general measurements.
if not self.check_sen_measure(port):
return (False, {})
if not self.check_read_debug_measures(port):
return (False, {})
# Check timing for read ports. Power is only checked if it was read correctly
read_port_dict = {}
for measure in self.read_lib_meas:
read_port_dict[measure.name] = measure.retrieve_measure(port=port)
if not self.check_valid_delays(read_port_dict):
return (False, {})
if not check_dict_values_is_float(read_port_dict):
debug.error("Failed to Measure Read Port Values:\n\t\t{0}".format(read_port_dict), 1)
result[port].update(read_port_dict)
if self.sen_path_meas and self.bl_path_meas:
self.path_delays = self.check_path_measures()
return (True, result)
def check_sen_measure(self, port):
"""Checks that the sen occurred within a half-period"""
sen_val = self.sen_meas.retrieve_measure(port=port)
debug.info(2, "s_en delay={0}ns".format(sen_val))
if self.sen_meas.meta_add_delay:
max_delay = self.period / 2
else:
max_delay = self.period
return not (type(sen_val) != float or sen_val > max_delay)
def check_read_debug_measures(self, port):
"""Debug measures that indicate special conditions."""
# Currently, only check if the opposite than intended value was read during
# the read cycles i.e. neither of these measurements should pass.
# FIXME: these checks need to be re-done to be more robust against possible errors
bl_vals = {}
br_vals = {}
for meas in self.bitline_volt_meas:
val = meas.retrieve_measure(port=port)
if self.bl_name == meas.targ_name_no_port:
bl_vals[meas.meta_str] = val
elif self.br_name == meas.targ_name_no_port:
br_vals[meas.meta_str] = val
debug.info(2, "{0}={1}".format(meas.name, val))
dout_success = True
bl_success = False
for meas in self.dout_volt_meas:
val = meas.retrieve_measure(port=port)
debug.info(2, "{0}={1}".format(meas.name, val))
debug.check(type(val)==float, "Error retrieving numeric measurement: {0} {1}".format(meas.name, val))
if meas.meta_str == sram_op.READ_ONE and val < self.vdd_voltage * 0.1:
dout_success = False
debug.info(1, "Debug measurement failed. Value {0}V was read on read 1 cycle.".format(val))
bl_success = self.check_bitline_meas(bl_vals[sram_op.READ_ONE], br_vals[sram_op.READ_ONE])
elif meas.meta_str == sram_op.READ_ZERO and val > self.vdd_voltage * 0.9:
dout_success = False
debug.info(1, "Debug measurement failed. Value {0}V was read on read 0 cycle.".format(val))
bl_success = self.check_bitline_meas(br_vals[sram_op.READ_ONE], bl_vals[sram_op.READ_ONE])
# If the bitlines have a correct value while the output does not then that is a
# sen error. FIXME: there are other checks that can be done to solidfy this conclusion.
if not dout_success and bl_success:
debug.error("Sense amp enable timing error. Increase the delay chain through the configuration file.", 1)
return dout_success
def check_bit_measures(self, bit_measures, port):
"""
Checks the measurements which represent the internal storage voltages
at the end of the read cycle.
"""
success = False
for polarity, meas_list in bit_measures.items():
for meas in meas_list:
val = meas.retrieve_measure(port=port)
debug.info(2, "{0}={1}".format(meas.name, val))
if type(val) != float:
continue
meas_cycle = meas.meta_str
# Loose error conditions. Assume it's not metastable but account for noise during reads.
if (meas_cycle == sram_op.READ_ZERO and polarity == bit_polarity.NONINVERTING) or\
(meas_cycle == sram_op.READ_ONE and polarity == bit_polarity.INVERTING):
success = val < self.vdd_voltage / 2
elif (meas_cycle == sram_op.READ_ZERO and polarity == bit_polarity.INVERTING) or\
(meas_cycle == sram_op.READ_ONE and polarity == bit_polarity.NONINVERTING):
success = val > self.vdd_voltage / 2
elif (meas_cycle == sram_op.WRITE_ZERO and polarity == bit_polarity.INVERTING) or\
(meas_cycle == sram_op.WRITE_ONE and polarity == bit_polarity.NONINVERTING):
success = val > self.vdd_voltage / 2
elif (meas_cycle == sram_op.WRITE_ONE and polarity == bit_polarity.INVERTING) or\
(meas_cycle == sram_op.WRITE_ZERO and polarity == bit_polarity.NONINVERTING):
success = val < self.vdd_voltage / 2
if not success:
debug.info(1, ("Wrong value detected on probe bit during read/write cycle. "
"Check writes and control logic for bugs.\n measure={0}, op={1}, "
"bit_storage={2}, V(bit)={3}").format(meas.name, meas_cycle.name, polarity.name, val))
return success
def check_bitline_meas(self, v_discharged_bl, v_charged_bl):
"""
Checks the value of the discharging bitline. Confirms s_en timing errors.
Returns true if the bitlines are at there their value.
"""
# The inputs looks at discharge/charged bitline rather than left or right (bl/br)
# Performs two checks, discharging bitline is at least 10% away from vdd and there is a
# 10% vdd difference between the bitlines. Both need to fail to be considered a s_en error.
min_dicharge = v_discharged_bl < self.vdd_voltage * 0.9
min_diff = (v_charged_bl - v_discharged_bl) > self.vdd_voltage * 0.1
debug.info(1, "min_dicharge={0}, min_diff={1}".format(min_dicharge, min_diff))
return (min_dicharge and min_diff)
def check_path_measures(self):
"""Get and check all the delays along the sen and bitline paths"""
# Get and set measurement, no error checking done other than prints.
debug.info(2, "Checking measures in Delay Path")
value_dict = {}
for meas in self.sen_path_meas + self.bl_path_meas:
val = meas.retrieve_measure()
debug.info(2, '{0}={1}'.format(meas.name, val))
if type(val) != float or val > self.period / 2:
debug.info(1, 'Failed measurement:{}={}'.format(meas.name, val))
value_dict[meas.name] = val
# debug.info(0, "value_dict={}".format(value_dict))
return value_dict
def run_power_simulation(self):
"""
This simulates a disabled SRAM to get the leakage power when it is off.
"""
debug.info(1, "Performing leakage power simulations.")
self.write_power_stimulus(trim=False)
self.stim.run_sim(self.power_stim_sp)
leakage_power=parse_spice_list("timing", "leakage_power")
debug.check(leakage_power!="Failed", "Could not measure leakage power.")
debug.info(1, "Leakage power of full array is {0} mW".format(leakage_power * 1e3))
# debug
# sys.exit(1)
self.write_power_stimulus(trim=True)
self.stim.run_sim(self.power_stim_sp)
trim_leakage_power=parse_spice_list("timing", "leakage_power")
debug.check(trim_leakage_power!="Failed", "Could not measure leakage power.")
debug.info(1, "Leakage power of trimmed array is {0} mW".format(trim_leakage_power * 1e3))
# For debug, you sometimes want to inspect each simulation.
# key=raw_input("press return to continue")
return (leakage_power * 1e3, trim_leakage_power * 1e3)
def check_valid_delays(self, result_dict):
""" Check if the measurements are defined and if they are valid. """
# Hard coded names currently
delay_hl = result_dict["delay_hl"]
delay_lh = result_dict["delay_lh"]
slew_hl = result_dict["slew_hl"]
slew_lh = result_dict["slew_lh"]
period_load_slew_str = "period {0} load {1} slew {2}".format(self.period, self.load, self.slew)
# if it failed or the read was longer than a period
if type(delay_hl)!=float or type(delay_lh)!=float or type(slew_lh)!=float or type(slew_hl)!=float:
delays_str = "delay_hl={0} delay_lh={1}".format(delay_hl, delay_lh)
slews_str = "slew_hl={0} slew_lh={1}".format(slew_hl, slew_lh)
debug.info(2, "Failed simulation (in sec):\n\t\t{0}\n\t\t{1}\n\t\t{2}".format(period_load_slew_str,
delays_str,
slews_str))
return False
delays_str = "delay_hl={0} delay_lh={1}".format(delay_hl, delay_lh)
slews_str = "slew_hl={0} slew_lh={1}".format(slew_hl, slew_lh)
# high-to-low delays start at neg. clk edge, so they need to be less than half_period
half_period = self.period / 2
if abs(delay_hl)>half_period or abs(delay_lh)>half_period or abs(slew_hl)>half_period or abs(slew_lh)>self.period \
or (delay_hl<0 and delay_lh<0) or slew_hl<0 or slew_lh<0:
debug.info(2, "UNsuccessful simulation (in ns):\n\t\t{0}\n\t\t{1}\n\t\t{2}".format(period_load_slew_str,
delays_str,
slews_str))
return False
else:
debug.info(2, "Successful simulation (in ns):\n\t\t{0}\n\t\t{1}\n\t\t{2}".format(period_load_slew_str,
delays_str,
slews_str))
if delay_lh < 0 and delay_hl > 0:
result_dict["delay_lh"] = result_dict["delay_hl"]
debug.info(2, "delay_lh captured precharge, using delay_hl instead")
elif delay_hl < 0 and delay_lh > 0:
result_dict["delay_hl"] = result_dict["delay_lh"]
debug.info(2, "delay_hl captured precharge, using delay_lh instead")
return True
def find_min_period(self, feasible_delays):
"""
Determine a single minimum period for all ports.
"""
feasible_period = ub_period = self.period
lb_period = 0.0
target_period = 0.5 * (ub_period + lb_period)
# Find the minimum period for all ports. Start at one port and perform binary search then use that delay as a starting position.
# For testing purposes, only checks read ports.
for port in self.read_ports:
target_period = self.find_min_period_one_port(feasible_delays, port, lb_period, ub_period, target_period)
# The min period of one port becomes the new lower bound. Reset the upper_bound.
lb_period = target_period
ub_period = feasible_period
# Clear the target ports before leaving
self.targ_read_ports = []
self.targ_write_ports = []
return target_period
def find_min_period_one_port(self, feasible_delays, port, lb_period, ub_period, target_period):
"""
Searches for the smallest period with output delays being within 5% of
long period. For the current logic to characterize multiport, bounds are required as an input.
"""
# previous_period = ub_period = self.period
# ub_period = self.period
# lb_period = 0.0
# target_period = 0.5 * (ub_period + lb_period)
# Binary search algorithm to find the min period (max frequency) of input port
time_out = 25
# Write ports are assumed non-critical to timing, so the first available is used
self.targ_write_ports = [self.write_ports[0]]
self.targ_read_ports = [port]
while True:
time_out -= 1
if (time_out <= 0):
debug.error("Timed out, could not converge on minimum period.", 2)
self.period = target_period
debug.info(1, "MinPeriod Search Port {3}: {0}ns (ub: {1} lb: {2})".format(target_period,
ub_period,
lb_period,
port))
if self.try_period(feasible_delays):
ub_period = target_period
else:
lb_period = target_period
if relative_compare(ub_period, lb_period, error_tolerance=0.05):
# ub_period is always feasible.
return ub_period
# Update target
target_period = 0.5 * (ub_period + lb_period)
# key=input("press return to continue")
def try_period(self, feasible_delays):
"""
This tries to simulate a period and checks if the result
works. If it does and the delay is within 5% still, it returns True.
"""
# Run Delay simulation but Power results not used.
(success, results) = self.run_delay_simulation()
if not success:
return False
# Check the values of target readwrite and read ports. Write ports do not produce delays in this current version
for port in self.targ_read_ports:
# check that the delays and slews do not degrade with tested period.
for dname in self.delay_meas_names:
# FIXME: This is a hack solution to fix the min period search. The slew will always be based on the period when there
# is a column mux. Therefore, the checks are skipped for this condition. This is hard to solve without changing the netlist.
# Delays/slews based on the period will cause the min_period search to come to the wrong period.
if self.sram.col_addr_size>0 and "slew" in dname:
continue
if not relative_compare(results[port][dname], feasible_delays[port][dname], error_tolerance=0.05):
debug.info(2, "Delay too big {0} vs {1}".format(results[port][dname], feasible_delays[port][dname]))
return False
# key=raw_input("press return to continue")
delay_str = ', '.join("{0}={1}ns".format(mname, results[port][mname]) for mname in self.delay_meas_names)
debug.info(2, "Successful period {0}, Port {2}, {1}".format(self.period,
delay_str,
port))
return True
def set_probe(self, probe_address, probe_data):
"""
Probe address and data can be set separately to utilize other
functions in this characterizer besides analyze.
Netlist reduced for simulation.
"""
super().set_probe(probe_address, probe_data)
def prepare_netlist(self):
""" Prepare a trimmed netlist and regular netlist. """
# Set up to trim the netlist here if that is enabled
# TODO: Copy old netlist if memchar
if OPTS.trim_netlist:
#self.trim_sp_file = "{0}trimmed.sp".format(self.output_path)
self.trim_sp_file = "{0}trimmed.sp".format(OPTS.openram_temp)
# Only genrate spice when running openram process
if OPTS.top_process != "memchar":
self.sram.sp_write(self.trim_sp_file, lvs=False, trim=True)
else:
# The non-reduced netlist file when it is disabled
self.trim_sp_file = "{0}sram.sp".format(self.output_path)
# The non-reduced netlist file for power simulation
self.sim_sp_file = "{0}sram.sp".format(self.output_path)
# Make a copy in temp for debugging
if self.sp_file != self.sim_sp_file:
shutil.copy(self.sp_file, self.sim_sp_file)
def recover_measurment_objects(self):
mf_path = path.join(OPTS.output_path, "delay_meas.sp")
self.sen_path_meas = None
self.bl_path_meas = None
if not path.exists(mf_path):
debug.info(1, "Delay measure file not found. Skipping measure recovery")
return
mf = open(mf_path, "r")
measure_text = mf.read()
port_iter = re.finditer(r"\* (Read|Write) ports (\d*)", measure_text)
port_measure_lines = []
loc = 0
port_name = ''
for port in port_iter:
port_measure_lines.append((port_name, measure_text[loc:port.end(0)]))
loc = port.start(0)
port_name = port.group(1) + port.group(2)
mf.close()
# Cycle comments, not sure if i need this
# cycle_lines = port_measure_lines.pop(0)[1]
# For now just recover the bit_measures and sen_and_bitline_path_measures
self.read_meas_lists.append([])
self.read_bit_meas = {bit_polarity.NONINVERTING: [], bit_polarity.INVERTING: []}
self.write_bit_meas = {bit_polarity.NONINVERTING: [], bit_polarity.INVERTING: []}
# bit_measure_rule = re.compile(r"\.meas tran (v_q_a\d+_b\d+_(read|write)_(zero|one)\d+) FIND v\((.*)\) AT=(\d+(\.\d+)?)n")
# for measures in port_measure_lines:
# port_name = measures[0]
# text = measures[1]
# bit_measure_iter = bit_measure_rule.finditer(text)
# for bit_measure in bit_measure_iter:
# meas_name = bit_measure.group(1)
# read = bit_measure.group(2) == "read"
# cycle = bit_measure.group(3)
# probe = bit_measure.group(4)
# polarity = bit_polarity.NONINVERTING
# if "q_bar" in meas_name:
# polarity = bit_polarity.INVERTING
# meas = voltage_at_measure(meas_name, probe)
# if read:
# if cycle == "one":
# meas.meta_str = sram_op.READ_ONE
# else:
# meas.meta_str = sram_op.READ_ZERO
# self.read_bit_meas[polarity].append(meas)
# self.read_meas_lists[-1].append(meas)
# else:
# if cycle == "one":
# meas.meta_str = sram_op.WRITE_ONE
# else:
# meas.meta_str = sram_op.WRITE_ZERO
# self.write_bit_meas[polarity].append(meas)
# self.write_meas_lists[-1].append(meas)
delay_path_rule = re.compile(r"\.meas tran delay_(.*)_to_(.*)_(sen|bl)_(id\d*) TRIG v\((.*)\) VAL=(\d+(\.\d+)?) (RISE|FALL)=(\d+) TD=(\d+(\.\d+)?)n TARG v\((.*)\) VAL=(\d+(\.\d+)?) (RISE|FALL)=(\d+) TD=(\d+(\.\d+)?)n")
port = self.read_ports[0]
meas_buff = []
self.sen_path_meas = []
self.bl_path_meas = []
for measures in port_measure_lines:
text = measures[1]
delay_path_iter = delay_path_rule.finditer(text)
for delay_path_measure in delay_path_iter:
from_ = delay_path_measure.group(1)
to_ = delay_path_measure.group(2)
path_ = delay_path_measure.group(3)
id_ = delay_path_measure.group(4)
trig_rise = delay_path_measure.group(8)
targ_rise = delay_path_measure.group(15)
meas_name = "delay_{0}_to_{1}_{2}_{3}".format(from_, to_, path_, id_)
meas = delay_measure(meas_name, from_, to_, trig_rise, targ_rise, measure_scale=1e9, has_port=False)
meas.meta_str = sram_op.READ_ZERO
meas.meta_add_delay = True
meas_buff.append(meas)
if path_ == "sen":
self.sen_path_meas.extend(meas_buff.copy())
meas_buff.clear()
elif path_ == "bl":
self.bl_path_meas.extend(meas_buff.copy())
meas_buff.clear()
self.read_meas_lists.append(self.sen_path_meas + self.bl_path_meas)
def set_spice_names(self):
"""This is run in place of set_internal_spice_names function from
simulation.py when running stand-alone characterizer."""
self.bl_name = OPTS.bl_format.format(name=self.sram.name,
hier_sep=OPTS.hier_seperator,
row="{}",
col=self.bitline_column)
self.br_name = OPTS.br_format.format(name=self.sram.name,
hier_sep=OPTS.hier_seperator,
row="{}",
col=self.bitline_column)
self.sen_name = OPTS.sen_format.format(name=self.sram.name,
hier_sep=OPTS.hier_seperator)
self.cell_name = OPTS.cell_format.format(name=self.sram.name,
hier_sep=OPTS.hier_seperator,
row="{}",
col="{}")
def analysis_init(self, probe_address, probe_data):
"""Sets values which are dependent on the data address/bit being tested."""
self.set_probe(probe_address, probe_data)
self.prepare_netlist()
if OPTS.top_process == "memchar":
self.set_spice_names()
self.create_measurement_names()
self.create_measurement_objects()
self.recover_measurment_objects()
else:
self.create_graph()
self.set_internal_spice_names()
self.create_measurement_names()
self.create_measurement_objects()
def analyze(self, probe_address, probe_data, load_slews):
"""
Main function to characterize an SRAM for a table. Computes both delay and power characterization.
"""
# Dict to hold all characterization values
char_sram_data = {}
self.analysis_init(probe_address, probe_data)
loads = []
slews = []
for load, slew in load_slews:
loads.append(load)
slews.append(slew)
self.load=max(loads)
self.slew=max(slews)
# 1) Find a feasible period and it's corresponding delays using the trimmed array.
feasible_delays = self.find_feasible_period()
# 2) Finds the minimum period without degrading the delays by X%
self.set_load_slew(max(loads), max(slews))
min_period = self.find_min_period(feasible_delays)
debug.check(type(min_period)==float, "Couldn't find minimum period.")
debug.info(1, "Min Period Found: {0}ns".format(min_period))
char_sram_data["min_period"] = round_time(min_period)
# 3) Find the leakage power of the trimmmed and UNtrimmed arrays.
(full_array_leakage, trim_array_leakage)=self.run_power_simulation()
char_sram_data["leakage_power"]=full_array_leakage
leakage_offset = full_array_leakage - trim_array_leakage
# 4) At the minimum period, measure the delay, slew and power for all slew/load pairs.
self.period = min_period
char_port_data = self.simulate_loads_and_slews(load_slews, leakage_offset)
if OPTS.use_specified_load_slew is not None and len(load_slews) > 1:
debug.warning("Path delay lists not correctly generated for characterizations of more than 1 load,slew")
# Get and save the path delays
if self.sen_path_meas and self.bl_path_meas:
bl_names, bl_delays, sen_names, sen_delays = self.get_delay_lists(self.path_delays)
# Removed from characterization output temporarily
# char_sram_data["bl_path_measures"] = bl_delays
# char_sram_data["sen_path_measures"] = sen_delays
# char_sram_data["bl_path_names"] = bl_names
# char_sram_data["sen_path_names"] = sen_names
# FIXME: low-to-high delays are altered to be independent of the period. This makes the lib results less accurate.
self.alter_lh_char_data(char_port_data)
return (char_sram_data, char_port_data)
def alter_lh_char_data(self, char_port_data):
"""Copies high-to-low data to low-to-high data to make them consistent on the same clock edge."""
# This is basically a hack solution which should be removed/fixed later.
for port in self.all_ports:
char_port_data[port]['delay_lh'] = char_port_data[port]['delay_hl']
char_port_data[port]['slew_lh'] = char_port_data[port]['slew_hl']
def simulate_loads_and_slews(self, load_slews, leakage_offset):
"""Simulate all specified output loads and input slews pairs of all ports"""
measure_data = self.get_empty_measure_data_dict()
# Set the target simulation ports to all available ports. This make sims slower but failed sims exit anyways.
self.targ_read_ports = self.read_ports
self.targ_write_ports = self.write_ports
for load, slew in load_slews:
self.set_load_slew(load, slew)
# Find the delay, dynamic power, and leakage power of the trimmed array.
(success, delay_results) = self.run_delay_simulation()
debug.check(success, "Couldn't run a simulation. slew={0} load={1}\n".format(self.slew, self.load))
debug.info(1, "Simulation Passed: Port {0} slew={1} load={2}".format("All", self.slew, self.load))
# The results has a dict for every port but dicts can be empty (e.g. ports were not targeted).
for port in self.all_ports:
for mname, value in delay_results[port].items():
if "power" in mname:
# Subtract partial array leakage and add full array leakage for the power measures
debug.info(1, "Adding leakage offset to {0} {1} + {2} = {3}".format(mname, value, leakage_offset, value + leakage_offset))
measure_data[port][mname].append(value + leakage_offset)
else:
measure_data[port][mname].append(value)
return measure_data
def get_delay_lists(self, value_dict):
"""Returns dicts for path measures of bitline and sen paths"""
sen_name_list = []
sen_delay_list = []
for meas in self.sen_path_meas:
sen_name_list.append(meas.name)
sen_delay_list.append(value_dict[meas.name])
bl_name_list = []
bl_delay_list = []
for meas in self.bl_path_meas:
bl_name_list.append(meas.name)
bl_delay_list.append(value_dict[meas.name])
return sen_name_list, sen_delay_list, bl_name_list, bl_delay_list
def calculate_inverse_address(self):
"""Determine dummy test address based on probe address and column mux size."""
# The inverse address needs to share the same bitlines as the probe address as the trimming will remove all other bitlines
# This is only an issue when there is a column mux and the address maps to different bitlines.
column_addr = self.get_column_addr() # do not invert this part
inverse_address = ""
for c in self.probe_address[self.sram.col_addr_size:]: # invert everything else
if c=="0":
inverse_address += "1"
elif c=="1":
inverse_address += "0"
else:
debug.error("Non-binary address string", 1)
return inverse_address + column_addr
def gen_test_cycles_one_port(self, read_port, write_port):
"""Sets a list of key time-points [ns] of the waveform (each rising edge)
of the cycles to do a timing evaluation of a single port """
# Create the inverse address for a scratch address
inverse_address = self.calculate_inverse_address()
# For now, ignore data patterns and write ones or zeros
data_ones = "1" * (self.word_size + self.num_spare_cols)
data_zeros = "0" * (self.word_size + self.num_spare_cols)
wmask_ones = "1" * self.num_wmasks
if self.t_current == 0:
self.add_noop_all_ports("Idle cycle (no positive clock edge)")
self.add_write("W data 1 address {0}".format(inverse_address),
inverse_address,
data_ones,
wmask_ones,
write_port)
self.add_write("W data 0 address {0} to write value".format(self.probe_address),
self.probe_address,
data_zeros,
wmask_ones,
write_port)
self.measure_cycles[write_port][sram_op.WRITE_ZERO] = len(self.cycle_times) - 1
self.add_noop_clock_one_port(write_port)
self.measure_cycles[write_port]["disabled_write0"] = len(self.cycle_times) - 1
# This also ensures we will have a H->L transition on the next read
self.add_read("R data 1 address {0} to set dout caps".format(inverse_address),
inverse_address,
read_port)
self.add_read("R data 0 address {0} to check W0 worked".format(self.probe_address),
self.probe_address,
read_port)
self.measure_cycles[read_port][sram_op.READ_ZERO] = len(self.cycle_times) - 1
self.add_noop_clock_one_port(read_port)
self.measure_cycles[read_port]["disabled_read0"] = len(self.cycle_times) - 1
self.add_noop_all_ports("Idle cycle (if read takes >1 cycle)")
self.add_write("W data 1 address {0} to write value".format(self.probe_address),
self.probe_address,
data_ones,
wmask_ones,
write_port)
self.measure_cycles[write_port][sram_op.WRITE_ONE] = len(self.cycle_times) - 1
self.add_noop_clock_one_port(write_port)
self.measure_cycles[write_port]["disabled_write1"] = len(self.cycle_times) - 1
self.add_write("W data 0 address {0} to clear din caps".format(inverse_address),
inverse_address,
data_zeros,
wmask_ones,
write_port)
self.add_noop_clock_one_port(read_port)
self.measure_cycles[read_port]["disabled_read1"] = len(self.cycle_times) - 1
# This also ensures we will have a L->H transition on the next read
self.add_read("R data 0 address {0} to clear dout caps".format(inverse_address),
inverse_address,
read_port)
self.add_read("R data 1 address {0} to check W1 worked".format(self.probe_address),
self.probe_address,
read_port)
self.measure_cycles[read_port][sram_op.READ_ONE] = len(self.cycle_times) - 1
self.add_noop_all_ports("Idle cycle (if read takes >1 cycle))")
def get_available_port(self, get_read_port):
"""Returns the first accessible read or write port. """
if get_read_port and len(self.read_ports) > 0:
return self.read_ports[0]
elif not get_read_port and len(self.write_ports) > 0:
return self.write_ports[0]
return None
def set_stimulus_variables(self):
simulation.set_stimulus_variables(self)
self.measure_cycles = [{} for port in self.all_ports]
def create_test_cycles(self):
"""
Returns a list of key time-points [ns] of the waveform (each rising edge)
of the cycles to do a timing evaluation. The last time is the end of the simulation
and does not need a rising edge.
"""
# Using this requires setting at least one port to target for simulation.
if len(self.targ_write_ports) == 0 or len(self.targ_read_ports) == 0:
debug.error("Write and read port must be specified for characterization.", 1)
self.set_stimulus_variables()
# Get any available read/write port in case only a single write or read ports is being characterized.
cur_read_port = self.get_available_port(get_read_port=True)
cur_write_port = self.get_available_port(get_read_port=False)
debug.check(cur_read_port is not None,
"Characterizer requires at least 1 read port")
debug.check(cur_write_port is not None,
"Characterizer requires at least 1 write port")
# Create test cycles for specified target ports.
write_pos = 0
read_pos = 0
while True:
# Exit when all ports have been characterized
if write_pos >= len(self.targ_write_ports) and read_pos >= len(self.targ_read_ports):
break
# Select new write and/or read ports for the next cycle. Use previous port if none remaining.
if write_pos < len(self.targ_write_ports):
cur_write_port = self.targ_write_ports[write_pos]
write_pos+=1
if read_pos < len(self.targ_read_ports):
cur_read_port = self.targ_read_ports[read_pos]
read_pos+=1
# Add test cycle of read/write port pair. One port could have been used already, but the other has not.
self.gen_test_cycles_one_port(cur_read_port, cur_write_port)
def gen_data(self):
""" Generates the PWL data inputs for a simulation timing test. """
for write_port in self.write_ports:
for i in range(self.word_size + self.num_spare_cols):
sig_name="{0}{1}_{2} ".format(self.din_name, write_port, i)
self.stim.gen_pwl(sig_name, self.cycle_times, self.data_values[write_port][i], self.period, self.slew, 0.05)
def gen_addr(self):
"""
Generates the address inputs for a simulation timing test.
This alternates between all 1's and all 0's for the address.
"""
for port in self.all_ports:
for i in range(self.bank_addr_size):
sig_name = "{0}{1}_{2}".format(self.addr_name, port, i)
self.stim.gen_pwl(sig_name, self.cycle_times, self.addr_values[port][i], self.period, self.slew, 0.05)
def gen_control(self):
""" Generates the control signals """
for port in self.all_ports:
self.stim.gen_pwl("CSB{0}".format(port), self.cycle_times, self.csb_values[port], self.period, self.slew, 0.05)
if port in self.readwrite_ports:
self.stim.gen_pwl("WEB{0}".format(port), self.cycle_times, self.web_values[port], self.period, self.slew, 0.05)
if self.sram.num_wmasks:
for bit in range(self.sram.num_wmasks):
self.stim.gen_pwl("WMASK{0}_{1}".format(port, bit), self.cycle_times, self.wmask_values[port][bit], self.period, self.slew, 0.05)