def run_main(): interp_method = 'spline' sim_env = 'tt' nmos_spec = 'specs_mos_char/nch_w0d5.yaml' pmos_spec = 'specs_mos_char/pch_w0d5.yaml' intent = 'lvt' nch_db = get_db(nmos_spec, intent, interp_method=interp_method, sim_env=sim_env) nch_op = nch_db.query(vbs=0, vds=0.5, vgs=0.5) pprint.pprint(nch_op) # building circuit cir = LTICircuit() cir.add_transistor(nch_op, 'out', 'in', 'gnd', 'gnd', fg=2) cir.add_res(10e3, 'out', 'gnd') cir.add_cap(100e-15, 'out', 'gnd') # get gain/poles/zeros/bode plot trans_fun = cir.get_transfer_function('in', 'out', in_type='v') print('poles: %s' % trans_fun.poles) print('zeros: %s' % trans_fun.zeros) # note: don't use the gain attribute. For some reason it's broken print('gain: %.4g' % (trans_fun.num[-1] / trans_fun.den[-1])) fvec = np.logspace(5, 10, 1000) _, mag, phase = sig.bode(trans_fun, w=2 * np.pi * fvec) # get transient response state_space = cir.get_state_space('in', 'out', in_type='v') tvec = np.linspace(0, 1e-8, 1000) _, yvec = sig.step(state_space, T=tvec) _, (ax0, ax1) = plt.subplots(2, sharex=True) ax0.semilogx(fvec, mag) ax0.set_ylabel('Magnitude (dB)') ax1.semilogx(fvec, phase) ax1.set_ylabel('Phase (degrees)') ax1.set_xlabel('Frequency (Hz)') plt.figure(2) plt.plot(tvec, yvec) plt.ylabel('Output (V/V)') plt.xlabel('Time (s)') plt.show()
def construct_sfP_LTI(p_op, p_B_op, n_B_op, nf_sfP_p, nf_sfP_pB, nf_sfP_nB, cload): ''' Inputs: *_op: Operating point information for a given device. A capital "B" indicates a "bias" device, i.e. a device whose gm doesn't really factor into the final gain expression. nf_*: Integer. Number of minimum channel width/length devices in parallel. cload: Float. Load capacitance in farads. Outputs: Returns the LTICircuit constructed for the second-stage P-input source follower. ''' ckt = LTICircuit() ckt.add_transistor(p_op, 'tail_copy_sfP', 'out2', 'out', fg=nf_sfP_p) ckt.add_transistor(p_B_op, 'out', 'gnd', 'gnd', fg=nf_sfP_pB) ckt.add_transistor(n_B_op, 'tail_copy_sfP', 'gnd', 'gnd', fg=nf_sfP_nB) ckt.add_cap(cload, 'out', 'gnd')
cond_print("ITAIL: {}".format(itail), debugMode) # cond_print("INPUT DEVICE CURRENT: {}".format(op_in['ibias'])) # Include trimming devices idiff_trim_base = abs(op_trim_on['ibias']-op_trim_off['ibias']) if idiff_trim_base == 0: nf_trim = 1 itrim = 0 else: vos_base = idiff_trim_base*rload nf_trim = ceil(vos_targ/vos_base) itrim = (op_trim_on['ibias']+op_trim_off['ibias'])*nf_trim # LTICircuit simulation circuit = LTICircuit() circuit.add_transistor(op_in, 'outp', 'gnd', 'tail', fg=nf_in) circuit.add_transistor(op_in, 'outn', 'inp', 'tail', fg=nf_in) circuit.add_transistor(op_trim_on, 'outn', 'gnd', 'gnd', fg=nf_trim) circuit.add_transistor(op_trim_off, 'outp', 'gnd', 'gnd', fg=nf_trim) circuit.add_transistor(op_tail, 'tail', 'vmirr', 'gnd', fg=nf_tail) circuit.add_transistor(op_mirr, 'vmirr', 'vmirr', 'gnd', fg=nf_mirr) circuit.add_transistor(op_dummy, 'gnd', 'gnd', 'vmirr', fg=nf_dummy) circuit.add_res(rload, 'outp', 'gnd') circuit.add_res(rload, 'outn', 'gnd') circuit.add_cap(cload, 'outp', 'gnd') circuit.add_cap(cload, 'outn', 'gnd') circuit.add_cap(dac_cap, 'tail', 'gnd') num, den = circuit.get_num_den(in_name='inp', out_name='outn', in_type='v') num_unintentional, den_unintentional = circuit.get_num_den(in_name='inp', out_name='outp', in_type='v') gain_intentional = num[-1]/den[-1]
def meet_spec(self, **params) -> List[Mapping[str, Any]]: """To be overridden by subclasses to design this module. Raises a ValueError if there is no solution. """ ### Get DBs for each device specfile_dict = params['specfile_dict'] l_dict = params['l_dict'] th_dict = params['th_dict'] sim_env = params['sim_env'] # Databases db_dict = { k: get_mos_db(spec_file=specfile_dict[k], intent=th_dict[k], lch=l_dict[k], sim_env=sim_env) for k in specfile_dict.keys() } ### Design devices in_type = params['in_type'] vdd = params['vdd'] vincm = params['vincm'] voutcm = params['voutcm'] cload = params['cload'] gain_min = params['gain'] fbw_min = params['fbw'] ugf_min = params['ugf'] ibias_max = params['ibias'] # Somewhat arbitrary vstar_min in this case optional_params = params['optional_params'] vstar_min = optional_params.get('vstar_min', 0.2) res_vstep = optional_params.get('res_vstep', 10e-3) error_tol = optional_params.get('error_tol', 0.01) vout1_opt = optional_params.get('vout1', None) n_in = in_type == 'n' vtest_in = vdd / 2 if n_in else -vdd / 2 vtest_tail = vdd / 2 if n_in else -vdd / 2 vtest_out = -vdd / 2 if n_in else vdd / 2 vb_in = 0 if n_in else vdd vb_tail = 0 if n_in else vdd vb_load = vdd if n_in else 0 vb_out = 0 if n_in else vdd # Estimate threshold of each device TODO can this be more generalized? vth_in = estimate_vth(is_nch=n_in, vgs=vtest_in, vbs=0, db=db_dict['in'], lch=l_dict['in']) vth_tail = estimate_vth(is_nch=n_in, vgs=vtest_tail, vbs=0, db=db_dict['tail'], lch=l_dict['tail']) vth_load = estimate_vth(is_nch=(not n_in), vgs=vtest_out, vbs=0, db=db_dict['load'], lch=l_dict['load']) vth_out = estimate_vth(is_nch=n_in, vgs=vtest_in, vbs=0, db=db_dict['out'], lch=l_dict['out']) # Keeping track of operating points which work for future comparison viable_op_list = [] # Get tail voltage range vtail_min = vstar_min if n_in else vincm - vth_in vtail_max = vincm - vth_in if n_in else vdd - vstar_min vtail_vec = np.arange(vtail_min, vtail_max, res_vstep) print(f'Sweeping tail from {vtail_min} to {vtail_max}') # Get out1 common mode range vout1_min = vincm - vth_in if n_in else vth_load + vstar_min vout1_max = vdd + vth_load - vstar_min if n_in else vincm - vth_in if vout1_opt == None: vout1_vec = np.arange(vout1_min, vout1_max, res_vstep) else: vout1_vec = [vout1_opt] if vout1_opt < vout1_min or vout1_opt > vout1_max: warnings.warn( f'vout11 {vout1_opt} falls outside recommended range ({vout1_min}, {vout1_max})' ) # Sweep tail voltage for vtail in vtail_vec: # Sweep out1 common mode for vout1 in vout1_vec: in_op = db_dict['in'].query(vgs=vincm - vtail, vds=vout1 - vtail, vbs=vb_in - vtail) load_op = db_dict['load'].query(vgs=vout1 - vb_load, vds=vout1 - vb_load, vbs=0) load_copy_op = db_dict['load'].query(vgs=vout1 - vb_load, vds=voutcm - vb_load, vbs=0) out_op = db_dict['out'].query(vgs=voutcm - vb_out, vds=voutcm - vb_out, vbs=0) itail_min = 4 * in_op['ibias'] # Step input device size (integer steps) nf_in_max = int(round(ibias_max / itail_min)) nf_in_vec = np.arange(1, nf_in_max, 1) # if len(nf_in_vec) > 0: # print(f'Number of input devices {min(nf_in_vec)} to {max(nf_in_vec)}') for nf_in in nf_in_vec: itail = itail_min * nf_in # Match load device size load_match, nf_load = verify_ratio(in_op['ibias'] * 2, load_op['ibias'], nf_in, error_tol) if not load_match: print(f"load match {nf_load}") # assert False, 'blep' continue iflip_max = (ibias_max - itail) / 2 nf_out_max = int(round(iflip_max / out_op['ibias'])) nf_out_vec = [1] if nf_out_max == 1 else np.arange( 1, nf_out_max, 1) # if len(nf_out_vec) > 0: # print(f'Number of output devices {min(nf_out_vec)} to {max(nf_out_vec)}') # Step output device size for nf_out in nf_out_vec: iflip_branch = out_op['ibias'] * nf_out # Match load copy device size load_copy_match, nf_load_copy = verify_ratio( out_op['ibias'], load_copy_op['ibias'], nf_out, error_tol) if not load_copy_match: print('load copy match') continue # Check target specs ckt_half = LTICircuit() ckt_half.add_transistor(in_op, 'out', 'in', 'gnd', fg=nf_in * 2, neg_cap=False) ckt_half.add_transistor(load_op, 'out1', 'out1', 'gnd', fg=nf_load, neg_cap=False) ckt_half.add_transistor(load_copy_op, 'out', 'out1', 'gnd', fg=nf_load_copy, neg_cap=False) ckt_half.add_transistor(out_op, 'out', 'out', 'gnd', fg=nf_out, neg_cap=False) ckt_half.add_cap(cload, 'out', 'gnd') num, den = ckt_half.get_num_den(in_name='in', out_name='out', in_type='v') # num = np.convolve(num, [-1]) # To get positive gain wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) # wu, _ = get_w_crossings(num, den) # if wu == None: # wu = 0 # ugf = wu/(2*np.pi) # Rout = parallel(1/(nf_in*2*in_op['gds']), # 1/(nf_out*out_op['gm']), # 1/(nf_out*out_op['gds'])) # Gm = in_op['gm']*nf_in*2 # gain = Gm * Rout # Cout = cload + (nf_in*2*in_op['cgs']) + (1+gain)*(nf_in*2*in_op['cds']) # fbw = 1/(2*np.pi*Rout*Cout) # ugf = Gm / Cout * (1/2*np.pi) gain = -num[-1] / den[-1] ugf = fbw * gain if fbw < fbw_min: print(f"fbw {fbw}") continue if ugf < ugf_min: print(f"ugf {ugf}") break if gain < gain_min: print(f'gain {gain}') break # Design matching tail vgtail_min = vth_tail + vstar_min if n_in else vtail + vth_tail vgtail_max = vtail + vth_tail if n_in else vdd + vth_tail - vstar_min vgtail_vec = np.arange(vgtail_min, vgtail_max, res_vstep) print(f"Tail gate from {vgtail_min} to {vgtail_max}") for vgtail in vgtail_vec: tail_op = db_dict['tail'].query( vgs=vgtail - vb_tail, vds=vtail - vb_tail, vbs=0) tail_match, nf_tail = verify_ratio( in_op['ibias'] * 4, tail_op['ibias'], nf_in, error_tol) if not tail_match: print("tail match") continue print('(SUCCESS)') viable_op = dict( nf_in=nf_in, nf_load=nf_load, nf_load_copy=nf_load_copy, nf_out=nf_out, nf_tail=nf_tail, vgtail=vgtail, gain=gain, fbw=fbw, ugf=ugf, vtail=vtail, vout1=vout1, itail=itail, iflip_branch=nf_out * out_op['ibias'], ibias=itail + 2 * nf_out * out_op['ibias']) print(viable_op) viable_op_list.append(viable_op) self.other_params = dict( in_type=in_type, lch_dict=l_dict, w_dict={k: db.width_list[0] for k, db in db_dict.items()}, th_dict=th_dict) return viable_op_list
def design_inverter_tia_lti(db_n, db_p, sim_env, vg_res, rf_res, vdd, cpd, cload, rdc_min, fbw_min, pm_min, vb_n, vb_p): """ Designs a transimpedance amplifier with an inverter amplifier in resistive feedback. Uses the LTICircuit functionality. Inputs: db_n/p: Databases for NMOS and PMOS device characterization data, respectively. sim_env: Simulation corner. vg_res: Float. Step resolution when sweeping gate voltage. rf_res: Float. Step resolution when sweeping feedback resistance. vdd: Float. Supply voltage. cpd: Float. Input parasitic capacitance. cload: Float. Output load capacitance. rdc_min: Float. Minimum DC transimpedance. fbw_min: Float. Minimum bandwidth (Hz). pm_min: Float. Minimum phase margin. vb_n/p: Float. Back-gate/body voltage of NMOS and PMOS, respectively. Raises: ValueError: If unable to meet the specification requirements. Returns: A dictionary with the following key:value pairings: vg: Float. Input bias voltage. nf_n: Integer. NMOS number of channel fingers. nf_p: Integer. PMOS number of channel fingers. rf: Float. Value of feedback resistor. rdc: Float. Expected DC transimpedance. fbw: Float. Expected bandwidth (Hz). pm: Float. Expected phase margin. ibias: Float. Expected DC bias current. """ ibn_fun = db_n.get_function('ibias', env=sim_env) ibp_fun = db_p.get_function('ibias', env=sim_env) # Get sweep values (Vg, Vd) vg_min = 0 vg_max = vdd vg_vec = np.arange(vg_min, vg_max, vg_res) nf_n_vec = np.arange(1, 20, 1) # DEBUGGING: Is there a non-brute force way of setting this? # Find the best operating point best_ibias = float('inf') best_op = None for vg in vg_vec: vdd_vd_ratio = vdd/vg #print("\nVD/VG: {}".format(vg)) n_op_info = db_n.query(vgs=vg, vds=vg, vbs=vb_n-0) p_op_info = db_p.query(vgs=vg-vdd, vds=vg-vdd, vbs=vb_p-vdd) # Find ratio of fingers to get desired output common mode ibias_n = n_op_info['ibias'] ibias_p = p_op_info['ibias'] pn_match = abs(ibias_n/ibias_p) pn_ratio = pn_match/(vdd_vd_ratio - 1) # DEBUGGING: Won't be exact if pn_ratio == 0: continue # Sweep the number of fingers to minimize power for nf_n in nf_n_vec: nf_p = int(round(nf_n * pn_ratio)) if nf_p <= 0: continue ibias_error = abs(abs(ibias_p)*nf_p-abs(ibias_n)*nf_n)/(abs(ibias_n)*nf_n) if ibias_error > 0.05: continue print("N/P: {}/{} fingers".format(nf_n, nf_p)) # Finding amplifier ss parameters inv = LTICircuit() inv.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n) inv.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p) inv_num, inv_den = inv.get_num_den(in_name='in', out_name='out', in_type='v') A0 = abs(inv_num[-1]/inv_den[-1]) gds_n = n_op_info['gds'] * nf_n gm_n = n_op_info['gm'] * nf_n cgs_n = n_op_info['cgs'] * nf_n cgd_n = n_op_info['cgd'] * nf_n cds_n = n_op_info['cds'] * nf_n cgb_n = n_op_info.get('cgb', 0) * nf_n cdb_n = n_op_info.get('cdb', 0) * nf_n cdd_n = n_op_info['cdd'] * nf_n cgg_n = n_op_info['cgg'] * nf_n gds_p = p_op_info['gds'] * nf_p gm_p = p_op_info['gm'] * nf_p cgs_p = p_op_info['cgs'] * nf_p cgd_p = p_op_info['cgd'] * nf_p cds_p = p_op_info['cds'] * nf_p cgb_p = p_op_info.get('cgb', 0) * nf_p cdb_p = p_op_info.get('cdb', 0) * nf_p cdd_p = p_op_info['cdd'] * nf_p cgg_p = p_op_info['cgg'] * nf_p gm = abs(gm_n) + abs(gm_p) gds = abs(gds_n) + abs(gds_p) ro = 1/gds cgs = cgs_n + cgs_p cds = cds_n + cds_p cgb = cgb_n + cgb_p cdb = cdb_n + cdb_p cgd = cgd_n + cgd_p cdd = cdd_n + cdd_p cgg = cgg_n + cgg_p # Assume Rdc is negative, bound Rf rf_min = max(rdc_min*(1+A0)/A0 + ro/A0, 0) rf_vec = np.arange(rf_min, rdc_min*5, rf_res) # Sweep values of Rf to check f3dB and PM spec for rf in rf_vec: # Circuit for GBW circuit = LTICircuit() circuit.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n) circuit.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p) circuit.add_res(rf, 'in', 'out') circuit.add_cap(cpd, 'in', 'gnd') circuit.add_cap(cload, 'out', 'gnd') # Determining if it meets spec num, den = circuit.get_num_den(in_name='in', out_name='out', in_type='i') rdc = num[-1]/den[-1] if abs(rdc) < rdc_min-1e-8: print("RDC: {0:.2f} (FAIL)\n".format(rdc)) continue else: print("RDC: {0:.2f}".format(rdc)) fbw = get_w_3db(num, den)/(2*np.pi) if fbw < fbw_min or isnan(fbw): print("BW: {} (FAIL)\n".format(fbw)) break # Increasing Rf isn't going to help else: print("BW: {}".format(fbw)) # Circuit for phase margin # miller = (1-gm*rf)/(ro+rf)*ro circuit2 = LTICircuit() circuit2.add_conductance(gds, 'out', 'gnd') circuit2.add_cap(cgg+cpd, 'in', 'gnd') circuit2.add_cap(cdd+cload, 'out', 'gnd') circuit2.add_cap(cgd, 'in', 'out') circuit2.add_res(rf, 'in', 'out') loopBreak = circuit2.get_transfer_function(in_name='out', out_name='in', in_type='i') pm, gainm = get_stability_margins(loopBreak.num*gm, loopBreak.den) if pm < pm_min or isnan(pm): print("PM: {} (FAIL)\n".format(pm)) continue else: print("PM: {}\n".format(pm)) if ibias_n*nf_n < best_ibias: best_ibias = ibias_n*nf_n best_op = dict( vg=vg, nf_n=nf_n, nf_p=nf_p, rf=rf, rdc=rdc, fbw=fbw, pm=pm, ibias=best_ibias) if best_op == None: raise ValueError("No solutions.") return best_op
def _make_circuit(cls, env_idx, gm_db, load_db, vtail, vg, vmid, vout, vbias, vb_gm, vb_load, cload, cpar1, w_dict, th_dict, stack_dict, seg_dict, gz, neg_cap=False, no_fb=False): cur_env = gm_db.env_list[env_idx] gm_db.set_dsn_params(w=w_dict['tail'], intent=th_dict['tail'], stack=stack_dict['tail']) tail1_params = gm_db.query(env=cur_env, vbs=0, vds=vtail - vb_gm, vgs=vbias - vb_gm) tail2_params = gm_db.query(env=cur_env, vbs=0, vds=vout - vb_gm, vgs=vbias - vb_gm) gm_db.set_dsn_params(w=w_dict['in'], intent=th_dict['in'], stack=stack_dict['in']) gm1_params = gm_db.query(env=cur_env, vbs=vb_gm - vtail, vds=vmid - vtail, vgs=vg - vtail) load_db.set_dsn_params(w=w_dict['load'], intent=th_dict['load'], stack=stack_dict['diode']) diode1_params = load_db.query(env=cur_env, vbs=0, vds=vmid - vb_load, vgs=vmid - vb_load) diode2_params = load_db.query(env=cur_env, vbs=0, vds=vout - vb_load, vgs=vmid - vb_load) load_db.set_dsn_params(stack=stack_dict['ngm']) ngm1_params = load_db.query(env=cur_env, vbs=0, vds=vmid - vb_load, vgs=vmid - vb_load) ngm2_params = load_db.query(env=cur_env, vbs=0, vds=vout - vb_load, vgs=vmid - vb_load) cir = LTICircuit() # stage 1 cir.add_transistor(tail1_params, 'tail', 'gnd', 'gnd', 'gnd', fg=seg_dict['tail1'], neg_cap=neg_cap) cir.add_transistor(gm1_params, 'midp', 'inn', 'tail', 'gnd', fg=seg_dict['in'], neg_cap=neg_cap) cir.add_transistor(gm1_params, 'midn', 'inp', 'tail', 'gnd', fg=seg_dict['in'], neg_cap=neg_cap) cir.add_transistor(diode1_params, 'midp', 'midp', 'gnd', 'gnd', fg=seg_dict['diode1'], neg_cap=neg_cap) cir.add_transistor(diode1_params, 'midn', 'midn', 'gnd', 'gnd', fg=seg_dict['diode1'], neg_cap=neg_cap) cir.add_transistor(ngm1_params, 'midn', 'midp', 'gnd', 'gnd', fg=seg_dict['ngm1'], neg_cap=neg_cap) cir.add_transistor(ngm1_params, 'midp', 'midn', 'gnd', 'gnd', fg=seg_dict['ngm1'], neg_cap=neg_cap) # stage 2 cir.add_transistor(tail2_params, 'outp', 'gnd', 'gnd', 'gnd', fg=seg_dict['tail2'], neg_cap=neg_cap) cir.add_transistor(tail2_params, 'outn', 'gnd', 'gnd', 'gnd', fg=seg_dict['tail2'], neg_cap=neg_cap) cir.add_transistor(diode2_params, 'outp', 'midn', 'gnd', 'gnd', fg=seg_dict['diode2'], neg_cap=neg_cap) cir.add_transistor(diode2_params, 'outn', 'midp', 'gnd', 'gnd', fg=seg_dict['diode2'], neg_cap=neg_cap) cir.add_transistor(ngm2_params, 'outp', 'midn', 'gnd', 'gnd', fg=seg_dict['ngm2'], neg_cap=neg_cap) cir.add_transistor(ngm2_params, 'outn', 'midp', 'gnd', 'gnd', fg=seg_dict['ngm2'], neg_cap=neg_cap) # parasitic cap cir.add_cap(cpar1, 'midp', 'gnd') cir.add_cap(cpar1, 'midn', 'gnd') # load cap cir.add_cap(cload, 'outp', 'gnd') cir.add_cap(cload, 'outn', 'gnd') # feedback resistors if not no_fb: cir.add_conductance(gz, 'xp', 'midn') cir.add_conductance(gz, 'xn', 'midp') # diff-to-single conversion cir.add_vcvs(0.5, 'inp', 'gnd', 'in', 'gnd') cir.add_vcvs(-0.5, 'inn', 'gnd', 'in', 'gnd') cir.add_vcvs(1, 'out', 'gnd', 'outp', 'outn') return cir
def design_preamp_chain(db_n, db_p, lch, w_finger, vdd, cload, vin_signal, vin_shift, vtail_res, gain_min, fbw_min, vb_p, vb_n, itail_max=10e-6, error_tol=0.01): """ Designs a preamplifier to go between the comparator and TIA. N-input differential pair with an active PMOS load. Inputs: db_n/p: Database with NMOS/PMOS info lch: Float. Channel length (m) w_finger: Float. Width of a single finger (m) vdd: Float. Supply voltage (V) cload: Float. Load capacitance (F) vin_signal: Float. Input bias voltage (V). vin_shift: Float. Gate bias voltage for the non-signal-facing input device (V). vtail_res: Float. Resolution for tail voltage of amplifying and offset devices for sweep (V). gain_min: Float. Minimum gain (V/V) fbw_min: Float. Minimum bandwidth (Hz) vb_n/p: Float. Body/back-gate voltage (V) of NMOS and PMOS devices. itail_max: Float. Max allowable tail current of the main amplifier (A). error_tol: Float. Fractional tolerance for ibias error when computing device ratios. Raises: ValueError if there is no solution given the requirements. Outputs: nf_in: Integer. Number of fingers for input devices. nf_tail: Integer. Number of fingers for tail device. nf_load: Integer. Number of fingers for active load devices. itail: Float. Tail current of main amp (A). gain: Float. Amp gain (V/V). fbw: Float. Bandwidth (Hz). cin: Float. Approximation of input cap (F). vtail: Float. Tail voltage (V). """ vincm = (vin_signal + vin_shift) / 2 voutcm = vincm best_op = None best_itail = np.inf vthn = estimate_vth(db_n, vdd, 0) vtail_min = 0.15 # TODO: Avoid hardcoding? vtail_max = vincm vtail_vec = np.arange(vtail_min, vtail_max, vtail_res) for vtail in vtail_vec: print('VTAIL:\t{0}'.format(vtail)) op_tail = db_n.query(vgs=voutcm, vds=vtail, vbs=vb_n) op_in = db_n.query(vgs=vincm - vtail, vds=voutcm - vtail, vbs=vb_n - vtail) op_load = db_p.query(vgs=voutcm - vdd, vds=voutcm - vdd, vbs=vb_p - vdd) nf_in_min = 2 nf_in_max = min(100, int(round(.5 * itail_max / op_in['ibias']))) nf_in_vec = np.arange(nf_in_min, nf_in_max, 2) for nf_in in nf_in_vec: # Check if those bias points are feasible given the # device sizing quantization load_ratio_good, nf_load = verify_ratio(op_in['ibias'], op_load['ibias'], nf_in, error_tol) if not load_ratio_good: continue tail_ratio_good, nf_tail_half = verify_ratio( op_in['ibias'], op_tail['ibias'], nf_in, error_tol) nf_tail = nf_tail_half * 2 if not tail_ratio_good: continue # Check if it's burning more power than previous solutions itail = op_tail['ibias'] * nf_tail if itail > best_itail: break # Check the half circuit for small signal parameters half_circuit = LTICircuit() half_circuit.add_transistor(op_in, 'out', 'in', 'tail', fg=nf_in) half_circuit.add_transistor(op_load, 'out', 'gnd', 'gnd', fg=nf_load) half_circuit.add_transistor(op_tail, 'tail', 'gnd', 'gnd', fg=nf_tail_half) half_circuit.add_cap(cload, 'out', 'gnd') num, den = half_circuit.get_num_den(in_name='in', out_name='out', in_type='v') gain = abs(num[-1] / den[-1]) if gain < gain_min: print("(FAIL) GAIN:\t{0}".format(gain)) break wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) if fbw < fbw_min: print("(FAIL) BW:\t{0}".format(fbw)) continue cin = nf_in * (op_in['cgs'] + op_in['cgd'] * (1 + gain)) best_itail = itail best_op = dict(nf_in=nf_in, nf_tail=nf_tail, nf_load=nf_load, itail=itail, gain=gain, fbw=fbw, vtail=vtail, cin=cin) if best_op == None: raise ValueError("No viable solutions.") return best_op
def characterize_casc_amp(env_list, lch, intent_list, fg_list, w_list, db_list, vbias_list, vload_list, vtail_list, vmid_list, vincm, voutcm, vdd, vin_clip, clip_ratio_min, cw, rw, fanout, ton, k_settle_targ, scale_min=0.25, scale_max=20): # compute DC transfer function curve and compute linearity spec ndb, pdb = db_list[0], db_list[-1] results = solve_casc_diff_dc(env_list, ndb, pdb, lch, intent_list, w_list, fg_list, vbias_list, vload_list, vtail_list, vmid_list, vdd, vincm, voutcm, vin_clip, clip_ratio_min, num_points=20) vmat_list, ratio_list, gain_list = results # compute settling ratio fg_tail, fg_in, fg_casc, fg_load = fg_list db_tail, db_in, db_casc, db_load = db_list w_tail, w_in, w_casc, w_load = w_list fzin = 1.0 / (2 * ton) wzin = 2 * np.pi * fzin tvec = np.linspace(0, ton, 200, endpoint=True) scale_list = [] cin_list = [] for env, vbias, vload, vtail, vmid, vmat in zip(env_list, vbias_list, vload_list, vtail_list, vmid_list, vmat_list): # step 1: get half circuit parameters and compute input capacitance in_par = db_in.query(env=env, w=w_in, vbs=-vtail, vds=vmid - vtail, vgs=vincm - vtail) casc_par = db_casc.query(env=env, w=w_casc, vbs=-vmid, vds=voutcm - vmid, vgs=vdd - vmid) load_par = db_load.query(env=env, w=w_load, vbs=0, vds=voutcm - vdd, vgs=vload - vdd) cir = LTICircuit() cir.add_transistor(in_par, 'mid', 'in', 'gnd', fg=fg_in) cir.add_transistor(casc_par, 'out', 'gnd', 'mid', fg=fg_casc) cir.add_transistor(load_par, 'out', 'gnd', 'gnd', fg=fg_load) zin = cir.get_impedance('in', fzin) cin = (1 / zin).imag / wzin cin_list.append(cin) # step 3A: construct differential circuit with vin_clip / 2. vts, vmps, vmns, vops, vons = vmat[-1, :] vinp = vincm + vin_clip / 4 vinn = vincm - vin_clip / 4 tail_par = db_tail.query(env=env, w=w_tail, vbs=0, vds=vts, vgs=vbias) inp_par = db_in.query(env=env, w=w_in, vbs=-vts, vds=vmns - vts, vgs=vinp - vts) inn_par = db_in.query(env=env, w=w_in, vbs=-vts, vds=vmps - vts, vgs=vinn - vts) cascp_par = db_casc.query(env=env, w=w_casc, vbs=-vmns, vds=vons - vmns, vgs=vdd - vmns) cascn_par = db_casc.query(env=env, w=w_casc, vbs=-vmps, vds=vops - vmps, vgs=vdd - vmps) loadp_par = db_load.query(env=env, w=w_load, vbs=0, vds=vons - vdd, vgs=vload - vdd) loadn_par = db_load.query(env=env, w=w_load, vbs=0, vds=vops - vdd, vgs=vload - vdd) cir = LTICircuit() cir.add_transistor(tail_par, 'tail', 'gnd', 'gnd', fg=fg_tail) cir.add_transistor(inp_par, 'midn', 'inp', 'tail', fg=fg_in) cir.add_transistor(inn_par, 'midp', 'inn', 'tail', fg=fg_in) cir.add_transistor(cascp_par, 'dn', 'gnd', 'midn', fg=fg_casc) cir.add_transistor(cascn_par, 'dp', 'gnd', 'midp', fg=fg_casc) cir.add_transistor(loadp_par, 'dn', 'gnd', 'gnd', fg=fg_load) cir.add_transistor(loadn_par, 'dp', 'gnd', 'gnd', fg=fg_load) cir.add_vcvs(0.5, 'inp', 'gnd', 'inac', 'gnd') cir.add_vcvs(-0.5, 'inn', 'gnd', 'inac', 'gnd') # step 3B: compute DC gain cir.add_vcvs(1, 'dac', 'gnd', 'dp', 'dn') dc_tf = cir.get_transfer_function('inac', 'dac') _, gain_vec = dc_tf.freqresp(w=[0.1]) dc_gain = gain_vec[0].real # step 3C add cap loading cload = fanout * cin cir.add_cap(cload, 'outp', 'gnd') cir.add_cap(cload, 'outn', 'gnd') cir.add_vcvs(1, 'outac', 'gnd', 'outp', 'outn') # step 4: find scale factor to achieve k_settle # noinspection PyUnresolvedReferences scale_swp = np.arange(scale_min, scale_max, 0.25).tolist() max_kset = 0 opt_scale = 0 for cur_scale in scale_swp: # add scaled wired parasitics cap_cur = cw / 2 / cur_scale res_cur = rw * cur_scale cir.add_cap(cap_cur, 'dp', 'gnd') cir.add_cap(cap_cur, 'dn', 'gnd') cir.add_cap(cap_cur, 'outp', 'gnd') cir.add_cap(cap_cur, 'outn', 'gnd') cir.add_res(res_cur, 'dp', 'outp') cir.add_res(res_cur, 'dn', 'outn') # get settling factor sys = cir.get_state_space('inac', 'outac') _, yvec = scipy.signal.step( sys, T=tvec) # type: Tuple[np.ndarray, np.ndarray] k_settle_cur = yvec[-1] / dc_gain # print('scale = %.4g, k_settle = %.4g' % (cur_scale, k_settle_cur)) # update next scale factor if k_settle_cur > max_kset: max_kset = k_settle_cur opt_scale = cur_scale else: # k_settle is decreasing, break break if max_kset >= k_settle_targ: break # remove wire parasitics cir.add_cap(-cap_cur, 'dp', 'gnd') cir.add_cap(-cap_cur, 'dn', 'gnd') cir.add_cap(-cap_cur, 'outp', 'gnd') cir.add_cap(-cap_cur, 'outn', 'gnd') cir.add_res(-res_cur, 'dp', 'outp') cir.add_res(-res_cur, 'dn', 'outn') if max_kset < k_settle_targ: raise ValueError('%s max kset = %.4g @ scale = %.4g, ' 'cannot meet settling time spec.' % (env, max_kset, opt_scale)) scale_list.append(opt_scale) return vmat_list, ratio_list, gain_list, scale_list, cin_list
def _get_ss_lti(self, op_dict: Mapping[str, Any], nf_dict: Mapping[str, int], cload: float): ''' Inputs: op_dict: Dictionary of queried database operating points. nf_dict: Dictionary of number of fingers for devices. cload: Load capacitance in farads. Return: gain: Gain from the input to output in V/V fbw: 3dB bandwidth in Hz ''' in_d = 'd' if nf_dict['opp'] > 0 else 'gnd' ckt = LTICircuit() ckt.add_cap(cload, 'out', 'gnd') ckt.add_transistor(op_dict['in'], in_d, 'in', 'out', fg=nf_dict['in'], neg_cap=False) ckt.add_transistor(op_dict['same'], 'out', 'gnd', 'gnd', fg=nf_dict['same'], neg_cap=False) if nf_dict['opp'] > 0: ckt.add_transistor(op_dict['opp'], 'd', 'gnd', 'gnd', fg=nf_dict['opp'], neg_cap=False) num, den = ckt.get_num_den(in_name='in', out_name='out', in_type='v') gain = num[-1] / den[-1] wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) return gain, fbw
def design_inv_tia(specs, pch_op_info, nch_op_info, pch_scale): sim_env = specs['sim_env'] pch_db = specs['pch_db'] nch_db = specs['nch_db'] vgs_res = specs['vgs_res'] vdd = specs['vdd'] isw = specs['isw'] ber_targ = specs['ber'] voff = specs['voff'] snr_like_targ = specs['snr_like'] pmos_noise_scale = specs['pmos_noise_scale'] # rdc_targ = specs['rdc'] noise_const = specs['noise_const'] tper = specs['tper'] f_factor = specs['f_factor'] # f3db_targ = specs['f3db'] f3db_targ = f_factor * 1 / tper pm_targ = specs['pm'] cin = specs['cin'] cl = specs['cl'] scale_min = specs['scale_min'] scale_max = specs['scale_max'] n_scale = specs['n_scale'] rf_min = specs['rf_min'] rf_max = specs['rf_max'] n_rf = specs['n_rf'] pch_ibias = pch_op_info['ibias'] nch_ibias = nch_op_info['ibias'] gmp = pch_op_info['gm'] * pch_scale gmn = nch_op_info['gm'] gm_tot = gmp + gmn gammap = pch_op_info['gamma'] gamman = nch_op_info['gamma'] gdsp = pch_op_info['gds'] * pch_scale gdsn = nch_op_info['gds'] rop = 1 / gdsp ron = 1 / gdsn ro_tot = rop * ron / (rop + ron) gds_tot = 1 / ro_tot cgsp = pch_op_info['cgs'] * pch_scale cgsn = nch_op_info['cgs'] cgs_tot = cgsp + cgsn cgbp = pch_op_info['cgb'] * pch_scale cgbn = nch_op_info['cgb'] cgb_tot = cgbp + cgbn cgdp = pch_op_info['cgd'] * pch_scale cgdn = nch_op_info['cgd'] cgd_tot = cgdp + cgdn cggp = pch_op_info['cgg'] * pch_scale cggn = nch_op_info['cgg'] cgg_tot = cggp + cggn cdsp = pch_op_info['cds'] * pch_scale cdsn = nch_op_info['cds'] cds_tot = cdsp + cdsn cdbp = pch_op_info['cdb'] * pch_scale cdbn = nch_op_info['cdb'] cdb_tot = cdbp + cdbn cddp = pch_op_info['cdd'] * pch_scale cddn = nch_op_info['cdd'] cdd_tot = cddp + cddn scale_vec = np.linspace(scale_min, scale_max, n_scale) for scale in scale_vec: for rf in np.linspace(rf_max, rf_min, n_rf): # Build circuit cir = LTICircuit() cir.add_transistor(pch_op_info, 'out', 'in', 'gnd', 'gnd', fg=scale * pch_scale) cir.add_transistor(nch_op_info, 'out', 'in', 'gnd', 'gnd', fg=scale) cir.add_res(rf, 'out', 'in') cir.add_cap(cin, 'in', 'gnd') cir.add_cap(cl, 'out', 'gnd') # Get gain/poles/zeros/Bode plot # Note: any in_type other than 'v' results in current source input tf = cir.get_transfer_function('in', 'out', in_type='i') rdc = np.absolute(tf.num[-1] / tf.den[-1]) w3db = get_w_3db(tf.num, tf.den) f3db = w3db / (2 * np.pi) if f3db >= f3db_targ: # Noise in_gmp = noise_const * gammap * gmp in_gmn = noise_const * gamman * gmn in_rf = noise_const / rf tf_gm = cir.get_transfer_function('out', 'out', in_type='i') dc_gm = tf_gm.num[-1] / tf_gm.den[-1] wo2_gm = tf_gm.den[-1] / tf_gm.den[0] woQ_gm = tf_gm.den[-1] / tf_gm.den[1] wz_gm = tf_gm.num[-1] / tf_gm.num[0] von_gmp = pmos_noise_scale * in_gmp * dc_gm**2 * woQ_gm / 4 * ( wo2_gm / wz_gm**2 + 1) von_gmn = in_gmn * dc_gm**2 * woQ_gm / 4 * (wo2_gm / wz_gm**2 + 1) cir.add_vccs(1, 'out', 'in', 'vn', 'gnd') tf_rf = cir.get_transfer_function('vn', 'out', in_type='v') dc_rf = tf_rf.num[-1] / tf_rf.den[-1] wo2_rf = tf_rf.den[-1] / tf_rf.den[0] woQ_rf = tf_rf.den[-1] / tf_rf.den[1] wz_rf = tf_rf.num[-1] / tf_rf.num[0] von_rf = in_rf * dc_rf**2 * woQ_rf / 4 * (wo2_rf / wz_rf**2 + 1) von = von_gmp + von_gmn + von_rf # Signal vo = isw * rdc snr_like = (vo - voff) / np.sqrt(von) if snr_like >= snr_like_targ: cin_tot = cin + cgg_tot * scale cl_tot = cl + cdd_tot * scale cgd_scaled = cgd_tot * scale gm_scaled = gm_tot * scale gds_scaled = gds_tot * scale # Build open loop circuit for phase margin cir_open_loop = LTICircuit() cir_open_loop.add_vccs(gm_scaled, 'out', 'gnd', 'vt', 'gnd') # *** cir_open_loop.add_conductance(gds_scaled, 'out', 'gnd') cir_open_loop.add_cap(cl_tot, 'out', 'gnd') cir_open_loop.add_res(rf, 'out', 'vr') cir_open_loop.add_cap(cgd_scaled, 'out', 'vr') cir_open_loop.add_cap(cin_tot, 'vr', 'gnd') tf_open_loop = cir_open_loop.get_transfer_function( 'vt', 'vr', in_type='v') pm, gm = get_stability_margins(tf_open_loop.num, tf_open_loop.den) pm = pm - 180 if pm >= pm_targ: ibias = scale * nch_ibias design = dict(IB=ibias, Scale=scale, Rf=rf, RDC=rdc, f3dB=f3db, PM=pm, Von_Rf=von_rf, Von_gmp=von_gmp, Von_gmn=von_gmn, Von=von, SNR_Like=snr_like) return design
def _get_loadreg_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource, vout, iout) -> float: ''' Returns: loadreg: Load regulation for peak-to-peak load current variation of 20% (V/V) ''' n_ser = ser_type == 'n' n_amp = amp_in == 'n' vdd = 'vdd' if rsource != 0 else 'gnd' # Supply -> output gain ckt = LTICircuit() ser_d = vdd if n_ser else 'reg' ser_s = 'reg' if n_ser else vdd inp_conn = 'gnd' if n_ser else 'reg' inn_conn = 'reg' if n_ser else 'gnd' tail_rail = 'gnd' if n_amp else vdd load_rail = vdd if n_amp else 'gnd' # Passives if rsource != 0: ckt.add_res(rsource, 'gnd', 'vdd') ckt.add_cap(cload, 'reg', 'gnd') ckt.add_cap(cdecap_amp, 'out', 'reg') # Series device ckt.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) # Amplifier ckt.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) num, den = ckt.get_num_den(in_name='reg', out_name='reg', in_type='i') transimpedance = num[-1]/den[-1] loadreg = transimpedance*0.2*iout/vout return loadreg
def _get_stb_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource) -> float: ''' Returns: pm: Phase margin (degrees) ''' ckt = LTICircuit() n_ser = ser_type == 'n' n_amp = amp_in == 'n' vdd = 'vdd' if rsource != 0 else 'gnd' # Series device ser_d = vdd if n_ser else 'reg' ser_s = 'reg' if n_ser else vdd ckt.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) # Passives ckt.add_cap(cload, 'reg', 'gnd') ckt.add_cap(cdecap_amp, 'out', 'reg') if rsource != 0: ckt.add_res(rsource, 'gnd', 'vdd') # Amplifier tail_rail = 'gnd' if n_amp else vdd load_rail = vdd if n_amp else 'gnd' inp_conn = 'gnd' if n_ser else 'amp_in' inn_conn = 'gnd' if not n_ser else 'amp_in' ckt.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) # Calculating stability margins num, den = ckt.get_num_den(in_name='amp_in', out_name='reg', in_type='v') pm, _ = get_stability_margins(np.convolve(num, [-1]), den) return pm
def _get_psrr_lti(self, op_dict, nf_dict, ser_type, amp_in, cload, cdecap_amp, rsource) -> float: ''' Returns: psrr: PSRR (dB) fbw: Power supply -> output 3dB bandwidth (Hz) ''' n_ser = ser_type == 'n' n_amp = amp_in == 'n' # Supply -> output gain ckt_sup = LTICircuit() ser_d = 'vdd' if n_ser else 'reg' ser_s = 'reg' if n_ser else 'vdd' inp_conn = 'gnd' if n_ser else 'reg' inn_conn = 'reg' if n_ser else 'gnd' tail_rail = 'gnd' if n_amp else 'vdd' load_rail = 'vdd' if n_amp else 'gnd' # Passives if rsource != 0: ckt_sup.add_res(rsource, 'vbat', 'vdd') ckt_sup.add_cap(cload, 'reg', 'gnd') ckt_sup.add_cap(cdecap_amp, 'out', 'reg') # Series device ckt_sup.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) # Amplifier ckt_sup.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt_sup.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt_sup.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False) ckt_sup.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) ckt_sup.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) if rsource == 0: num_sup, den_sup = ckt_sup.get_num_den(in_name='vdd', out_name='reg', in_type='v') else: num_sup, den_sup = ckt_sup.get_num_den(in_name='vbat', out_name='reg', in_type='v') gain_sup = num_sup[-1]/den_sup[-1] wbw_sup = get_w_3db(den_sup, num_sup) if gain_sup == 0: return float('inf') if wbw_sup == None: wbw_sup = 0 fbw_sup = wbw_sup / (2*np.pi) psrr = 10*np.log10((1/gain_sup)**2) return psrr, fbw_sup
def _get_loopgain_lti(self, op_dict, nf_dict, ser_type, amp_in, rsource) -> float: ''' Returns: A: DC loop gain ''' ckt = LTICircuit() n_ser = ser_type == 'n' n_amp = amp_in == 'n' vdd = 'vdd' if rsource != 0 else 'gnd' # Series device ser_d = vdd if n_ser else 'reg' ser_s = 'reg' if n_ser else vdd ckt.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=True) # Passives if rsource != 0: ckt.add_res(rsource, 'gnd', 'vdd') # Amplifier tail_rail = 'gnd' if n_amp else vdd load_rail = vdd if n_amp else 'gnd' inp_conn = 'gnd' if n_ser else 'amp_in' inn_conn = 'gnd' if not n_ser else 'amp_in' ckt.add_transistor(op_dict['amp_in'], 'outx', inp_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_in'], 'out', inn_conn, 'tail', fg=nf_dict['amp_in'], neg_cap=False) ckt.add_transistor(op_dict['amp_tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['amp_tail'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'outx', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) ckt.add_transistor(op_dict['amp_load'], 'out', 'outx', load_rail, fg=nf_dict['amp_load'], neg_cap=False) # Calculating stability margins num, den = ckt.get_num_den(in_name='amp_in', out_name='reg', in_type='v') A = num[-1]/den[-1] return A
def construct_feedback(n_op, p_op, tail_op, nf_n, nf_p, nf_tail, cload, cn=165e-15, cp=83e-15, r=70.71e3): ''' Inputs: p/n/tail_op: Operating point information for a given device. NMOS (input), PMOS (active load), or tail device. nf_n/p/tail: Integer. Number of minimum channel width/length devices in parallel. cload: Float. Load capacitance in farads. cn: Float. Capacitance in farads attached to the negative input terminal of the amplifier for the filter. cp: Float. Capacitance in farads attached to the positive input terminal of the amplifier for the filter. r: Float. Resistance in ohms used external to the amplifier for the filter. Outputs: Returns the LTICircuit constructed for closed-loop analysis of the amplifier with the loop broken. ''' ckt = LTICircuit() # Left side ckt.add_transistor(n_op, 'out_copy', 'inp', 'tail', fg=nf_n) ckt.add_transistor(p_op, 'out_copy', 'out_copy', 'gnd', fg=nf_p) # Right side ckt.add_transistor(n_op, 'out', 'inn', 'tail', fg=nf_n) ckt.add_transistor(p_op, 'out', 'out_copy', 'gnd', fg=nf_p) # Tail ckt.add_transistor(tail_op, 'tail', 'gnd', 'gnd', fg=nf_tail) # Adding additional passives ckt.add_cap(cload, 'out', 'gnd') ckt.add_cap(cn, 'inn', 'rmid') ckt.add_cap(cp, 'inp', 'gnd') ckt.add_res(r, 'in', 'rmid') ckt.add_res(r, 'rmid', 'inp') return ckt
def _get_psrr_lti(self, op_dict, nf_dict, series_type, amp_in, rload, cload) -> float: ''' Outputs: psrr: PSRR (dB) fbw: Power supply -> output 3dB bandwidth (Hz) ''' n_ser = series_type == 'n' n_amp = amp_in == 'n' # Supply -> output gain ckt_sup = LTICircuit() ser_d = 'vdd' if n_ser else 'reg' ser_s = 'reg' if n_ser else 'vdd' inp_conn = 'gnd' if n_ser else 'reg' inn_conn = 'reg' if n_ser else 'gnd' tail_rail = 'gnd' if n_amp else 'vdd' load_rail = 'vdd' if n_amp else 'gnd' ckt_sup.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) ckt_sup.add_res(rload, 'reg', 'gnd') ckt_sup.add_cap(rload, 'reg', 'gnd') ckt_sup.add_transistor(op_dict['in'], 'outx', inp_conn, 'tail', fg=nf_dict['in'], neg_cap=False) ckt_sup.add_transistor(op_dict['in'], 'out', inn_conn, 'tail', fg=nf_dict['in'], neg_cap=False) ckt_sup.add_transistor(op_dict['tail'], 'tail', 'gnd', tail_rail, fg=nf_dict['tail'], neg_cap=False) ckt_sup.add_transistor(op_dict['load'], 'outx', 'outx', load_rail, fg=nf_dict['load'], neg_cap=False) ckt_sup.add_transistor(op_dict['load'], 'out', 'outx', load_rail, fg=nf_dict['load'], neg_cap=False) num_sup, den_sup = ckt_sup.get_num_den(in_name='vdd', out_name='reg', in_type='v') gain_sup = num_sup[-1] / den_sup[-1] wbw_sup = get_w_3db(num_sup, den_sup) # Reference -> output gain # ckt_norm = LTICircuit() # ser_d = 'gnd' if n_ser else 'reg' # ser_s = 'reg' if n_ser else 'gnd' # inp_conn = 'in' if n_ser else 'reg' # inn_conn = 'reg' if n_ser else 'in' # ckt_norm.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) # ckt_norm.add_res(rload, 'reg', 'gnd') # ckt_norm.add_cap(rload, 'reg', 'gnd') # ckt_norm.add_transistor(op_dict['in'], 'outx', inp_conn, 'tail', fg=nf_dict['in'], neg_cap=False) # ckt_norm.add_transistor(op_dict['in'], 'out', inn_conn, 'tail', fg=nf_dict['in'], neg_cap=False) # ckt_norm.add_transistor(op_dict['tail'], 'tail', 'gnd', 'gnd', fg=nf_dict['tail'], neg_cap=False) # ckt_norm.add_transistor(op_dict['load'], 'outx', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False) # ckt_norm.add_transistor(op_dict['load'], 'out', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False) # num_norm, den_norm = ckt_norm.get_num_den(in_name='in', out_name='reg', in_type='v') # gain_norm = num_norm[-1]/den_norm[-1] if gain_sup == 0: return float('inf') if wbw_sup == None: wbw_sup = 0 fbw_sup = wbw_sup / (2 * np.pi) psrr = 10 * np.log10((1 / gain_sup)**2) return psrr, fbw_sup
def design_inv_tia(specs, pch_op_info, nch_op_info, pch_scale): sim_env = specs['sim_env'] pch_db = specs['pch_db'] nch_db = specs['nch_db'] vgs_res = specs['vgs_res'] vdd = specs['vdd'] rdc_targ = specs['rdc'] f3db_targ = specs['f3db'] pm_targ = specs['pm'] cin = specs['cin'] cl = specs['cl'] scale_min = specs['scale_min'] scale_max = specs['scale_max'] n_scale = specs['n_scale'] rf_min = specs['rf_min'] rf_max = specs['rf_max'] n_rf = specs['n_rf'] pch_ibias = pch_op_info['ibias'] nch_ibias = nch_op_info['ibias'] gmp = pch_op_info['gm'] * pch_scale gmn = nch_op_info['gm'] gm_tot = gmp + gmn gdsp = pch_op_info['gds'] * pch_scale gdsn = nch_op_info['gds'] rop = 1 / gdsp ron = 1 / gdsn ro_tot = rop * ron / (rop + ron) gds_tot = 1 / ro_tot cgsp = pch_op_info['cgs'] * pch_scale cgsn = nch_op_info['cgs'] cgs_tot = cgsp + cgsn cgbp = pch_op_info['cgb'] * pch_scale cgbn = nch_op_info['cgb'] cgb_tot = cgbp + cgbn cgdp = pch_op_info['cgd'] * pch_scale cgdn = nch_op_info['cgd'] cgd_tot = cgdp + cgdn cggp = pch_op_info['cgg'] * pch_scale cggn = nch_op_info['cgg'] cgg_tot = cggp + cggn cdsp = pch_op_info['cds'] * pch_scale cdsn = nch_op_info['cds'] cds_tot = cdsp + cdsn cdbp = pch_op_info['cdb'] * pch_scale cdbn = nch_op_info['cdb'] cdb_tot = cdbp + cdbn cddp = pch_op_info['cdd'] * pch_scale cddn = nch_op_info['cdd'] cdd_tot = cddp + cddn scale_vec = np.linspace(scale_min, scale_max, n_scale) for scale in scale_vec: for rf in np.linspace(rf_min, rf_max, n_rf): # Build circuit cir = LTICircuit() cir.add_transistor(pch_op_info, 'out', 'in', 'gnd', 'gnd', fg=scale * pch_scale) cir.add_transistor(nch_op_info, 'out', 'in', 'gnd', 'gnd', fg=scale) cir.add_res(rf, 'out', 'in') cir.add_cap(cin, 'in', 'gnd') cir.add_cap(cl, 'out', 'gnd') # Get gain/poles/zeros/Bode plot # Note: any in_type other than 'v' results in current source input tf = cir.get_transfer_function('in', 'out', in_type='i') rdc = np.absolute(tf.num[-1] / tf.den[-1]) w3db = get_w_3db(tf.num, tf.den) f3db = w3db / (2 * np.pi) cin_tot = cin + cgg_tot * scale cl_tot = cl + cdd_tot * scale cgd_scaled = cgd_tot * scale gm_scaled = gm_tot * scale gds_scaled = gds_tot * scale cir_open_loop = LTICircuit() cir_open_loop.add_vccs(gm_scaled, 'out', 'gnd', 'vt', 'gnd') cir_open_loop.add_conductance(gds_scaled, 'out', 'gnd') cir_open_loop.add_cap(cl_tot, 'out', 'gnd') cir_open_loop.add_res(rf, 'out', 'vr') cir_open_loop.add_cap(cgd_scaled, 'out', 'vr') cir_open_loop.add_cap(cin_tot, 'vr', 'gnd') tf_open_loop = cir_open_loop.get_transfer_function('vt', 'vr', in_type='v') pm, gain_margin = get_stability_margins(tf_open_loop.num, tf_open_loop.den) pm = pm - 180 if rdc >= rdc_targ and f3db >= f3db_targ and pm >= pm_targ: ibias = scale * nch_ibias design = dict(IB=ibias, Scale=scale, Rf=rf, RDC=rdc, f3dB=f3db, PM=pm) return design
def _get_stb_lti(self, op_dict, nf_dict, series_type, rload, cload) -> float: ''' Returns: pm: Phase margins (in degrees) ''' ckt = LTICircuit() n_ser = series_type == 'n' # Series device ser_d = 'gnd' if n_ser else 'reg' ser_s = 'reg' if n_ser else 'gnd' ckt.add_transistor(op_dict['ser'], ser_d, 'out', ser_s, fg=nf_dict['ser'], neg_cap=False) # Load passives ckt.add_res(rload, 'reg', 'gnd') ckt.add_cap(rload, 'reg', 'gnd') # TODO include any compensation passives # Amplifier inp_conn = 'gnd' if n_ser else 'in' inn_conn = 'gnd' if not n_ser else 'in' ckt.add_transistor(op_dict['in'], 'outx', inp_conn, 'tail', fg=nf_dict['in'], neg_cap=False) ckt.add_transistor(op_dict['in'], 'out', inn_conn, 'tail', fg=nf_dict['in'], neg_cap=False) ckt.add_transistor(op_dict['tail'], 'tail', 'gnd', 'gnd', fg=nf_dict['tail'], neg_cap=False) ckt.add_transistor(op_dict['load'], 'outx', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False) ckt.add_transistor(op_dict['load'], 'out', 'outx', 'gnd', fg=nf_dict['load'], neg_cap=False) # Calculating stability margins num, den = ckt.get_num_den(in_name='in', out_name='reg', in_type='v') pm, _ = get_stability_margins(np.convolve(num, [-1]), den) return pm
def design_load(specs, db, ax_list=None, label=None): sim_env = specs['sim_env'] vgs_res = specs['vgs_res'] vdd = specs['vdd'] voutcm = specs['voutcm'] vstar_in = specs['vstar_in'] vstar_load = specs['vstar_load'] casc_scale = specs['casc_scale'] vgs_idx = db.get_fun_arg_index('vgs') vstar_fun = db.get_function('vstar', env=sim_env) ibias_fun = db.get_function('ibias', env=sim_env) vgs_min, vgs_max = vstar_fun.get_input_range(vgs_idx) def zero_fun1(vm, vgs): arg = db.get_fun_arg(vgs=vgs, vds=vm-vdd, vbs=0) return (vstar_fun(arg) - vstar_load) * 1e3 def zero_fun2(vc, vm, itarg): arg = db.get_fun_arg(vgs=vc-vm, vds=voutcm-vm, vbs=vdd-vm) return (ibias_fun(arg) * casc_scale - itarg) * 1e6 num_pts = int(np.ceil((vgs_max - vgs_min) / vgs_res)) + 1 vgs_list, vds_list, vcasc_list, ibias_list, ro_list = [], [], [], [], [] p1_list, p2_list, gain_list = [], [], [] for vgs_val in np.linspace(vgs_min, vgs_max, num_pts, endpoint=True): fout1 = zero_fun1(vdd - 5e-3, vgs_val) fout2 = zero_fun1(voutcm + 5e-3, vgs_val) if fout1 * fout2 > 0: continue vmid = opt.brentq(zero_fun1, voutcm + 5e-3, vdd - 5e-3, args=(vgs_val,)) if vmid + vgs_max <= 0: continue barg = db.get_fun_arg(vgs=vgs_val, vds=vmid-vdd, vbs=0) bot_op = db.query(vbs=0, vds=vmid-vdd, vgs=vgs_val) ib = bot_op['ibias'] args = (vmid, ib) fout1 = zero_fun2(vmid + vgs_max, *args) fout2 = zero_fun2(0, *args) if fout1 * fout2 > 0: continue vcasc = opt.brentq(zero_fun2, 0, vmid + vgs_max, args=args) top_op = db.query(vbs=vdd-vmid, vds=voutcm-vmid, vgs=vcasc-vmid) cir = LTICircuit() cir.add_transistor(bot_op, 'mid', 'gnd', 'gnd', 'gnd', fg=1) cir.add_transistor(top_op, 'out', 'gnd', 'mid', 'gnd', fg=casc_scale) cur_tf = cir.get_transfer_function('out', 'out', in_type='i') ro = cur_tf.num[-1] / cur_tf.den[-1] p1, p2 = cur_tf.poles p1, p2 = min(-p1, -p2), max(-p1, -p2) p2 /= 2 * np.pi # add approximation from input branch cpar = 1 / (ro * p1) p1 = 1 / (2 * np.pi * 2 * cpar * (ro / 4)) cur_gain = 2 * ib / vstar_in * (ro / 4) vgs_list.append(vgs_val) vds_list.append(vmid-vdd) vcasc_list.append(vcasc) ibias_list.append(ib) ro_list.append(ro * ib) p1_list.append(p1) p2_list.append(p2) gain_list.append(cur_gain) if ax_list is not None: val_list_list = [vds_list, gain_list, p1_list, p2_list] ylabel_list = ['$V_{DS}$ (V)', 'Gain (V/V)', '$p_{1o}$ (GHz)', '$p_{2o}$ (GHz)'] sp_list = [1, 1, 1e-9, 1e-9] for ax, val_list, ylabel, sp in zip(ax_list, val_list_list, ylabel_list, sp_list): ax.plot(vgs_list, np.asarray(val_list) * sp, '-o', label=label) ax.set_ylabel(ylabel) ax_list[-1].set_xlabel('$V_{GS}$ (V)') return vgs_list[0], vds_list[0], vcasc_list[0]
def funity_vs_scale2(op_in, op_load, op_tail, cload, phase_margin, fg=2): s2min = 1 s2max = 40 num_s = 100 cmin = 1e-16 cmax = 1e-9 ctol = 1e-17 cstep = 1e-15 scale_load = op_in['ibias'] / op_load['ibias'] * fg gfb = op_load['gm'] * scale_load s2vec = np.linspace(s2min, s2max, num_s).tolist() f0_list, pm0_list, f1_list, pm1_list, copt_list = [], [], [], [], [] for s2 in s2vec: cir = LTICircuit() cir.add_transistor(op_in, 'mid', 'in', 'gnd', 'gnd', fg=fg) cir.add_transistor(op_load, 'mid', 'gnd', 'gnd', 'gnd', fg=scale_load) cir.add_transistor(op_load, 'out', 'mid', 'gnd', 'gnd', fg=scale_load * s2) cir.add_transistor(op_tail, 'out', 'gnd', 'gnd', 'gnd', fg=fg * s2) cir.add_cap(cload, 'out', 'gnd') num, den = cir.get_num_den('in', 'out') f0_list.append(get_w_crossings(num, den)[0] / (2 * np.pi)) pm0_list.append(get_stability_margins(num, den)[0]) cir.add_conductance(gfb * s2, 'mid', 'x') copt = opt_cfb(phase_margin, cir, cmin, cmax, cstep, ctol) if copt is None: raise ValueError('oops, Cfb is None') cir.add_cap(copt, 'x', 'out') num, den = cir.get_num_den('in', 'out') f1_list.append(get_w_crossings(num, den)[0] / (2 * np.pi)) pm1_list.append(get_stability_margins(num, den)[0]) copt_list.append(copt) f, (ax0, ax1, ax2) = plt.subplots(3, sharex='all') ax0.plot(s2vec, np.array(copt_list) * 1e15) ax0.set_ylabel('Cf (fF)') ax1.plot(s2vec, pm1_list, label='Cf') ax1.plot(s2vec, pm0_list, label='no Cf') ax1.legend() ax1.set_ylabel('$\phi_{PM}$ (deg)') ax2.plot(s2vec, np.array(f1_list) * 1e-9, label='Cf') ax2.plot(s2vec, np.array(f0_list) * 1e-9, label='no Cf') ax2.legend() ax2.set_ylabel('$f_{UG}$ (GHz)') ax2.set_xlabel('$I_2/I_1$') plt.show()
def design_preamp(db_n, db_p, lch, w_finger, vdd, cload, vin_signal, vin_shift, voutcm_res, vtail_res, gain_min, fbw_min, vb_p, vb_n, itail_max=10e-6, error_tol=.01): """ Designs a preamplifier to go between the comparator and TIA. N-input differential pair with an active PMOS load. Inputs: db_n/p: Database with NMOS/PMOS info lch: Float. Channel length (m) w_finger: Float. Width of a single finger (m) vdd: Float. Supply voltage (V) cload: Float. Load capacitance (F) vin_signal: Float. Input bias voltage (V). vin_shift: Float. Gate bias voltage for the non-signal-facing input device (V). voutcm_res: Float. Resolution for output common mode voltage sweep (V). vtail_res: Float. Resolution for tail voltage of amplifying and offset devices for sweep (V). gain_min: Float. Minimum gain (V/V) fbw_min: Float. Minimum bandwidth (Hz) vb_n/p: Float. Body/back-gate voltage (V) of NMOS and PMOS devices. itail_max: Float. Max allowable tail current of the main amplifier (A). error_tol: Float. Fractional tolerance for ibias error when computing device ratios. Raises: ValueError if there is no solution given the requirements. Outputs: nf_in: Integer. Number of fingers for input devices. nf_tail: Integer. Number of fingers for tail device. nf_load: Integer. Number of fingers for active load devices. itail: Float. Tail current of main amp (A). gain: Float. Amp gain (V/V). fbw: Float. Bandwidth (Hz). voutcm: Float. Output common mode voltage (V). cin: Float. Approximation of input cap (F). """ vthn = estimate_vth(db_n, vdd, 0) vthp = estimate_vth(db_p, vdd, 0, mos_type="pmos") vincm = (vin_signal + vin_shift) / 2 vstar_min = .15 # TODO: Avoid hardcoding? vtail_min = vstar_min vtail_max = vin_signal - vstar_min vtail_vec = np.arange(vtail_min, vtail_max, vtail_res) best_op = None best_itail = np.inf for vtail in vtail_vec: voutcm_min = max(vin_signal - vthn, vin_shift - vthn, vtail) voutcm_max = vdd - vstar_min voutcm_vec = np.arange(voutcm_min, voutcm_max, voutcm_res) for voutcm in voutcm_vec: op_tail = db_n.query(vgs=voutcm, vds=vtail, vbs=vb_n) # The "mean" of the input and load devices op_in = db_n.query(vgs=vincm - vtail, vds=voutcm - vtail, vbs=vb_n - vtail) op_load = db_p.query(vgs=vtail - vdd, vds=voutcm - vdd, vbs=vb_p - vdd) # Calculate the max tail size based on current limit # and size input devices accordingly nf_in_min = 2 nf_in_max = int(round(itail_max / 2 / op_in['ibias'])) nf_in_vec = np.arange(nf_in_min, nf_in_max, 1) for nf_in in nf_in_vec: # Matching device ratios to sink the same amount of current # given the bias voltages tail_ratio_good, nf_tail = verify_ratio( op_in['ibias'] / 2, op_tail['ibias'], nf_in, error_tol) if not tail_ratio_good: continue itail = op_tail['ibias'] * nf_tail load_ratio_good, nf_load = verify_ratio( op_in['ibias'], op_load['ibias'], nf_in, error_tol) if not load_ratio_good: continue # Check small signal parameters with symmetric circuit ckt_sym = LTICircuit() ckt_sym.add_transistor(op_in, 'outp', 'gnd', 'tail', fg=nf_in) ckt_sym.add_transistor(op_in, 'outn', 'inp', 'tail', fg=nf_in) ckt_sym.add_transistor(op_tail, 'tail', 'gnd', 'gnd', fg=nf_tail) ckt_sym.add_transistor(op_load, 'outp', 'gnd', 'gnd', fg=nf_load) ckt_sym.add_transistor(op_load, 'outn', 'gnd', 'gnd', fg=nf_load) ckt_sym.add_cap(cload, 'outp', 'gnd') ckt_sym.add_cap(cload, 'outn', 'gnd') num, den = ckt_sym.get_num_den(in_name='inp', out_name='outn', in_type='v') num_unintent, den_unintent = ckt_sym.get_num_den( in_name='inp', out_name='outp', in_type='v') gain_intentional = num[-1] / den[-1] gain_unintentional = num_unintent[-1] / den_unintent[-1] gain = abs(gain_intentional - gain_unintentional) wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) if fbw < fbw_min: # print("(FAIL) BW:\t{0}".format(fbw)) continue if gain < gain_min: # print("(FAIL) GAIN:\t{0}".format(gain)) break print("(SUCCESS1)") if itail > best_itail: break ############################## if False: # Check once again with asymmetric circuit vin_diff = vin_signal - vin_shift voutn = voutcm - gain * vin_diff / 2 voutp = voutcm + gain * vin_diff / 2 op_signal = db_n.query(vgs=vin_signal - vtail, vds=voutn - vtail, vbs=vb_n - vtail) op_shift = db_n.query(vgs=vin_shift - vtail, vds=voutp - vtail, vbs=vb_n - vtail) op_loadsignal = db_p.query(vgs=vtail - vdd, vds=voutn - vdd, vbs=vb_p - vdd) op_loadshift = db_p.query(vgs=vtail - vdd, vds=voutp - vdd, vbs=vb_p - vdd) ckt = LTICircuit() ckt.add_transistor(op_shift, 'outp', 'gnd', 'tail', fg=nf_in) ckt.add_transistor(op_signal, 'outn', 'inp', 'tail', fg=nf_in) ckt.add_transistor(op_tail, 'tail', 'gnd', 'gnd', fg=nf_tail) ckt.add_transistor(op_loadsignal, 'outn', 'gnd', 'gnd', nf_load) ckt.add_transistor(op_loadshift, 'outp', 'gnd', 'gnd', nf_load) ckt.add_cap(cload, 'outn', 'gnd') ckt.add_cap(cload, 'outp', 'gnd') num, den = ckt.get_num_den(in_name='inp', out_name='outn', in_type='v') num_unintent, den_unintent = ckt.get_num_den( in_name='inp', out_name='outp', in_type='v') gain_intentional = num[-1] / den[-1] gain_unintentional = num_unintent[-1] / den_unintent[-1] gain = abs(gain_intentional - gain_unintentional) wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) if fbw < fbw_min: print("(FAIL) BW:\t{0}".format(fbw)) continue if gain < gain_min: print("(FAIL) GAIN:\t{0}".format(gain)) break print("(SUCCESS2)") ################################ op_signal = op_in if itail < best_itail: best_itail = itail best_op = dict(nf_in=nf_in, nf_tail=nf_tail, nf_load=nf_load, itail=itail, gain=gain, fbw=fbw, voutcm=voutcm, vtail=vtail, cin=op_signal['cgg'] * nf_in) if best_op == None: raise ValueError("No viable solutions.") return best_op
def tf_vs_cfb(op_in, op_load, op_tail, cload, fg=2): fvec = np.logspace(6, 11, 1000) cvec = np.logspace(np.log10(5e-16), np.log10(5e-14), 5).tolist() scale_load = op_in['ibias'] / op_load['ibias'] * fg cir = LTICircuit() cir.add_transistor(op_in, 'mid', 'in', 'gnd', 'gnd', fg=fg) cir.add_transistor(op_load, 'mid', 'gnd', 'gnd', 'gnd', fg=scale_load) cir.add_transistor(op_load, 'out', 'mid', 'gnd', 'gnd', fg=scale_load) cir.add_transistor(op_tail, 'out', 'gnd', 'gnd', 'gnd', fg=fg) cir.add_cap(cload, 'out', 'gnd') gfb = op_load['gm'] * scale_load cir.add_conductance(gfb, 'mid', 'x') print('fg_in = %d, fg_load=%.3g, rfb = %.4g' % (fg, scale_load, 1 / gfb)) tf_list, lbl_list = [], [] for cval in cvec: cir.add_cap(cval, 'x', 'out') tf_list.append(cir.get_num_den('in', 'out')) cir.add_cap(-cval, 'x', 'out') lbl_list.append('$C_{f} = %.3g$f' % (cval * 1e15)) plot_tf(fvec, tf_list, lbl_list)
def make_ltickt(self, op_dict: Mapping[str, Any], nf_dict: Mapping[str, int], cload: float, rload: float, meas_side: str) -> LTICircuit: ckt = LTICircuit() inp_conn = 'gnd' if meas_side == 'n' else 'inp' inn_conn = 'gnd' if meas_side == 'p' else 'inn' ckt.add_transistor(op_dict['main_in'], 'main_outn', inp_conn, 'main_tail', fg=nf_dict['main_in']) ckt.add_transistor(op_dict['main_in'], 'main_outp', inn_conn, 'main_tail', fg=nf_dict['main_in']) # ckt.add_transistor(op_dict['main_tail'], 'main_tail', 'cmfb_outp', 'gnd', fg=nf_dict['main_tail']) ckt.add_transistor(op_dict['main_tail'], 'main_tail', 'gnd', 'gnd', fg=nf_dict['main_tail']) ckt.add_res(rload, 'main_outn', 'gnd') ckt.add_res(rload, 'main_outp', 'gnd') ckt.add_cap(cload, 'main_outn', 'gnd') ckt.add_cap(cload, 'main_outp', 'gnd') # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_out1n', 'gnd', 'cmfb_tail', fg=nf_dict['cmfb_in']*2) # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_out1p', 'main_outn', 'cmfb_tail', fg=nf_dict['cmfb_in']) # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_out1p', 'main_outp', 'cmfb_tail', fg=nf_dict['cmfb_in']) # ckt.add_transistor(op_dict['cmfb_tail'], 'cmfb_tail', 'gnd', 'gnd', fg=nf_dict['cmfb_tail']) # ckt.add_transistor(op_dict['cmfb_load'], 'cmfb_out1n', 'cmfb_out1n', 'gnd', fg=nf_dict['cmfb_load']) # ckt.add_transistor(op_dict['cmfb_load'], 'cmfb_out1p', 'cmfb_out1p', 'gnd', fg=nf_dict['cmfb_load']) # ckt.add_transistor(op_dict['cmfb_load_copy'], 'cmfb_outp', 'cmfb_out1n', 'gnd', fg=nf_dict['cmfb_load_copy']) # ckt.add_transistor(op_dict['cmfb_load_copy'], 'cmfb_outn', 'cmfb_out1p', 'gnd', fg=nf_dict['cmfb_load_copy']) # ckt.add_transistor(op_dict['cmfb_out'], 'cmfb_outp', 'cmfb_outp', 'gnd', fg=nf_dict['cmfb_out']) # ckt.add_transistor(op_dict['cmfb_out'], 'cmfb_outn', 'cmfb_outn', 'gnd', fg=nf_dict['cmfb_out']) # assert False, 'blep' # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_outn', 'gnd', 'cmfb_tail', fg=nf_dict['cmfb_in']*2, neg_cap=False) # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_outp', 'main_outn', 'cmfb_tail', fg=nf_dict['cmfb_in'], neg_cap=False) # ckt.add_transistor(op_dict['cmfb_in'], 'cmfb_outp', 'main_outp', 'cmfb_tail', fg=nf_dict['cmfb_in'], neg_cap=False) # ckt.add_transistor(op_dict['cmfb_tail'], 'cmfb_tail', 'gnd', 'gnd', fg=nf_dict['cmfb_tail'], neg_cap=False) # ckt.add_transistor(op_dict['cmfb_out'], 'cmfb_outn', 'cmfb_outn', 'gnd', fg=nf_dict['cmfb_out'], neg_cap=False) # ckt.add_transistor(op_dict['cmfb_out'], 'cmfb_outp', 'cmfb_outp', 'gnd', fg=nf_dict['cmfb_out'], neg_cap=False) return ckt
def make_ltickt(self, op_dict:Mapping[str,Any], nf_dict:Mapping[str,int], cload:float, opp_drain_conn:Mapping[str,str], meas_side:str) -> LTICircuit: ''' Constructs and LTICircuit for the amplifier. Input dictionary keys must include: in tail same_outer same_inner opp_inner opp_outer Inputs: op_dict: Dictionary of queried database operating points. nf_dict: Dictionary of number of fingers for devices. cload: Load capacitance in farads. meas_side: p = only p-input connected (n-input is held at AC ground); n = only n-input connected (p-input is held at AC ground); anything else = both inputs are connected to AC inputs Output: LTICircuit object of this amplifier with inputs as inp and/or inn, output as out. ''' def conv_drain_conn(d_conn): return d_conn.replace('G', 'g').replace('D', 'd').replace('N', '').replace('P', '').replace('<', '').replace('>', '') diode_inner = opp_drain_conn[1] in ('GN<1>', 'GP<1>') diode_outer = opp_drain_conn[0] in ('GN<0>', 'GP<0>') highswing_outer = opp_drain_conn[1] in ('GN<0>', 'GP<0>') opp_drain_conn_conv = [conv_drain_conn(d_conn) for d_conn in opp_drain_conn] ckt = LTICircuit() # Input pair inp_conn = 'gnd' if meas_side=='n' else 'inp' inn_conn = 'gnd' if meas_side=='p' else 'inn' ckt.add_transistor(op_dict['in'], 'out1n', inp_conn, 'tail', fg=nf_dict['in'], neg_cap=False) ckt.add_transistor(op_dict['in'], 'out1p', inn_conn, 'tail', fg=nf_dict['in'], neg_cap=False) # Tail ckt.add_transistor(op_dict['tail'], 'tail', 'gnd', 'gnd', fg=nf_dict['tail'], neg_cap=False) # Outer "same"-side cascode devices ckt.add_transistor(op_dict['same_outer'], 'out1n', 'gnd', 'gnd', fg=nf_dict['same_outer'], neg_cap=False) ckt.add_transistor(op_dict['same_outer'], 'out1p', 'gnd', 'gnd', fg=nf_dict['same_outer'], neg_cap=False) # Inner "same"-side cascode devices d_inner = 'g0' if highswing_outer else 'g1' if diode_inner else 'outx' ckt.add_transistor(op_dict['same_inner'], d_inner, 'gnd', 'out1n', fg=nf_dict['same_inner'], neg_cap=False) ckt.add_transistor(op_dict['same_inner'], 'out', 'gnd', 'out1p', fg=nf_dict['same_inner'], neg_cap=False) # Inner "opposite" side cascode devices g_inner = 'g0' if (highswing_outer and diode_inner) else 'g1' if diode_inner else 'gnd' g_outer = 'g0' if (highswing_outer or diode_inner) else 'gnd' d_outer = 'g0' if diode_outer else 'd0' ckt.add_transistor(op_dict['opp_inner'], d_inner, g_inner, d_outer, fg=nf_dict['opp_inner'], neg_cap=False) ckt.add_transistor(op_dict['opp_inner'], 'out', g_inner, f'{d_outer}x', fg=nf_dict['opp_inner'], neg_cap=False) # Inner "opposite" side cascode devices ckt.add_transistor(op_dict['opp_outer'], d_outer, g_outer, 'gnd', fg=nf_dict['opp_outer'], neg_cap=False) ckt.add_transistor(op_dict['opp_outer'], f'{d_outer}x', g_outer, 'gnd', fg=nf_dict['opp_outer'], neg_cap=False) # Load ckt.add_cap(cload, 'out', 'gnd') return ckt
def make_ltickt(self, op_dict:Mapping[str,Any], nf_dict:Mapping[str,int], cload:float, meas_side:str) -> LTICircuit: inn_conn = 'gnd' if meas_side=='p' else 'inn' inp_conn = 'gnd' if meas_side=='n' else 'inp' ckt = LTICircuit() ckt.add_transistor(op_dict['tail'], 'tail', 'gnd', 'gnd', fg=nf_dict['tail']) ckt.add_transistor(op_dict['in'], 'out', inn_conn, 'tail', fg=nf_dict['in']) ckt.add_transistor(op_dict['in'], 'outx', inp_conn, 'tail', fg=nf_dict['in']) ckt.add_transistor(op_dict['load'], 'outx', 'outx', 'gnd', fg=nf_dict['load']) ckt.add_transistor(op_dict['load'], 'out', 'outx', 'gnd', fg=nf_dict['load']) ckt.add_cap(cload, 'out', 'gnd') return ckt
def design_amp(specs, in_op, nbot_op, ntop_op, pbot_op, ptop_op): casc_nscale = specs['casc_nscale'] casc_pscale = specs['casc_pscale'] nbot_fg = 2 ibias = nbot_op['ibias'] ntop_fg = casc_nscale pbot_fg = ibias / pbot_op['ibias'] ptop_fg = pbot_fg * casc_pscale in_fg = ibias / in_op['ibias'] scale = 3 cir = LTICircuit() cir.add_transistor(in_op, 'x', 'in', 'gnd', 'gnd', fg=in_fg * scale) cir.add_transistor(nbot_op, 'x', 'gnd', 'gnd', 'gnd', fg=nbot_fg * scale) cir.add_transistor(ntop_op, 'out', 'gnd', 'x', 'gnd', fg=ntop_fg * scale) cir.add_transistor(ptop_op, 'out', 'gnd', 'm', 'gnd', fg=ptop_fg * scale) cir.add_transistor(pbot_op, 'm', 'gnd', 'gnd', 'gnd', fg=pbot_fg * scale) cir.add_cap(20e-15 * scale, 'out', 'gnd') cur_tf = cir.get_transfer_function('in', 'out', in_type='v') gain = cur_tf.num[-1] / cur_tf.den[-1] p1, p2, p3 = sorted(-cur_tf.poles) p1 /= 2 * np.pi p2 /= 2 * np.pi p3 /= 2 * np.pi for name, op, fg in [('in', in_op, in_fg), ('nbot', nbot_op, nbot_fg), ('ntop', ntop_op, ntop_fg), ('ptop', ptop_op, ptop_fg), ('pbot', pbot_op, pbot_fg)]: print('%s fg : \t%.3g' % (name, fg * scale)) for val in ('gm', 'gds', 'gb', 'ibias'): if val in op: print('%s %s : \t%.6g' % (name, val, op[val] * scale * fg)) print('vtail: %.6g' % (specs['vincm'] - in_op['vgs'])) print('vmidn: %.6g' % (nbot_op['vds'])) print('vmidp: %.6g' % (specs['vdd'] + pbot_op['vds'])) print('vb1: %.6g' % (specs['vdd'] + pbot_op['vgs'])) print('vb2: %.6g' % (specs['vdd'] + pbot_op['vds'] + ptop_op['vgs'])) print('vb3: %.6g' % (nbot_op['vds'] + ntop_op['vgs'])) print('vb4: %.6g' % (nbot_op['vgs'])) print(gain, p1 / 1e9, p2 / 1e9, p3 / 1e9, nbot_fg * ibias)
def characterize_casc_amp(env_list, fg_list, w_list, db_list, vbias_list, vload_list, vtail_list, vmid_list, vcm, vdd, vin_max, cw, rw, fanout, ton, k_settle_targ, verr_max, scale_res=0.1, scale_min=0.25, scale_max=20): # compute DC transfer function curve and compute linearity spec results = solve_casc_diff_dc(env_list, db_list, w_list, fg_list, vbias_list, vload_list, vtail_list, vmid_list, vdd, vcm, vin_max, verr_max, num_points=20) vin_vec, vmat_list, verr_list, gain_list = results # compute settling ratio fg_in, fg_casc, fg_load = fg_list[1:] db_in, db_casc, db_load = db_list[1:] w_in, w_casc, w_load = w_list[1:] fzin = 1.0 / (2 * ton) wzin = 2 * np.pi * fzin tvec = np.linspace(0, ton, 200, endpoint=True) scale_list = [] for env, vload, vtail, vmid in zip(env_list, vload_list, vtail_list, vmid_list): # step 1: construct half circuit in_params = db_in.query(env=env, w=w_in, vbs=-vtail, vds=vmid - vtail, vgs=vcm - vtail) casc_params = db_casc.query(env=env, w=w_casc, vbs=-vmid, vds=vcm - vmid, vgs=vdd - vmid) load_params = db_load.query(env=env, w=w_load, vbs=0, vds=vcm - vdd, vgs=vload - vdd) circuit = LTICircuit() circuit.add_transistor(in_params, 'mid', 'in', 'gnd', fg=fg_in) circuit.add_transistor(casc_params, 'd', 'gnd', 'mid', fg=fg_casc) circuit.add_transistor(load_params, 'd', 'gnd', 'gnd', fg=fg_load) # step 2: get input capacitance zin = circuit.get_impedance('in', fzin) cin = (1 / zin).imag / wzin circuit.add_cap(cin * fanout, 'out', 'gnd') # step 3: find scale factor to achieve k_settle bin_iter = BinaryIterator(scale_min, None, step=scale_res, is_float=True) while bin_iter.has_next(): # add scaled wired parasitics cur_scale = bin_iter.get_next() cap_cur = cw / 2 / cur_scale res_cur = rw * cur_scale circuit.add_cap(cap_cur, 'd', 'gnd') circuit.add_cap(cap_cur, 'out', 'gnd') circuit.add_res(res_cur, 'd', 'out') # get settling factor sys = circuit.get_voltage_gain_system('in', 'out') dc_gain = sys.freqresp(w=np.array([0.1]))[1][0] sgn = 1 if dc_gain.real >= 0 else -1 dc_gain = abs(dc_gain) _, yvec = scipy.signal.step( sys, T=tvec) # type: Tuple[np.ndarray, np.ndarray] k_settle_cur = 1 - abs(yvec[-1] - sgn * dc_gain) / dc_gain print('scale = %.4g, k_settle = %.4g' % (cur_scale, k_settle_cur)) # update next scale factor if k_settle_cur >= k_settle_targ: print('save scale = %.4g' % cur_scale) bin_iter.save() bin_iter.down() else: if cur_scale > scale_max: raise ValueError( 'cannot meet settling time spec at scale = %d' % cur_scale) bin_iter.up() # remove wire parasitics circuit.add_cap(-cap_cur, 'd', 'gnd') circuit.add_cap(-cap_cur, 'out', 'gnd') circuit.add_res(-res_cur, 'd', 'out') scale_list.append(bin_iter.get_last_save()) return vmat_list, verr_list, gain_list, scale_list
def construct_openLoop(n_op, p_op, tail_op, nf_n, nf_p, nf_tail, cload): ''' Inputs: p/n/tail_op: Operating point information for a given device. NMOS (input), PMOS (active load), or tail device. nf_n/p/tail: Integer. Number of minimum channel width/length devices in parallel. cload: Float. Load capacitance in farads. Outputs: Returns the LTICircuit constructed for open loop analysis of the amplifier. ''' ckt = LTICircuit() # Left side ckt.add_transistor(n_op, 'out_copy', 'in', 'tail', fg=nf_n) ckt.add_transistor(p_op, 'out_copy', 'out_copy', 'gnd', fg=nf_p) # Right side ckt.add_transistor(n_op, 'out', 'gnd', 'tail', fg=nf_n) ckt.add_transistor(p_op, 'out', 'out_copy', 'gnd', fg=nf_p) # Tail ckt.add_transistor(tail_op, 'tail', 'gnd', 'gnd', fg=nf_tail) # Adding additional load ckt.add_cap(cload, 'out', 'gnd') return ckt
def meet_spec(self, **params) -> List[Mapping[str, Any]]: """To be overridden by subclasses to design this module. Raises a ValueError if there is no solution. """ ### Get DBs for each device specfile_dict = params['specfile_dict'] in_type = params['in_type'] l_dict = params['l_dict'] th_dict = params['th_dict'] sim_env = params['sim_env'] # Databases db_dict = { k: get_mos_db(spec_file=specfile_dict[k], intent=th_dict[k], lch=l_dict[k], sim_env=sim_env) for k in specfile_dict.keys() } ### Design devices vdd = params['vdd'] vincm = params['vincm'] cload = params['cload'] gain_min, gain_max = params['gain_lim'] fbw_min = params['fbw'] ibias_max = params['ibias'] optional_params = params['optional_params'] # Pulling out optional parameters vstar_min = optional_params.get('vstar_min', 0.2) res_vstep = optional_params.get('res_vstep', 10e-3) error_tol = optional_params.get('error_tol', 0.01) n_in = in_type == 'n' # Estimate threshold of each device vtest = vdd / 2 if n_in else -vdd / 2 vb = 0 if n_in else vdd vth_in = estimate_vth(is_nch=n_in, vgs=vtest, vbs=0, db=db_dict['in'], lch=l_dict['in']) vth_tail = estimate_vth(is_nch=n_in, vgs=vtest, vbs=0, db=db_dict['tail'], lch=l_dict['tail']) # Keeping track of operating points which work for future comparison viable_op_list = [] # Sweep tail voltage vtail_min = vstar_min if n_in else vincm - vth_in vtail_max = vincm - vth_in if n_in else vdd - vstar_min vtail_vec = np.arange(vtail_min, vtail_max, res_vstep) print(f'Sweeping tail from {vtail_min} to {vtail_max}') for vtail in vtail_vec: voutcm_min = vincm - vth_in if n_in else 0 voutcm_max = vdd if n_in else vincm - vth_in voutcm = optional_params.get('voutcm', None) if voutcm == None: voutcm_vec = np.arange(voutcm_min, voutcm_max, res_vstep) else: voutcm_vec = [voutcm] # Sweep output common mode for voutcm in voutcm_vec: in_op = db_dict['in'].query(vgs=vincm - vtail, vds=voutcm - vtail, vbs=vb - vtail) ibias_min = 2 * in_op['ibias'] # Step input device size (integer steps) nf_in_max = int(round(ibias_max / ibias_min)) nf_in_vec = np.arange(2, nf_in_max, 2) for nf_in in nf_in_vec: ibias = ibias_min * nf_in ibranch = ibias / 2 res_val = (vdd - voutcm) / ibranch if n_in else voutcm / ibranch # Check gain, bandwidth ckt_half = LTICircuit() ckt_half.add_transistor(in_op, 'out', 'in', 'gnd', fg=nf_in, neg_cap=False) ckt_half.add_res(res_val, 'out', 'gnd') ckt_half.add_cap(cload, 'out', 'gnd') num, den = ckt_half.get_num_den(in_name='in', out_name='out', in_type='v') gain = -(num[-1] / den[-1]) wbw = get_w_3db(num, den) if wbw == None: wbw = 0 fbw = wbw / (2 * np.pi) if gain < gain_min or gain > gain_max: print(f'gain: {gain}') break if fbw < fbw_min: print(f'fbw: {fbw}') continue # Design tail to current match vgtail_min = vth_tail + vstar_min if n_in else vtail + vth_tail vgtail_max = vtail + vth_tail if n_in else vdd + vth_tail - vstar_min vgtail_vec = np.arange(vgtail_min, vgtail_max, res_vstep) print(f'vgtail {vgtail_min} to {vgtail_max}') for vgtail in vgtail_vec: tail_op = db_dict['tail'].query(vgs=vgtail - vb, vds=vtail - vb, vbs=0) tail_success, nf_tail = verify_ratio( in_op['ibias'] * 2, tail_op['ibias'], nf_in, error_tol) if not tail_success: print('tail match') continue viable_op = dict(nf_in=nf_in, nf_tail=nf_tail, res_val=res_val, voutcm=voutcm, vgtail=vgtail, gain=gain, fbw=fbw, vtail=vtail, ibias=tail_op['ibias'] * nf_tail, cmfb_cload=tail_op['cgg'] * nf_tail) viable_op_list.append(viable_op) print("\n(SUCCESS)") print(viable_op) self.other_params = dict( in_type=in_type, l_dict=l_dict, w_dict={k: db.width_list[0] for k, db in db_dict.items()}, th_dict=th_dict) # assert False, 'blep' return viable_op_list
def design_TIA_inverter(db_n, db_p, sim_env, vg_res, rf_res, vdd_nom, vdd_vec, cpd, cload, rdc_min, fbw_min, pm_min, BER_max, vos, isw_pkpk, vb_n, vb_p, error_tol=0.05, ibias_max=20e-6): """ Designs a transimpedance amplifier with an inverter amplifier in resistive feedback. Uses the LTICircuit functionality. Inputs: db_n/p: Databases for NMOS and PMOS device characterization data, respectively. sim_env: Simulation corner. vg_res: Float. Step resolution in volts when sweeping gate voltage. rf_res: Float. Step resolution in ohms when sweeping feedback resistance. vdd_nom: Float. Nominal supply voltage in volts. vdd_vec: Collection of floats. Elements should include the min and max supply voltage in volts. cpd: Float. Input parasitic capacitance in farads. cload: Float. Output load capacitance in farads. rdc_min: Float. Minimum DC transimpedance in ohms. fbw_min: Float. Minimum bandwidth (Hz). pm_min: Float. Minimum phase margin in degrees. BER_max: Float. Maximum allowable bit error rate (as a fraction). vos: Float. Input-referred DC offset for any subsequent comparator stage as seen at the output of the TIA. isw_pkpk: Float. Input current peak-to-peak swing in amperes. vb_n/p: Float. Back-gate/body voltage (V) of NMOS and PMOS, respectively. error_tol: Float. Fractional tolerance for ibias error when computing the p-to-n ratio. ibias_max: Float. Maximum bias current (A) allowed. Raises: ValueError: If unable to meet the specification requirements. Outputs: A dictionary with the following key:value pairings: vg: Float. Input bias voltage. nf_n: Integer. NMOS number of channel fingers. nf_p: Integer. PMOS number of channel fingers. rf: Float. Value of feedback resistor. rdc: Float. Expected DC transimpedance. fbw: Float. Expected bandwidth (Hz). pm: Float. Expected phase margin. ibias: Float. Expected DC bias current. """ # Finds all possible designs for one value of VDD, then # confirm which work with all other VDD values. possibilities = [] vg_vec = np.arange(0, vdd_nom, vg_res) for vg in vg_vec: print("VIN:\t{0}".format(vg)) n_op_info = db_n.query(vgs=vg, vds=vg, vbs=vb_n - 0) p_op_info = db_p.query(vgs=vg - vdd_nom, vds=vg - vdd_nom, vbs=vb_p - vdd_nom) if np.isinf(ibias_max): nf_n_max = 200 else: nf_n_max = int(round(ibias_max / n_op_info['ibias'])) nf_n_vec = np.arange(1, nf_n_max, 1) for nf_n in nf_n_vec: # Number of fingers can only be integer, # so increase as necessary until you get # sufficiently accurate/precise bias + current match ratio_good, nf_p = verify_ratio(n_op_info['ibias'], p_op_info['ibias'], nf_n, error_tol) if not ratio_good: continue # Getting small signal parameters to constrain Rf inv = LTICircuit() inv.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n) inv.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p) inv_num, inv_den = inv.get_num_den(in_name='in', out_name='out', in_type='v') A0 = abs(inv_num[-1] / inv_den[-1]) gds_n = n_op_info['gds'] * nf_n gds_p = p_op_info['gds'] * nf_p gds = abs(gds_n) + abs(gds_p) ro = 1 / gds # Assume Rdc is negative, bound Rf rf_min = max(rdc_min * (1 + A0) / A0 + ro / A0, 0) rf_vec = np.arange(rf_min, rdc_min * 2, rf_res) for rf in rf_vec: # With all parameters, check if it meets small signal spec meets_SS, SS_vals = verify_TIA_inverter_SS( n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload, rdc_min, fbw_min, pm_min) # With all parameters, estimate if it will meet noise spec meets_noise, BER = verify_TIA_inverter_BER( n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload, BER_max, vos, isw_pkpk) meets_spec = meets_SS # and meets_noise # If it meets small signal spec, append it to the list # of possibilities if meets_spec: possibilities.append( dict(vg=vg, vdd=vdd_nom, nf_n=nf_n, nf_p=nf_p, rf=rf, rdc=SS_vals['rdc'], fbw=SS_vals['fbw'], pm=SS_vals['pm'], ibias=ibias_n, BER=BER)) elif SS_vals['fbw'] != None and SS_vals['fbw'] < fbw_min: # Increasing resistor size won't help bandwidth break # Go through all possibilities which work at the nominal voltage # and ensure functionality at other bias voltages # Remove any nonviable options print("{0} working at nominal VDD".format(len(possibilities))) for candidate in possibilities: nf_n = candidate['nf_n'] nf_p = candidate['nf_p'] rf = candidate['rf'] for vdd in vdd_vec: new_op_dict = vary_supply(vdd, db_n, db_p, nf_n, nf_p, vb_n, vb_p) vg = new_op_dict['vb'] n_op = new_op_dict['n_op'] p_op = new_op_dict['p_op'] # Confirm small signal spec is met meets_SS, scratch = verify_TIA_inverter_SS(n_op, p_op, nf_n, nf_p, rf, cpd, cload, rdc_min, fbw_min, pm_min) # Confirm noise spec is met meets_noise, BER = verify_TIA_inverter_BER(n_op, p_op, nf_n, nf_p, rf, cpd, cload, BER_max, vos, isw_pkpk) meets_spec = meets_SS # and meets_noise if not meets_spec: possibilities.remove(candidate) break # Of the remaining possibilities, check for lowest power. # If there are none, raise a ValueError. if len(possibilities) == 0: raise ValueError("No final viable solutions") print("{0} working at all VDD".format(len(possibilities))) best_op = possibilities[0] for candidate in possibilities: best_op = choose_op_comparison(best_op, candidate) return best_op