main
Colby Knox Altium Designer build of the TPS54202 buck molecule: Altium project, manufacturing set, STEP and GLB, design, simulatio… 845dd0d 4d ago
#!/usr/bin/env python3
"""Switching transient of the TPS54202 5 V / 1 A design in ngspice (XSPICE latch, cycle by cycle).

Peak-current-mode modulator: a 500 kHz clock sets an SR latch, the latch resets when
Ri*iL + slope ramp >= Vc (or iL hits the 2.5 A minimum high-side limit). Synchronous FETs are
switches with the datasheet Rds(on); the error amplifier is the same calibrated equivalent as
loop.py, with the 5 ms soft start as a ramp on its reference.

Test: VIN 12 V, 10 ohm load plus a switched 10 ohm step (1 A total) from t=0; the step opens 7 to 8 ms: 1 A -> 0.5 A -> 1 A.
Run: python3 transient.py -> transient.json, transient.png
"""
import json, math, os, subprocess
import numpy as np

HERE = os.path.dirname(os.path.abspath(__file__))
calcs = json.load(open(os.path.join(HERE, "..", "design", "calcs.json")))
loop = json.load(open(os.path.join(HERE, "loop.json")))["model"]
VIN = float(os.environ.get("VIN", 12))
VOUT, FSW, L = 5.0, 500e3, 15e-6
RTOP, RBOT = calcs["feedback"]["Rtop"], calcs["feedback"]["Rbot"]
CFF = calcs["cff"]["chosen_pF"] * 1e-12
COUT = calcs["cout"]["effective_uF"] * 1e-6
RI, GM, RC, CC, CP = loop["Ri"], loop["gm"], loop["Rc"], loop["Cc"], loop["Cp"]
SE = 0.5 * VOUT / L * RI                  # slope comp V/s
TSTOP = 9e-3

cir = f"""* TPS54202 switching transient
.option method=gear reltol=1e-3
Vsrc src 0 {VIN}
Rcable src vinc 0.05
Lcable vinc vin 0.5u
Cbulk vin b1 47u
Rbulk b1 0 0.3
Cin vin 0 12u
* power stage
Shs vin sw hg 0 swm_hs
Sls sw 0 lg 0 swm_ls
Dls 0 sw dbody
Lo sw lx {L}
Rdcr lx vout 0.118
Cout vout ce {COUT}
Resr ce 0 0.001
Rload vout 0 10
Rstep vout st 10
Sstep st 0 stp 0 swm_step
Vstp stp 0 PWL(0 1 7m 1 7.0001m 0 8m 0 8.0001m 1)
* feedback
Rtop vout fb {RTOP}
Cff vout fb {CFF}
Rbot fb 0 {RBOT}
* soft-start reference 0 -> 0.596 V over 5 ms
Bref vref 0 V = min(0.596, 0.596*time/5m)
Gea vc 0 fb vref {GM}
Rea vc 0 {1000/GM}
Rc vc cz {RC}
Cc cz 0 {CC}
Cp vc 0 {CP}
* modulator
Vclk clk 0 PULSE(0 1 0 1n 1n 20n {1/FSW})
Bramp ramp 0 V = {SE} * (time - floor(time*{FSW})/{FSW})
Brst rst 0 V = ((i(Vsense)*{RI} + v(ramp) >= v(vc)) || (i(Vsense) >= 2.5)) ? 1 : 0
Vsense lx lxs 0
.model swm_hs sw vt=0.5 vh=0 ron=0.148 roff=1meg
.model swm_ls sw vt=0.5 vh=0 ron=0.078 roff=1meg
.model swm_step sw vt=0.5 vh=0 ron=0.001 roff=1g
.model dbody d is=1e-9 n=1.2 rs=0.05
aset [clk rst] [dset drst] adc
.model adc adc_bridge in_low=0.4 in_high=0.6
apu den pullup
.model pullup d_pullup
alatch dset drst den NULL NULL dq dqb latch
.model latch d_srlatch sr_value=0 rise_delay=5n fall_delay=5n
adac [dq dqb] [q qb] dac
.model dac dac_bridge out_low=0 out_high=1 t_rise=2n t_fall=2n
* 10 ns dead time: gate drives through RC delay
Bhg hg 0 V = v(q) > 0.5 ? 1 : 0
Rqd qb qbd 1k
Cqd qbd 0 10p
Blg lg 0 V = (v(qbd) > 0.9) && (v(q) < 0.5) ? 1 : 0
.tran 10n {TSTOP} 0 10n uic
.control
set wr_singlescale
run
wrdata {HERE}/_tr.txt v(vout) i(Vsense) v(sw) v(vc)
.endc
.end
"""
# Vsense sits in series with the inductor: move the DCR after it
cir = cir.replace("Lo sw lx {L}".format(L=L), f"Lo sw lx {L}")
cir = cir.replace("Rdcr lx vout 0.118", "Rdcr lxs vout 0.118")
open(os.path.join(HERE, "_tr.cir"), "w").write(cir)
r = subprocess.run(["ngspice", "-b", os.path.join(HERE, "_tr.cir")], capture_output=True, text=True)
if r.returncode:
    print(r.stdout[-3000:], r.stderr[-3000:])
    raise SystemExit(1)
