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 _get_ss_lti(self, op_dict:Mapping[str,Any], nf_dict:Mapping[str,int], cload:float) -> Tuple[float,float]: ckt_p = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, cload=cload, meas_side='p') p_num, p_den = ckt_p.get_num_den(in_name='inp', out_name='out', in_type='v') ckt_n = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, cload=cload, meas_side='n') n_num, n_den = ckt_n.get_num_den(in_name='inn', out_name='out', in_type='v') num, den = num_den_add(p_num, np.convolve(n_num, [-1]), p_den, n_den) num = np.convolve(num, [0.5]) gain = num[-1]/den[-1] wbw = get_w_3db(num, den) pm, _ = get_stability_margins(num, den) ugw, _ = get_w_crossings(num, den) if wbw == None: wbw = 0 fbw = wbw/(2*np.pi) if ugw == None: ugw = 0 ugf = ugw/(2*np.pi) return gain, fbw, ugf, pm
def verify_closedLoop(n_op, p_op, tail_op, nf_n, nf_p, nf_tail, pm_min, cload, cn=165e-15, cp=83e-15, r=70.71e3): ''' Inputs: Outputs: ''' ckt = construct_feedback(n_op, p_op, tail_op, nf_n, nf_p, nf_tail, cload) loopBreak = ckt.get_transfer_function(in_name='inn', out_name='out', in_type='v') pm, gainm = get_stability_margins(loopBreak.num, loopBreak.den) if pm < pm_min or isnan(pm): return False, dict(pm=pm) return True, dict(pm=pm)
def opt_cfb(phase_margin, cir, cmin, cmax, cstep, ctol): bin_iter = FloatBinaryIterator(cmin, None, ctol, search_step=cstep) while bin_iter.has_next(): cur_cf = bin_iter.get_next() cir.add_cap(cur_cf, 'x', 'out') num, den = cir.get_num_den('in', 'out') cur_pm, _ = get_stability_margins(num, den) if cur_pm < phase_margin: if cur_cf > cmax: # no way to make amplifier stable, just return return None bin_iter.up() else: bin_iter.save() bin_iter.down() cir.add_cap(-cur_cf, 'x', 'out') return bin_iter.get_last_save()
def _get_ss_lti(self, op_dict:Mapping[str,Any], nf_dict:Mapping[str,int], opp_drain_conn:Mapping[str,str], cload:float) -> Tuple[float,float,float]: ''' Inputs: op_dict: Dictionary of queried database operating points. nf_dict: Dictionary of number of fingers for devices. opp_drain_conn: Drain connection dictionary, a la n_drain_conn or p_drain_conn cload: Load capacitance in farads. Outputs: gain: Calculated DC gain in V/V fbw: Calculated 3dB frequency in Hz ugf: Calculated unity gain frequency in Hz pm: Simulated unity gain phase margin in degrees. Can also be NaN. ''' ckt_p = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, meas_side='p', opp_drain_conn=opp_drain_conn, cload=cload) p_num, p_den = ckt_p.get_num_den(in_name='inp', out_name='out', in_type='v') ckt_n = self.make_ltickt(op_dict=op_dict, nf_dict=nf_dict, meas_side='n', opp_drain_conn=opp_drain_conn, cload=cload) n_num, n_den = ckt_n.get_num_den(in_name='inn', out_name='out', in_type='v') # Superposition for inverting and noninverting inputs num, den = num_den_add(p_num, np.convolve(n_num, [-1]), p_den, n_den) num = np.convolve(num, [0.5]) # Calculate figures of merit using the LTICircuit transfer function gain = num[-1]/den[-1] wbw = get_w_3db(num, den) pm, _ = get_stability_margins(num, den) if wbw == None: wbw = 0 fbw = wbw/(2*np.pi) ugw, _ = get_w_crossings(num, den) if ugw == None: ugw = 0 ugf = ugw/(2*np.pi) return gain, fbw, ugf, pm
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 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 _find_rz_cf(self, gm_db, load_db, vtail_list, vg_list, vmid_list, vout_list, vbias_list, vb_gm, vb_load, cload, cpar1, w_dict, th_dict, stack_dict, seg_dict, gm2_list, res_var, phase_margin, cap_tol=1e-15, cap_step=10e-15, cap_min=1e-15, cap_max=1e-9): """Find minimum miller cap that stabilizes the system. NOTE: This function assume phase of system for any miller cap value will not loop around 360, otherwise it may get the phase margin wrong. This assumption should be valid for this op amp. """ gz_worst = float(min(gm2_list)) gz_nom = gz_worst * (1 - res_var) # find maximum Cf needed to stabilize all corners cf_min = cap_min for env_idx, (vtail, vg, vmid, vout, vbias) in \ enumerate(zip(vtail_list, vg_list, vmid_list, vout_list, vbias_list)): cir = self._make_circuit(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_worst) bin_iter = FloatBinaryIterator(cf_min, None, cap_tol, search_step=cap_step) while bin_iter.has_next(): cur_cf = bin_iter.get_next() cir.add_cap(cur_cf, 'outp', 'xp') cir.add_cap(cur_cf, 'outn', 'xn') num, den = cir.get_num_den('in', 'out') cur_pm, _ = get_stability_margins(num, den) if cur_pm < phase_margin: if cur_cf > cap_max: # no way to make amplifier stable, just return return None, None, None, None, None, None bin_iter.up() else: bin_iter.save() bin_iter.down() cir.add_cap(-cur_cf, 'outp', 'xp') cir.add_cap(-cur_cf, 'outn', 'xn') # bin_iter is guaranteed to save at least one value, so don't need to worry about # cf_min being None cf_min = bin_iter.get_last_save() # find gain, unity gain bandwidth, and phase margin across corners gain_list, f3db_list, funity_list, pm_list = [], [], [], [] for env_idx, (vtail, vg, vmid, vout, vbias) in \ enumerate(zip(vtail_list, vg_list, vmid_list, vout_list, vbias_list)): cir = self._make_circuit(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_nom) cir.add_cap(cf_min, 'outp', 'xp') cir.add_cap(cf_min, 'outn', 'xn') num, den = cir.get_num_den('in', 'out') pn = np.poly1d(num) pd = np.poly1d(den) gain_list.append(abs(pn(0) / pd(0))) f3db_list.append(get_w_3db(num, den) / 2 / np.pi) funity_list.append(get_w_crossings(num, den)[0] / 2 / np.pi) pm_list.append(get_stability_margins(num, den)[0]) return funity_list, 1 / gz_nom, cf_min, gain_list, f3db_list, pm_list
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_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 design_LPF_AMP(db_n, db_p, db_bias, sim_env, vin, vdd_nom, vdd_vec, cload, gain_min, fbw_min, pm_min, vb_n, vb_p, error_tol=0.1, ibias_max=20e-6): ''' Designs an amplifier with an N-input differential pair with a source follower. Uses the LTICircuit functionality. Inputs: db_n/p: Databases for non-biasing NMOS and PMOS device characterization data, respectively. db_bias: Database for tail NMOS device characterization data. sim_env: Simulation corner. vin: Float. Input (and output and tail) bias voltage in volts. vtail_res: Float. Step resolution in volts when sweeping tail voltage. vdd_nom: Float. Nominal supply voltage in volts. vdd_vec: Collection of floats. Elements should include the min and max supply voltage in volts. cload: Float. Output load capacitance in farads. gain_min: Float. Minimum DC voltage gain in V/V. fbw_min: Float. Minimum bandwidth (Hz). pm_min: Float. Minimum phase margin in degrees. vb_n/p: Float. Back-gate/body voltage (V) of NMOS and PMOS, respectively (nominal). error_tol: Float. Fractional tolerance for ibias error when computing the p-to-n ratio. ibias_max: Float. Maximum bias current (A) allowed with nominal vdd. Raises: ValueError: If unable to meet the specification requirements. Outputs: A dictionary with the following key:value pairings: nf_cs_n/pB/nB: Integer. Number of minimum device widths for common source N-input/active P-load/tail device. nf_sfN_n/pB/nB: Integer. Number of minimum device widths for source follower N-input/P-bias/N-bias. nf_sfP_p/pB/nB: Integer. Number of minimum device widths for source follower P-input/P-bias/N-bias. vtail: Float. DC tail voltage for the common source stage. This value is reused throughout the circuit. gain_cs: Float. DC voltage gain of the CS amplifier (V/V). gain_sfN: Float. DC voltage gain of the first source follower (V/V). gain_sfP: Float. DC voltage gain of the second source follower (V/V). gain: Float. DC voltage gain of both stages combined (V/V). fbw: Float. Bandwdith (Hz). pm: Float. Phase margin (degrees) for unity gain. ''' possibilities = [] ibias_budget = ibias_max # Given this amplifier will be unity gain feedback at DC vout = vin vstar_min = 0.15 vtail_vec = np.arange(vstar_min, vout, vtail_res) for vtail in vtail_vec: print("\nVTAIL: {0}".format(vtail)) n_op = db_n.query(vgs=vin-vtail, vds=vout-vtail, vbs=vb_n-vtail) p_B_op = db_p.query(vgs=vout-vdd_nom, vds=vout-vdd_nom, vbs=vb_p-vdd_nom) n_B_op = db_bias.query(vgs=vout-0, vds=vtail-0, vbs=vb_n-0) p_op = db_p.query(vgs=vtail-vout, vds=vtail-vout, vbs=vb_p-vout) # Finding the ratio between devices to converge to correct biasing idn_base = n_op['ibias'] idp_B_base = p_B_op['ibias'] idn_B_base = n_B_op['ibias'] idp_base = p_op['ibias'] pB_to_n = abs(idn_base/idp_B_base) nB_to_n = abs(idn_base/idn_B_base) pB_to_p = abs(idp_base/idp_B_base) nB_to_p = abs(idp_base/idn_B_base) ### P-input SF ### sfP_mult_max = int(round(abs(ibias_budget/idn_base))) sfP_mult_vec = np.arange(1, sfP_mult_max, 1) for sfP_mult in sfP_mult_vec in nf_sfP_p = sfP_mult # Verify that the sizing is feasible and gets sufficiently # good current matching pB_sfP_good, nf_sfP_pB = verify_ratio(idp_base, idp_B_base, pB_to_p, nf_sfP_p, error_tol) nB_sfP_good, nf_sfP_nB = verify_ratio(idp_base, idn_B_base, nB_to_p, nf_sfP_p, error_tol) if not (pB_sfP_good and nB_sfP_good): continue # Check SF2 bandwidth; size up until it meets ckt = construct_sfP_LTI(p_op, p_B_op, n_B_op, nf_sfP_p, nf_sfP_pB, nf_sfP_nB, cload) sfP_num, sfP_den = ckt.get_num_den(in_name='out2', out_name='out', in_type='v') fbw_sfP = get_w_3db(sfP_num, sfP_den)/(2*np.pi) # No need to keep sizing up if fbw_sfP >= fbw_min: break # Final stage can never meet bandwidth given biasing if fbw_sfP < fbw_min: print("SFP: BW failure\n") continue # Sizing ratio failure if 0 in [nf_sfP_pB, nf_sfP_nB]: print("SFP: sizing ratio failure\n") continue ibias_sfP = idp_base * nf_sfP_p ibias_budget = ibias_budget - ibias_sfP print("Remaining budget\t:\t {0}".format(ibias_budget)) ### N-input SF ### sfN_mult_max = int(round(abs(ibias_budget/idn_base))) sfN_mult_vec = np.arange(1, sfN_mult_max, 1) for sfN_mult in sfN_mult_vec: nf_sfN_n = sfN_mult # Repeat the same feasibility + current matching check pB_sfN_good, nf_sfN_pB = verify_ratio(idn_base, idp_B_base, pB_to_n, nf_sfN_n, error_tol) nB_sfN_good, nf_sfN_pN = verify_ratio(idn_base, idn_B_base, nB_to_n, nf_sfN_n, error_tol) if not (pB_sfn_good and nB_sfN_good): continue # Check SF1 bandwidth; size up until it meets ckt = construct_sfN_LTI(p_op, p_B_op, n_B_op, nf_sfP_p, nf_sfP_pB, nf_sfP_nB, cload, n_op, nf_sfN_n, nf_sfN_pB, nf_sfN_nB) sfN_num, sfN_den = ckt.get_num_den(in_name='out1', out_name='out', in_type='v') fbw_sfN = get_w_3db(sfN_num, sfN_den)/(2*np.pi) # No need to keep sizing up if fbw_sfN >= fbw_min: break # Second stage can never meet bandwidth given restrictions if fbw_sfN < fbw_min: print("SFN: BW failure\n") continue # Sizing ratio failure if 0 in [nf_sfN_pB, nf_sfN_nB]: print("SFN: sizing ratio failure\n") continue ibias_sfN = idn_base * nf_sfN_n ibias_budget = ibias_budget - ibias_sfN print("Remaining budget\t:\t {0}".format(ibias_budget)) ### CS input ### cs_mult_max = int(round(abs(ibias_budget/idn_base))) cs_mult_vec = np.arange(1, cs_mult_max, 1) for cs_mult in cs_mult_vec: nf_cs_n = cs_mult_vec # Verify that the sizing is feasible and gets sufficiently # good current matching pB_cs_good, nf_cs_pB = verify_ratio(idn_base, idp_B_base, pB_to_n, nf_cs_n, error_tol) nB_cs_good, nf_cs_nB = verify_ratio(idn_base, idn_B_base, nB_to_n, nf_cs_n*2, error_tol) if not (pB_cs_good and nB_cs_good): continue # Check combined stages' small signal ckt = construct_total_LTI(p_op, p_B_op, n_B_op, nf_sfP_p, nf_sfP_pB, nf_sfP_nB, cload, n_op, nf_sfN_n, nf_sfN_pB, nf_sfN_nB, nf_cs_n, nf_cs_pB, nf_cs_nB) total_num, total_den = ckt.get_num_den(in_name='in', out_name='out', in_type='v') # Check cumulative gain gain_total = abs(total_num[-1]/total_den[-1]) if gain_total < gain_min: print("CS: A0 failure {0}\n".format(gain_total)) break # Biasing sets the gain # Check cumulative bandwidth fbw_total = get_w_3db(total_num, total_den)/(2*np.pi) if fbw_total < fbw_min: print("CS: BW failure {0}\n".format(fbw_total)) continue # Check phase margin (TODO?) loopBreak = ckt.get_transfer_function(in_name='out', out_name='in', in_type='v') pm, gainm = get_stability_margins(loopBreak.num, loopBreak.den) if pm < pm_min or isnan(pm): print("CS: PM failure {0}\n".format(pm)) continue # If gain, bw, and PM are met, no need to keep sizing up break # Sizing ratio failure if 0 in [nf_cs_pB, nf_cs_nB]: print("CS: sizing ratio failure\n") continue # Iterated, biasing condition + constraints can't meet spec if fbw_total < fbw_min or gain_total < gain_min or pm < pm_min: continue else: print("HALLELUJAH SUCCESS\n") cs_num, cs_den = ckt.get_num_den(in_name='in', out_name='out1', in_type='v') sfN_num, sfN_den = ckt.get_num_den(in_name='out1', out_name='out2', in_type='v') sfP_num, sfP_den = ckt.get_num_den(in_name='out2', out_name='out', in_type='v') gain_cs = abs(cs_num[-1]/cs_den[-1]) gain_sfN = abs(sfN_num[-1]/sfN_den[-1]) gain_sfP = abs(sfP_num[-1]/sfP_den[-1]) viable = dict( nf_cs_n = nf_cs_n, nf_cs_pB = nf_cs_pB, nf_cs_nB = nf_cs_nB, nf_sfN_n = nf_sfN_n, nf_sfN_pB = nf_sfN_pB, nf_sfN_nB = nf_sfN_nB, nf_sfP_p = nf_sfP_p, nf_sfP_pB = nf_sfP_pB, nf_sfP_nB = nf_sfP_nB, vtail = vtail, gain_cs = gain_cs, gain_sfN = gain_sfN, gain_sfP = gain_sfP, gain = gain_total, fbw = fbw_total, pm = pm) pprint.pprint(viable) print("\n") possibilities.append([viable]) # TODO: Check all other VDDs return possibilities
def verify_TIA_inverter_SS(n_op_info, p_op_info, nf_n, nf_p, rf, cpd, cload, rdc_min, fbw_min, pm_min): """ Inputs: n/p_op_info: The MOSDBDiscrete library for the NMOS and PMOS devices in the bias point for verification. nf_n/p: Integer. Number of channel fingers for the NMOS/PMOS. rf: Float. Value of the feedback resistor in ohms. cpd: Float. Input capacitance not from the TIA in farads. cload: Float. Output capacitance not from the TIA in farads. rdc_min: Float. Minimum DC transimpedance in ohms. fbw_min: Float. Minimum bandwidth (Hz). pm_min: Float. Minimum phase margin in degrees. Outputs: Returns two values The first is True if the spec is met, False otherwise. The second is a dictionary of values for rdc (DC transimpedance, V/I), bw (bandwidth, Hz), and pm (phase margin, deg) if computed. None otherwise. """ # Getting relevant small-signal parameters gds_n = n_op_info['gds'] * nf_n gds_p = p_op_info['gds'] * nf_p gds = gds_n + gds_p gm_n = n_op_info['gm'] * nf_n gm_p = p_op_info['gm'] * nf_p gm = gm_n + gm_p cgs_n = n_op_info['cgs'] * nf_n cgs_p = p_op_info['cgs'] * nf_p cgs = cgs_n + cgs_p cds_n = n_op_info['cds'] * nf_n cds_p = p_op_info['cds'] * nf_p cds = cds_n + cds_p cgd_n = n_op_info['cgd'] * nf_n cgd_p = p_op_info['cgd'] * nf_p cgd = cgd_n + cgd_p # 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') # Check gain num, den = circuit.get_num_den(in_name='in', out_name='out', in_type='i') rdc = num[-1] / den[-1] if abs(round(rdc)) < round(rdc_min): print("GAIN:\t{0} (FAIL)".format(rdc)) return False, dict(rdc=rdc, fbw=None, pm=None) # Check bandwidth fbw = get_w_3db(num, den) / (2 * np.pi) if fbw < fbw_min or np.isnan(fbw): print("BW:\t{0} (FAIL)".format(fbw)) return False, dict(rdc=rdc, fbw=fbw, pm=None) # Check phase margin by constructing an LTICircuit first circuit2 = LTICircuit() """circuit2.add_transistor(n_op_info, 'out', 'in', 'gnd', fg=nf_n) circuit2.add_transistor(p_op_info, 'out', 'in', 'gnd', fg=nf_p) circuit2.add_cap(cpd, 'in', 'gnd') circuit2.add_cap(cload, 'out', 'gnd') circuit2.add_res(rf, 'in', 'break') # Cancel Cgd to correctly break loop circuit2.add_cap(-cgd, 'in' , 'out') circuit.add_cap(cgd, 'in', 'break')""" circuit2.add_conductance(gds, 'out', 'gnd') circuit2.add_cap(cgs + cpd, 'in', 'gnd') circuit2.add_cap(cds + cload, 'out', 'gnd') circuit2.add_cap(cgd, 'in', 'out') circuit2.add_res(rf, 'in', 'out') loopBreak = circuit2.get_transfer_function(in_name='in', out_name='out', in_type='i') pm, gainm = get_stability_margins(loopBreak.num * gm, loopBreak.den) if pm < pm_min or np.isnan(pm): print("PM:\t{0} (FAIL)\n".format(pm)) return False, dict(rdc=rdc, fbw=fbw, pm=pm) print("SUCCESS\n") return True, dict(rdc=rdc, fbw=fbw, pm=pm)
def design_diffAmpP(db_n, db_p, sim_env, vdd, cload, vincm, T, gain_min, fbw_min, pm_min, inoiseout_std_max, vb_p, vb_n, error_tol=0.05): """ Inputs: Returns: """ # Constants kBT = 1.38e-23 * T # TODO: change hardcoded value vstar_min = 0.15 # find the best operating point best_ibias = float('inf') best_op = None # (loosely) constrain tail voltage # TODO: replace with binary search vtail_min = vincm + 2 * vstar_min vtail_max = vdd vtail_vec = np.arange(vtail_min, vtail_max, 10e-3) # sweep tail voltage for vtail in vtail_vec: # (loosely) constrain output DC voltage # TODO: replace with binary search vout_min = vstar_min vout_max = vtail - vstar_min vout_vec = np.arange(vout_min, vout_max, 10e-3) # sweep output DC voltage for vout in vout_vec: in_op = db_p.query(vgs=vincm - vtail, vds=vout - vtail, vbs=vb_p - vtail) load_op = db_n.query(vgs=vout, vds=vout, vbs=vb_n - 0) # TODO: constrain number of input devices nf_in_min = 4 nf_in_max = 30 nf_in_vec = np.arange(nf_in_min, nf_in_max, 2) for nf_in in nf_in_vec: ibranch = abs(in_op['ibias'] * nf_in) if ibranch * 2 > best_ibias: continue # matching NMOS and PMOS bias current nf_load = int(abs(round(ibranch / load_op['ibias']))) if nf_load < 1: continue iload = load_op['ibias'] * nf_load ibranch_error = (abs(iload) - abs(ibranch)) / abs(ibranch) if ibranch_error > error_tol: continue # create LTICircuit amp = LTICircuit() amp.add_transistor(in_op, 'out', 'in', 'gnd', 'gnd', fg=nf_in) amp.add_transistor(load_op, 'out', 'gnd', 'gnd', 'gnd', fg=nf_load) amp.add_cap(cload, 'out', 'gnd') num, den = amp.get_num_den(in_name='in', out_name='out', in_type='v') gm = in_op['gm'] * nf_in ro = 1 / (in_op['gds'] * nf_in + load_op['gds'] * nf_load) # Check against gain gain = abs(num[-1] / den[-1]) if gain < gain_min: print("GAIN: {0:.2f} (FAIL)\n".format(gain)) continue print("GAIN: {0:.2f}".format(gain)) # Check against bandwidth wbw = get_w_3db(num, den) if wbw == None: print("BW: None (FAIL)\n") continue fbw = wbw / (2 * np.pi) if fbw < fbw_min: print("BW: {0:.2f} (FAIL)\n".format(fbw)) continue print("BW: {0:.2f}".format(fbw)) pm, gainm = get_stability_margins(num, den) # Check against phase margin if pm < pm_min or isnan(pm): print("PM: {0:.2f} (FAIL)\n".format(pm)) continue print("PM: {0:.2f}".format(pm)) # Check against noise inoiseout_std = np.sqrt( 4 * kBT * (in_op['gamma'] * in_op['gm'] * nf_in * 2 + load_op['gamma'] * load_op['gm'] * nf_load * 2)) if inoiseout_std > inoiseout_std_max: print("INOISE STD: {} (FAIL)\n".format(inoiseout_std)) continue print("INOISE STD: {}".format(inoiseout_std)) # Check against best bias current if ibranch * 2 < best_ibias: biasing_spec = dict( db_n=db_n, db_p=db_p, sim_env=sim_env, vdd=vdd, vout=vout, vincm=vincm, nf_in=nf_in, nf_load=nf_load, vtail_target=vtail, itail_target=ibranch * 2, iref_res=10e-9, vb_p=vb_p, vb_n=vb_n, error_tol=error_tol, ) biasing_params = design_biasing(**biasing_spec) if biasing_params == None: print("BIASING PARAMS (FAIL)\n") continue print("(SUCCESS)\n") best_ibias = ibranch * 2 best_op = dict( itail=best_ibias, nf_in=nf_in, nf_load=nf_load, vout=vout, vtail=vtail, gain=gain, fbw=fbw, pm=pm, nf_tail=biasing_params['nf_tail'], nf_bias_tail=biasing_params['nf_bias_tail'], nf_bias_in=biasing_params['nf_bias_in'], nf_bias_loadM=biasing_params['nf_bias_loadM'], nf_bias_load1=biasing_params['nf_bias_load1'], iref=biasing_params['iref'], inoiseout_std=inoiseout_std) break if best_op == None: raise ValueError("No solution for P-in diffamp") return best_op