From fd806077d2b32f94c2efc8745eb4442bd68b789d Mon Sep 17 00:00:00 2001 From: Hunter Nichols Date: Mon, 8 Oct 2018 08:42:32 -0700 Subject: [PATCH] Added class and test for testing the delay of several bitcells. --- compiler/characterizer/__init__.py | 1 + compiler/characterizer/worst_case.py | 1047 ++++++++++++++++++++ compiler/globals.py | 2 +- compiler/sram.py | 32 +- compiler/tests/27_worst_case_delay_test.py | 59 ++ compiler/verify/__init__.py | 1 + 6 files changed, 1126 insertions(+), 16 deletions(-) create mode 100644 compiler/characterizer/worst_case.py create mode 100755 compiler/tests/27_worst_case_delay_test.py diff --git a/compiler/characterizer/__init__.py b/compiler/characterizer/__init__.py index ea99c51c..53155e09 100644 --- a/compiler/characterizer/__init__.py +++ b/compiler/characterizer/__init__.py @@ -6,6 +6,7 @@ from .lib import * from .delay import * from .setup_hold import * from .functional import * +from .worst_case import * from .simulation import * diff --git a/compiler/characterizer/worst_case.py b/compiler/characterizer/worst_case.py new file mode 100644 index 00000000..351b24e4 --- /dev/null +++ b/compiler/characterizer/worst_case.py @@ -0,0 +1,1047 @@ +import sys,re,shutil +import debug +import tech +import math +from .stimuli import * +from .trim_spice import * +from .charutils import * +import utils +from globals import OPTS + +class worst_case(): + """Functions to test for the worst case delay in a target SRAM + + The current worst case determines a feasible period for the SRAM then tests + several bits and record the delay and differences between the bits. + + """ + + def __init__(self, sram, spfile, corner): + self.sram = sram + self.name = sram.name + self.word_size = self.sram.word_size + self.addr_size = self.sram.addr_size + self.num_cols = self.sram.num_cols + self.num_rows = self.sram.num_rows + self.num_banks = self.sram.num_banks + self.sp_file = spfile + + self.total_ports = self.sram.total_ports + self.total_write = self.sram.total_write + self.total_read = self.sram.total_read + self.read_index = self.sram.read_index + self.write_index = self.sram.write_index + self.port_id = self.sram.port_id + + # These are the member variables for a simulation + self.period = 0 + self.set_load_slew(0,0) + self.set_corner(corner) + self.create_port_names() + self.create_signal_names() + + #Create global measure names. Should maybe be an input at some point. + self.create_measurement_names() + + def create_measurement_names(self): + """Create measurement names. The names themselves currently define the type of measurement""" + #Altering the names will crash the characterizer. TODO: object orientated approach to the measurements. + self.delay_meas_names = ["delay_lh", "delay_hl", "slew_lh", "slew_hl"] + self.power_meas_names = ["read0_power", "read1_power", "write0_power", "write1_power"] + + def create_signal_names(self): + self.addr_name = "A" + self.din_name = "DIN" + self.dout_name = "DOUT" + + #This is TODO once multiport control has been finalized. + #self.control_name = "CSB" + + def create_port_names(self): + """Generates the port names to be used in characterization and sets default simulation target ports""" + self.write_ports = [] + self.read_ports = [] + self.total_port_num = OPTS.num_rw_ports + OPTS.num_w_ports + OPTS.num_r_ports + + #save a member variable to avoid accessing global. readwrite ports have different control signals. + self.readwrite_port_num = OPTS.num_rw_ports + + #Generate the port names. readwrite ports are required to be added first for this to work. + for readwrite_port_num in range(OPTS.num_rw_ports): + self.read_ports.append(readwrite_port_num) + self.write_ports.append(readwrite_port_num) + #This placement is intentional. It makes indexing input data easier. See self.data_values + for write_port_num in range(OPTS.num_rw_ports, OPTS.num_rw_ports+OPTS.num_w_ports): + self.write_ports.append(write_port_num) + for read_port_num in range(OPTS.num_rw_ports+OPTS.num_w_ports, OPTS.num_rw_ports+OPTS.num_w_ports+OPTS.num_r_ports): + self.read_ports.append(read_port_num) + + #Set the default target ports for simulation. Default is all the ports. + self.targ_read_ports = self.read_ports + self.targ_write_ports = self.write_ports + + def set_corner(self,corner): + """ Set the corner values """ + self.corner = corner + (self.process, self.vdd_voltage, self.temperature) = corner + + 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.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_sram(sram=self.sram, + port_signal_names=(self.addr_name,self.din_name,self.dout_name), + port_info=(self.total_port_num,self.write_ports,self.read_ports), + abits=self.addr_size, + dbits=self.word_size, + sram_name=self.name) + self.sf.write("\n* SRAM output loads\n") + for port in self.read_ports: + for i in range(self.word_size): + 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 + temp_stim = "{0}/stim.sp".format(OPTS.openram_temp) + self.sf = open(temp_stim, "w") + 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.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 range(self.total_port_num): + 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.write_delay_measures() + + # run until the end of the cycle time + self.stim.write_control(self.cycle_times[-1] + self.period) + + self.sf.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() + + # obtains list of time-points for each rising clk edge + #self.create_test_cycles() + + # creates and opens stimulus file for writing + temp_stim = "{0}/stim.sp".format(OPTS.openram_temp) + self.sf = open(temp_stim, "w") + self.sf.write("* Power stimulus for period of {0}n\n\n".format(self.period)) + self.stim = stimuli(self.sf, 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.stim.gen_constant(sig_name="{0}{1}_{2} ".format(self.din_name,write_port, i), + v_val=0) + for port in range(self.total_port_num): + for i in range(self.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 range(self.total_port_num): + self.stim.gen_constant(sig_name="CSB{0}".format(port), v_val=self.vdd_voltage) + if port in self.write_ports and port in self.read_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 range(self.total_port_num): + self.stim.gen_constant(sig_name="CLK{0}".format(port), v_val=0) + + self.write_power_measures() + + # run until the end of the cycle time + self.stim.write_control(2*self.period) + + self.sf.close() + + def get_delay_meas_values(self, delay_name, port): + """Get the values needed to generate a Spice measurement statement based on the name of the measurement.""" + debug.check('lh' in delay_name or 'hl' in delay_name, "Measure command {0} does not contain direction (lh/hl)") + trig_clk_name = "clk{0}".format(port) + meas_name="{0}{1}".format(delay_name, port) + targ_name = "{0}".format("{0}{1}_{2}".format(self.dout_name,port,self.probe_data)) + half_vdd = 0.5 * self.vdd_voltage + trig_slew_low = 0.1 * self.vdd_voltage + targ_slew_high = 0.9 * self.vdd_voltage + if 'delay' in delay_name: + trig_dir="RISE" + trig_val = half_vdd + targ_val = half_vdd + trig_name = trig_clk_name + if 'lh' in delay_name: + targ_dir="RISE" + trig_td = targ_td = self.cycle_times[self.measure_cycles["read1_{0}".format(port)]] + else: + targ_dir="FALL" + trig_td = targ_td = self.cycle_times[self.measure_cycles["read0_{0}".format(port)]] + + elif 'slew' in delay_name: + trig_name = targ_name + if 'lh' in delay_name: + trig_val = trig_slew_low + targ_val = targ_slew_high + targ_dir = trig_dir = "RISE" + trig_td = targ_td = self.cycle_times[self.measure_cycles["read1_{0}".format(port)]] + else: + trig_val = targ_slew_high + targ_val = trig_slew_low + targ_dir = trig_dir = "FALL" + trig_td = targ_td = self.cycle_times[self.measure_cycles["read0_{0}".format(port)]] + else: + debug.error(1, "Measure command {0} not recognized".format(delay_name)) + return (meas_name,trig_name,targ_name,trig_val,targ_val,trig_dir,targ_dir,trig_td,targ_td) + + 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 dname in self.delay_meas_names: + meas_values = self.get_delay_meas_values(dname, port) + self.stim.gen_meas_delay(*meas_values) + + # add measure statements for power + for pname in self.power_meas_names: + if "read" not in pname: + continue + #Different naming schemes are used for the measure cycle dict and measurement names. + #TODO: make them the same so they can be indexed the same. + if '1' in pname: + t_initial = self.cycle_times[self.measure_cycles["read1_{0}".format(port)]] + t_final = self.cycle_times[self.measure_cycles["read1_{0}".format(port)]+1] + elif '0' in pname: + t_initial = self.cycle_times[self.measure_cycles["read0_{0}".format(port)]] + t_final = self.cycle_times[self.measure_cycles["read0_{0}".format(port)]+1] + self.stim.gen_meas_power(meas_name="{0}{1}".format(pname, port), + t_initial=t_initial, + t_final=t_final) + + 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 pname in self.power_meas_names: + if "write" not in pname: + continue + t_initial = self.cycle_times[self.measure_cycles["write0_{0}".format(port)]] + t_final = self.cycle_times[self.measure_cycles["write0_{0}".format(port)]+1] + if '1' in pname: + t_initial = self.cycle_times[self.measure_cycles["write1_{0}".format(port)]] + t_final = self.cycle_times[self.measure_cycles["write1_{0}".format(port)]+1] + + self.stim.gen_meas_power(meas_name="{0}{1}".format(pname, port), + t_initial=t_initial, + t_final=t_final) + + 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.sf.write("* {}\n".format(comment)) + + for read_port in self.targ_read_ports: + self.write_delay_measures_read_port(read_port) + for write_port in self.targ_write_ports: + self.write_delay_measures_write_port(write_port) + + + 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 + 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"]) + #feasible_period = float(2.5)#What happens if feasible starting point is wrong? + time_out = 9 + while True: + time_out -= 1 + if (time_out <= 0): + debug.error("Timed out, could not find a feasible period.",2) + + #Clear any write target ports and set read port + self.targ_write_ports = [] + self.targ_read_ports = [port] + success = False + + 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_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 range(self.total_port_num)] + self.period = float(tech.spice["feasible_period"]) + + #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 parse_values(self, values_names, port, mult = 1.0): + """Parse multiple values in the timing output file. Optional multiplier. + Return a dict of the input names and values. Port used for parsing file. + """ + values = [] + all_values_floats = True + for vname in values_names: + #ngspice converts all measure characters to lowercase, not tested on other sims + value = parse_spice_list("timing", "{0}{1}".format(vname.lower(), port)) + #Check if any of the values fail to parse + if type(value)!=float: + all_values_floats = False + values.append(value) + + #Apply Multiplier only if all values are floats. Let other check functions handle this error. + if all_values_floats: + return {values_names[i]:values[i]*mult for i in range(len(values))} + else: + return {values_names[i]:values[i] for i in range(len(values))} + + 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. + """ + #Sanity Check + debug.check(self.period > 0, "Target simulation period non-positive") + + result = [{} for i in range(self.total_port_num)] + # Checking from not data_value to data_value + self.write_delay_stimulus() + + self.stim.run_sim() + + #Loop through all targeted ports and collect delays and powers. + #Too much duplicate code here. Try reducing + for port in self.targ_read_ports: + debug.info(2, "Check delay values for port {}".format(port)) + delay_names = ["{0}{1}".format(mname,port) for mname in self.delay_meas_names] + delay_names = [mname for mname in self.delay_meas_names] + delays = self.parse_values(delay_names, port, 1e9) # scale delays to ns + if not self.check_valid_delays(tuple(delays.values())): + return (False,{}) + result[port].update(delays) + + power_names = [mname for mname in self.power_meas_names if 'read' in mname] + powers = self.parse_values(power_names, port, 1e3) # scale power to mw + #Check that power parsing worked. + for name, power in powers.items(): + if type(power)!=float: + debug.error("Failed to Parse Power Values:\n\t\t{0}".format(powers),1) #Printing the entire dict looks bad. + result[port].update(powers) + + for port in self.targ_write_ports: + power_names = [mname for mname in self.power_meas_names if 'write' in mname] + powers = self.parse_values(power_names, port, 1e3) # scale power to mw + #Check that power parsing worked. + for name, power in powers.items(): + if type(power)!=float: + debug.error("Failed to Parse Power Values:\n\t\t{0}".format(powers),1) #Printing the entire dict looks bad. + result[port].update(powers) + + # The delay is from the negative edge for our SRAM + return (True,result) + + + 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() + 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() + 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, delay_tuple): + """ Check if the measurements are defined and if they are valid. """ + + (delay_hl, delay_lh, slew_hl, slew_lh) = delay_tuple + 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) + if delay_hl>self.period or delay_lh>self.period or slew_hl>self.period or slew_lh>self.period: + 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)) + + 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 + 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) + + + 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: + delay_port_names = [mname for mname in self.delay_meas_names if "delay" in mname] + for dname in delay_port_names: + 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") + + #Dynamic way to build string. A bit messy though. + 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.""" + self.probe_address = probe_address + self.probe_data = probe_data + + + + def prepare_netlist(self): + """ Prepare a trimmed netlist and regular netlist. """ + + # Set up to trim the netlist here if that is enabled + if OPTS.trim_netlist: + self.trim_sp_file = "{}reduced.sp".format(OPTS.openram_temp) + self.trimsp=trim_spice(self.sp_file, self.trim_sp_file) + self.trimsp.set_configuration(self.num_banks, + self.num_rows, + self.num_cols, + self.word_size) + self.trimsp.trim(self.probe_address,self.probe_data) + else: + # The non-reduced netlist file when it is disabled + self.trim_sp_file = "{}sram.sp".format(OPTS.openram_temp) + + # The non-reduced netlist file for power simulation + self.sim_sp_file = "{}sram.sp".format(OPTS.openram_temp) + # Make a copy in temp for debugging + shutil.copy(self.sp_file, self.sim_sp_file) + + + + def analyze(self,probe_address, probe_data, slews, loads): + """ + Main function to test the delays of different bits. + """ + debug.check(OPTS.num_rw_ports < 2 and OPTS.num_w_ports < 1 and OPTS.num_r_ports < 1 , + "Bit testing does not currently support multiport.") + #Dict to hold all characterization values + char_sram_data = {} + + self.set_probe(probe_address, probe_data) + self.prepare_netlist() + + 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) Find the delays of several bits + test_bits = self.get_test_bits() + bit_delays = self.simulate_for_bit_delays(test_bits) + + for delay in bit_delays: + debug.info(1, "{}".format(delay)) + + def simulate_for_bit_delays(self, test_bits): + """Simulates the delay of the sram of over several bits.""" + bit_delays = [{} for i in range(len(test_bits))] + + #Assumes a bitcell with only 1 rw port. (6t, port 0) + port = 0 + self.targ_read_ports = [self.read_ports[port]] + self.targ_write_ports = [self.write_ports[port]] + + for i in range(len(test_bits)): + (bit_addr, bit_data) = test_bits[i] + self.set_probe(bit_addr, bit_data) + debug.info(1,"Delay bit test: period {}, addr {}, data_pos {}".format(self.period, bit_addr, bit_data)) + (success, results)=self.run_delay_simulation() + debug.check(success, "Bit Test Failed: period {}, addr {}, data_pos {}".format(self.period, bit_addr, bit_data)) + bit_delays[i] = results[port] + + return bit_delays + + + def get_test_bits(self): + """Statically determines address and bit values to test""" + #First and last address, first and last bit + bit_addrs = ["0"*self.addr_size, "1"*self.addr_size] + data_positions = [0, self.word_size-1] + #Return them in a tuple form + return [(bit_addrs[i], data_positions[i]) for i in range(len(bit_addrs))] + + def simulate_loads_and_slews(self, slews, loads, 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 slew in slews: + for load in loads: + 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 range(self.total_port_num): + 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 + measure_data[port][mname].append(value + leakage_offset) + else: + measure_data[port][mname].append(value) + return measure_data + + def add_data(self, data, port): + """ Add the array of data values """ + debug.check(len(data)==self.word_size, "Invalid data word size.") + debug.check(port < len(self.data_values), "Port number cannot index data values.") + index = 0 + for c in data: + if c=="0": + self.data_values[port][index].append(0) + elif c=="1": + self.data_values[port][index].append(1) + else: + debug.error("Non-binary data string",1) + index += 1 + + def add_address(self, address, port): + """ Add the array of address values """ + debug.check(len(address)==self.addr_size, "Invalid address size.") + index = 0 + for c in address: + if c=="0": + self.addr_values[port][index].append(0) + elif c=="1": + self.addr_values[port][index].append(1) + else: + debug.error("Non-binary address string",1) + index += 1 + + def add_noop_one_port(self, address, data, port): + """ Add the control values for a noop to a single port. """ + #This is to be used as a helper function for the other add functions. Cycle and comments are omitted. + self.add_control_one_port(port, "noop") + if port in self.write_ports: + self.add_data(data,port) + self.add_address(address, port) + + def add_noop_all_ports(self, comment, address, data): + """ Add the control values for a noop to all ports. """ + self.add_comment("All", comment) + self.cycle_times.append(self.t_current) + self.t_current += self.period + + for port in range(self.total_port_num): + self.add_noop_one_port(address, data, port) + + + def add_read(self, comment, address, data, port): + """ Add the control values for a read cycle. """ + debug.check(port in self.read_ports, "Cannot add read cycle to a write port.") + self.add_comment(port, comment) + self.cycle_times.append(self.t_current) + self.t_current += self.period + self.add_control_one_port(port, "read") + + #If the port is also a readwrite then add data. + if port in self.write_ports: + self.add_data(data,port) + self.add_address(address, port) + + #This value is hard coded here. Possibly change to member variable or set in add_noop_one_port + noop_data = "0"*self.word_size + #Add noops to all other ports. + for unselected_port in range(self.total_port_num): + if unselected_port != port: + self.add_noop_one_port(address, noop_data, unselected_port) + + def add_write(self, comment, address, data, port): + """ Add the control values for a write cycle. """ + debug.check(port in self.write_ports, "Cannot add read cycle to a read port.") + self.add_comment(port, comment) + self.cycle_times.append(self.t_current) + self.t_current += self.period + + self.add_control_one_port(port, "write") + self.add_data(data,port) + self.add_address(address,port) + + #This value is hard coded here. Possibly change to member variable or set in add_noop_one_port + noop_data = "0"*self.word_size + #Add noops to all other ports. + for unselected_port in range(self.total_port_num): + if unselected_port != port: + self.add_noop_one_port(address, noop_data, unselected_port) + + def add_control_one_port(self, port, op): + """Appends control signals for operation to a given port""" + #Determine values to write to port + web_val = 1 + csb_val = 1 + if op == "read": + csb_val = 0 + elif op == "write": + csb_val = 0 + web_val = 0 + elif op != "noop": + debug.error("Could not add control signals for port {0}. Command {1} not recognized".format(port,op),1) + + #Append the values depending on the type of port + self.csb_values[port].append(csb_val) + #If port is in both lists, add rw control signal. Condition indicates its a RW port. + if port in self.write_ports and port in self.read_ports: + self.web_values[port].append(web_val) + + def add_comment(self, port, comment): + """Add comment to list to be printed in stimulus file""" + #Clean up time before appending. Make spacing dynamic as well. + time = "{0:.2f} ns:".format(self.t_current) + time_spacing = len(time)+6 + self.cycle_comments.append("Cycle {0:<6d} Port {1:<6} {2:<{3}}: {4}".format(len(self.cycle_times), + port, + time, + time_spacing, + comment)) + def gen_test_cycles_one_port(self, read_port, write_port): + """Intended but not implemented: Returns 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. Current: Values overwritten for multiple calls""" + + # Create the inverse address for a scratch address + inverse_address = "" + for c in self.probe_address: + if c=="0": + inverse_address += "1" + elif c=="1": + inverse_address += "0" + else: + debug.error("Non-binary address string",1) + + # For now, ignore data patterns and write ones or zeros + data_ones = "1"*self.word_size + data_zeros = "0"*self.word_size + + if self.t_current == 0: + self.add_noop_all_ports("Idle cycle (no positive clock edge)", + inverse_address, data_zeros) + + self.add_write("W data 1 address 0..00", + inverse_address,data_ones,write_port) + + self.add_write("W data 0 address 11..11 to write value", + self.probe_address,data_zeros,write_port) + self.measure_cycles["write0_{0}".format(write_port)] = len(self.cycle_times)-1 + #self.write0_cycle=len(self.cycle_times)-1 # Remember for power measure + + # This also ensures we will have a H->L transition on the next read + self.add_read("R data 1 address 00..00 to set DOUT caps", + inverse_address,data_zeros,read_port) + + self.add_read("R data 0 address 11..11 to check W0 worked", + self.probe_address,data_zeros,read_port) + self.measure_cycles["read0_{0}".format(read_port)] = len(self.cycle_times)-1 + #self.read0_cycle=len(self.cycle_times)-1 # Remember for power measure + + self.add_noop_all_ports("Idle cycle (if read takes >1 cycle)", + inverse_address,data_zeros) + #Does not seem like is is used anywhere commenting out for now. + #self.idle_cycle=len(self.cycle_times)-1 # Remember for power measure + + self.add_write("W data 1 address 11..11 to write value", + self.probe_address,data_ones,write_port) + self.measure_cycles["write1_{0}".format(write_port)] = len(self.cycle_times)-1 + #self.write1_cycle=len(self.cycle_times)-1 # Remember for power measure + + self.add_write("W data 0 address 00..00 to clear DIN caps", + inverse_address,data_zeros,write_port) + + # This also ensures we will have a L->H transition on the next read + self.add_read("R data 0 address 00..00 to clear DOUT caps", + inverse_address,data_zeros,read_port) + + self.add_read("R data 1 address 11..11 to check W1 worked", + self.probe_address,data_zeros,read_port) + self.measure_cycles["read1_{0}".format(read_port)] = len(self.cycle_times)-1 + #self.read1_cycle=len(self.cycle_times)-1 # Remember for power measure + + self.add_noop_all_ports("Idle cycle (if read takes >1 cycle))", + self.probe_address,data_zeros) + + 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 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 and len(self.targ_read_ports) == 0: + debug.error("No ports selected for characterization.",1) + + # Start at time 0 + self.t_current = 0 + + # Cycle times (positive edge) with comment + self.cycle_comments = [] + self.cycle_times = [] + self.measure_cycles = {} + + # Control signals for ports. These are not the final signals and will likely be changed later. + #web is the enable for write ports. Dicts used for simplicity as ports are not necessarily incremental. + self.web_values = {port:[] for port in self.write_ports} + #csb acts as an enable for the read ports. + self.csb_values = {port:[] for port in range(self.total_port_num)} + + # Address and data values for each address/data bit. A 3d list of size #ports x bits x cycles. + self.data_values=[[[] for bit in range(self.word_size)] for port in range(len(self.write_ports))] + self.addr_values=[[[] for bit in range(self.addr_size)] for port in range(self.total_port_num)] + + #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) + + #These checks should be superceded by check_arguments which should have been called earlier, so this is a double check. + debug.check(cur_read_port != None, "Characterizer requires at least 1 read port") + debug.check(cur_write_port != None, "Characterizer requires at least 1 write port") + + #Characterizing the remaining target ports. Not the final design. + 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 analytical_delay(self,sram, slews, loads): + """ Return the analytical model results for the SRAM. + """ + debug.check(OPTS.num_rw_ports < 2 and OPTS.num_w_ports < 1 and OPTS.num_r_ports < 1 , + "Analytical characterization does not currently support multiport.") + + delay_lh = [] + delay_hl = [] + slew_lh = [] + slew_hl = [] + for slew in slews: + for load in loads: + self.set_load_slew(load,slew) + bank_delay = sram.analytical_delay(self.slew,self.load) + # Convert from ps to ns + delay_lh.append(bank_delay.delay/1e3) + delay_hl.append(bank_delay.delay/1e3) + slew_lh.append(bank_delay.slew/1e3) + slew_hl.append(bank_delay.slew/1e3) + + power = sram.analytical_power(self.process, self.vdd_voltage, self.temperature, load) + #convert from nW to mW + power.dynamic /= 1e6 + power.leakage /= 1e6 + debug.info(1,"Dynamic Power: {0} mW".format(power.dynamic)) + debug.info(1,"Leakage Power: {0} mW".format(power.leakage)) + + sram_data = { "min_period": 0, + "leakage_power": power.leakage} + port_data = [{"delay_lh": delay_lh, + "delay_hl": delay_hl, + "slew_lh": slew_lh, + "slew_hl": slew_hl, + "read0_power": power.dynamic, + "read1_power": power.dynamic, + "write0_power": power.dynamic, + "write1_power": power.dynamic, + }] + return (sram_data,port_data) + + 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): + 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 range(self.total_port_num): + for i in range(self.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 range(self.total_port_num): + 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.read_ports and port in self.write_ports: + self.stim.gen_pwl("WEB{0}".format(port), self.cycle_times, self.web_values[port], self.period, self.slew, 0.05) + + + def get_empty_measure_data_dict(self): + """Make a dict of lists for each type of delay and power measurement to append results to""" + measure_names = self.delay_meas_names + self.power_meas_names + #Create list of dicts. List lengths is # of ports. Each dict maps the measurement names to lists. + measure_data = [{mname:[] for mname in measure_names} for i in range(self.total_port_num)] + return measure_data diff --git a/compiler/globals.py b/compiler/globals.py index af89eaa4..a23c8563 100644 --- a/compiler/globals.py +++ b/compiler/globals.py @@ -24,7 +24,7 @@ def parse_args(): global OPTS option_list = { - optparse.make_option("-b", "--backannotated", action="store_true", dest="run_pex", + optparse.make_option("-b", "--backannotated", action="store_true", dest="use_pex", help="Back annotate simulation"), optparse.make_option("-o", "--output", dest="output_name", help="Base output file name(s) prefix", metavar="FILE"), diff --git a/compiler/sram.py b/compiler/sram.py index 0feea1b3..eafdf80a 100644 --- a/compiler/sram.py +++ b/compiler/sram.py @@ -61,6 +61,21 @@ class sram(): def save(self): """ Save all the output files while reporting time to do it as well. """ + if not OPTS.netlist_only: + # Write the layout + start_time = datetime.datetime.now() + gdsname = OPTS.output_path + self.s.name + ".gds" + print("GDS: Writing to {0}".format(gdsname)) + self.s.gds_write(gdsname) + print_time("GDS", datetime.datetime.now(), start_time) + + # Create a LEF physical model + start_time = datetime.datetime.now() + lefname = OPTS.output_path + self.s.name + ".lef" + print("LEF: Writing to {0}".format(lefname)) + self.s.lef_write(lefname) + print_time("LEF", datetime.datetime.now(), start_time) + # Save the spice file start_time = datetime.datetime.now() spname = OPTS.output_path + self.s.name + ".sp" @@ -70,6 +85,8 @@ class sram(): # Save the extracted spice file if OPTS.use_pex: + import verify + print(verify.__file__) start_time = datetime.datetime.now() # Output the extracted design if requested sp_file = OPTS.output_path + "temp_pex.sp" @@ -93,21 +110,6 @@ class sram(): lib(out_dir=OPTS.output_path, sram=self.s, sp_file=sp_file) print_time("Characterization", datetime.datetime.now(), start_time) - if not OPTS.netlist_only: - # Write the layout - start_time = datetime.datetime.now() - gdsname = OPTS.output_path + self.s.name + ".gds" - print("GDS: Writing to {0}".format(gdsname)) - self.s.gds_write(gdsname) - print_time("GDS", datetime.datetime.now(), start_time) - - # Create a LEF physical model - start_time = datetime.datetime.now() - lefname = OPTS.output_path + self.s.name + ".lef" - print("LEF: Writing to {0}".format(lefname)) - self.s.lef_write(lefname) - print_time("LEF", datetime.datetime.now(), start_time) - # Write a verilog model start_time = datetime.datetime.now() vname = OPTS.output_path + self.s.name + ".v" diff --git a/compiler/tests/27_worst_case_delay_test.py b/compiler/tests/27_worst_case_delay_test.py new file mode 100755 index 00000000..06283109 --- /dev/null +++ b/compiler/tests/27_worst_case_delay_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +""" +Run a regression test on various srams +""" + +import unittest +from testutils import header,openram_test +import sys,os +sys.path.append(os.path.join(sys.path[0],"..")) +import globals +from globals import OPTS +import debug + +class worst_case_timing_sram_test(openram_test): + + def runTest(self): + globals.init_openram("config_20_{0}".format(OPTS.tech_name)) + OPTS.spice_name="ngspice" + OPTS.analytical_delay = False + OPTS.trim_netlist = False + + # This is a hack to reload the characterizer __init__ with the spice version + from importlib import reload + import characterizer + reload(characterizer) + from characterizer import worst_case + if not OPTS.spice_exe: + debug.error("Could not find {} simulator.".format(OPTS.spice_name),-1) + + from sram import sram + from sram_config import sram_config + c = sram_config(word_size=4, + num_words=32, + num_banks=1) + c.words_per_row=1 + debug.info(1, "Testing the timing for 2 bits inside a 2bit, 16words SRAM with 1 bank") + s = sram(c, name="sram1") + + tempspice = OPTS.openram_temp + "temp.sp" + s.sp_write(tempspice) + + + corner = (OPTS.process_corners[0], OPTS.supply_voltages[0], OPTS.temperatures[0]) + wc = worst_case(s.s, tempspice, corner) + import tech + loads = [tech.spice["msflop_in_cap"]*4] + slews = [tech.spice["rise_time"]*2] + probe_address = "1" * s.s.addr_size + probe_data = s.s.word_size - 1 + wc.analyze(probe_address, probe_data, slews, loads) + + globals.end_openram() + +# instantiate a copdsay of the class to actually run the test +if __name__ == "__main__": + (OPTS, args) = globals.parse_args() + del sys.argv[1:] + header(__file__, OPTS.tech_name) + unittest.main() diff --git a/compiler/verify/__init__.py b/compiler/verify/__init__.py index 1f0ffaab..4ddd966c 100644 --- a/compiler/verify/__init__.py +++ b/compiler/verify/__init__.py @@ -54,6 +54,7 @@ else: if OPTS.pex_exe == None: from .none import run_pex,print_pex_stats + print("why god why") elif "calibre"==OPTS.pex_exe[0]: from .calibre import run_pex,print_pex_stats elif "magic"==OPTS.pex_exe[0]: