molecule
12 V to 5 V / 1 A Buck Molecule (TPS54202), built in Altium Designer
Public Unreviewedby Colby Knox
A 12 V to 5 V, 1 A TPS54202 buck molecule designed and built by AI in Altium Designer from one sentence, with the Altium project, Gerbers, BOM, simulation and current and heat analysis.
main
Colby Knox
Altium Designer build of the TPS54202 buck molecule: Altium project, manufacturing set, STEP and GLB, design, simulatio…
845dd0d
4d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
#!/usr/bin/env python3
"""Loop gain of the TPS54202 5 V design in ngspice (averaged peak-current-mode model).
The power stage is the standard PCM average model: the inductor becomes a current source Vc/Ri
into COUT || load, followed by the sampling double pole at fs/2 (Ridley). TI does not publish the
internal compensation; the equivalent gm-stage (gm*Rc/Ri) is calibrated so that, with no Cff, the
crossover equals the datasheet's own estimate eq 14 (fo = 3.95 / (VOUT * COUT) kHz, COUT in uF).
The loop is broken with a Middlebrook injection between VOUT and the divider.
Run: python3 loop.py -> loop.csv, loop.json, loop.png
"""
import json, math, subprocess, os
import numpy as np
HERE = os.path.dirname(os.path.abspath(__file__))
calcs = json.load(open(os.path.join(HERE, "..", "design", "calcs.json")))
VREF, VOUT, FSW, L = 0.596, 5.0, 500e3, 15e-6
RTOP, RBOT = calcs["feedback"]["Rtop"], calcs["feedback"]["Rbot"]
COUT = calcs["cout"]["effective_uF"] * 1e-6
ESR = 0.003 / 3
RI = 0.25 # V/A, equivalent current-sense gain (only gm*Rc/Ri matters mid-band)
GMRC = 3.95 * 2 * math.pi / VREF * RI # calibration to eq 14: fo[Hz] = 3.95 / (VOUT * COUT[F])
GM = 1e-4
RC = GMRC / GM
CC = 1 / (2 * math.pi * 2e3 * RC) # comp zero ~2 kHz
CP = 1 / (2 * math.pi * 200e3 * RC) # HF pole ~200 kHz
RO = 1000 / GM # EA DC gain 60 dB
def netlist(vin, rload, cff):
D = VOUT / vin
Sn = (vin - VOUT) / L * RI # on-slope at the sense output (V/s)
Se = 0.5 * VOUT / L * RI # slope comp: half the down-slope
mc = 1 + Se / Sn
Qp = 1 / (math.pi * (mc * (1 - D) - 0.5))
wn = math.pi * FSW
# sampling double pole as a series RLC low-pass: wn = 1/sqrt(LC), Q = sqrt(L/C)/R
Cs = 1e-9
Ls = 1 / (wn**2 * Cs)
Rs = math.sqrt(Ls / Cs) / Qp
cff_line = f"Cff vfbtop fb {cff}p" if cff else ""
return f"""* TPS54202 PCM loop gain vin={vin} rload={rload} cff={cff}p
Vx vout vfbtop DC 0 AC 1
Rtop vfbtop fb {RTOP}
{cff_line}
Rbot fb 0 {RBOT}
* gm error amplifier (fb vs Vref as AC ground), internal type-II equivalent
Gea vc 0 fb 0 {GM}
Rea vc 0 {RO}
Rc vc cz {RC}
Cc cz 0 {CC}
Cp vc 0 {CP}
* sampling double pole on the control path
Ebuf vcb 0 vc 0 1
Rs vcb s1 {Rs}
Ls s1 s2 {Ls}
Cs s2 0 {Cs}
* PCM average power stage: iL = Vc/Ri into COUT||load
Gpwr 0 vout s2 0 {1/RI}
Cout vout ce {COUT}
Resr ce 0 {ESR}
Rload vout 0 {rload}
.ac dec 200 100 1meg
.control
run
let T = -v(vout)/v(vfbtop)
wrdata {HERE}/_ac.txt vdb(vout) vdb(vfbtop) vp(vout) vp(vfbtop)
.endc
.end
"""
def run(vin, rload, cff):
open(os.path.join(HERE, "_loop.cir"), "w").write(netlist(vin, rload, cff))
subprocess.run(["ngspice", "-b", os.path.join(HERE, "_loop.cir")], capture_output=True, check=True)
d = np.loadtxt(os.path.join(HERE, "_ac.txt"))
f = d[:, 0]
H = 10 ** ((d[:, 1] - d[:, 3]) / 20) * np.exp(1j * (d[:, 5] - d[:, 7]))
T = -H # vout = -T * vfbtop at the injection point
mag_db = 20 * np.log10(np.abs(T))
ph = np.degrees(np.angle(T)) # wrapped to (-180, 180]; PM = 180 + angle at crossover
i = np.where(np.diff(np.sign(mag_db)) < 0)[0]
if not len(i):
return f, mag_db, ph, None, None
k = i[0]
fc = f[k] + (f[k + 1] - f[k]) * (0 - mag_db[k]) / (mag_db[k + 1] - mag_db[k])
phc = ph[k]
return f, mag_db, ph, fc, 180 + phc if phc < 0 else 180 - phc
results = []
curves = {}
for cff in (0, 47, 68, 100):
for vin in (9, 12, 16):
for rload in (5.0, 10.0):
f, m, p, fc, pm = run(vin, rload, cff)
results.append({"cff_pF": cff, "vin": vin, "iout": VOUT / rload,
"fc_kHz": round(fc / 1e3, 2) if fc else None, "pm_deg": round(pm, 1) if pm else None})
if rload == 5.0 and vin == 12:
curves[cff] = (f, m, p)
json.dump({"model": {"Ri": RI, "gm": GM, "Rc": RC, "Cc": CC, "Cp": CP, "gmRc_over_Ri": GMRC / RI,
"calibration": "eq 14, no Cff"}, "results": results},
open(os.path.join(HERE, "loop.json"), "w"), indent=1)
for r in results:
print(r)
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, (a1, a2) = plt.subplots(2, 1, figsize=(10, 7), sharex=True)
for cff, (f, m, p) in curves.items():
a1.semilogx(f, m, label=f"Cff {cff} pF")
a2.semilogx(f, 180 + p, label=f"Cff {cff} pF")
a1.axhline(0, color="k", lw=0.6); a1.set_ylabel("|T| dB"); a1.legend(); a1.grid(which="both", alpha=0.3)
a2.set_ylabel("phase margin curve (180+∠T) deg"); a2.set_xlabel("Hz"); a2.grid(which="both", alpha=0.3)
a1.set_title("TPS54202 5 V / 1 A loop gain, VIN 12 V (ngspice, PCM average model calibrated to datasheet eq 14)")
fig.tight_layout(); fig.savefig(os.path.join(HERE, "loop.png"), dpi=120)