d = np.loadtxt(os.path.join(HERE, "_tr.txt"))
t, vo, il, vsw, vc = d[:, 0], d[:, 1], d[:, 2], d[:, 3], d[:, 4]


def win(a, b):
    m = (t >= a) & (t <= b)
    return m


steady = win(6.5e-3, 7.0e-3)
half = win(7.6e-3, 8.0e-3)
res = {
    "vin": VIN,
    "vout_mean_1A": round(float(vo[steady].mean()), 4),
    "vout_ripple_pp_mV_1A": round(float(np.ptp(vo[steady])) * 1e3, 2),
    "il_ripple_pp_A": round(float(np.ptp(il[win(6.9e-3, 7.0e-3)])), 3),
    "il_peak_startup_A": round(float(il[t < 6e-3].max()), 3),
    "vout_overshoot_startup_mV": round(float((vo[t < 6.5e-3].max() - vo[steady].mean()) * 1e3), 1),
    "t_reach_95pct_ms": round(float(t[np.argmax(vo > 0.95 * vo[steady].mean())] * 1e3), 3),
    "step_down_1A_to_0.5A_overshoot_mV": round(float((vo[win(7e-3, 7.5e-3)].max() - vo[steady].mean()) * 1e3), 1),
    "step_up_0.5A_to_1A_undershoot_mV": round(float((vo[half].mean() - vo[win(8e-3, 8.5e-3)].min()) * 1e3), 1),
}
settle = win(8e-3, TSTOP)
ts = t[settle]; err = np.abs(vo[settle] - vo[steady].mean())
out_band = ts[err > 0.01]
res["step_up_settle_1pct_us"] = round(float((out_band.max() - 8e-3) * 1e6), 1) if len(out_band) else 0.0
json.dump(res, open(os.path.join(HERE, f"transient_{int(VIN)}V.json"), "w"), indent=1)
print(json.dumps(res, indent=1))

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots(3, 1, figsize=(11, 8))
ax[0].plot(t * 1e3, vo); ax[0].set_ylabel("VOUT V"); ax[0].set_title(f"TPS54202 12 V→5 V switching transient, VIN {VIN:g} V (ngspice, cycle-by-cycle PCM)")
ax[0].grid(alpha=0.3)
m = win(6.8e-3, 9e-3)
ax[1].plot(t[m] * 1e3, vo[m] * 1e3 - 5000); ax[1].set_ylabel("VOUT − 5 V (mV)"); ax[1].grid(alpha=0.3)
ax[1].axvline(7, color="k", lw=0.5, ls="--"); ax[1].axvline(8, color="k", lw=0.5, ls="--")
ax[1].text(7.02, ax[1].get_ylim()[1] * 0.8, "1 A→0.5 A"); ax[1].text(8.02, ax[1].get_ylim()[1] * 0.8, "0.5 A→1 A")
m2 = win(6.98e-3, 7.0e-3)
ax[2].plot(t[m2] * 1e6, il[m2], label="iL (A)"); ax[2].plot(t[m2] * 1e6, vsw[m2] / VIN, label="SW / VIN", alpha=0.6)
ax[2].set_xlabel("µs"); ax[2].legend(); ax[2].grid(alpha=0.3)
fig.tight_layout(); fig.savefig(os.path.join(HERE, f"transient_{int(VIN)}V.png"), dpi=110)