main
ae007 Project page: AFS_Adom rev A (Rice Eclipse AFS clean-room AI redesign): highlight reel and narrated process video with… cbe460e 7d ago
"""AFS_Adom power-path simulation (ngspice): ideal-diode OR of VBAT and VBUS into VSYS, both bucks as averaged models.

Own behavioural model (not a vendor model): each LTC4359 + DMN3404L-7 ideal diode is a voltage-controlled switch that
closes when its input exceeds VSYS by the 30 mV regulation point (LTC4359 Rev C "forward regulation") and opens on reverse
current; the AP63203/AP63205 bucks are averaged (ideal) converters with the real output filter and a 3.8 V UVLO.
Checks: VSYS never falls below the 3.8 V buck UVLO while the battery is unplugged with USB present, and the 3.3 V rail
stays inside +/-3 % through every source change (no RP2350 reset).  Reads design/calcs.json. Writes powerpath.json + PNG.
"""
import json, subprocess, os, math
here = os.path.dirname(os.path.abspath(__file__))
calcs = json.load(open(os.path.join(here, '..', 'design', 'calcs.json')))
# switchover scenario: the board's own loads (headers unloaded: a USB-only session cannot carry the 1 A / 0.5 A header budgets)
L33 = calcs['blocks']['power_budget']['loads_3V3_A']; L5 = calcs['blocks']['power_budget']['loads_5V_A']
I3V3 = sum(v for k, v in L33.items() if 'budget' not in k)
I5V = sum(v for k, v in L5.items() if 'budget' not in k)
RDS = calcs['blocks']['ideal_diode']['rds_hot_ohm']

net = f"""* AFS_Adom power path: VBAT and VBUS ideal-diode OR into VSYS, averaged bucks
.param rds={RDS} vreg=0.03
* sources: battery present 0..30 ms, unplugged at 30 ms (hot removal with USB present), replugged at 60 ms; USB present whole time
Vbat_src vbat_src 0 PWL(0 8.0 30m 8.0 30.001m 0 60m 0 60.001m 8.0 100m 8.0)
Rbat vbat_src vbat_in 0.05
Rshunt vbat_in vbat 0.005
Cbulk vbat 0 440u ic=8
Rcb vbat 0 1meg
Vbus_src vbus_src 0 PWL(0 5.0 100m 5.0)
Rbus vbus_src vbus 0.3
Cbus vbus 0 10u ic=5
* ideal diodes as smooth behavioural conductances: forward current only above the 30 mV regulation point, through rds(on)
Bbat vbat vsys I = (v(vbat) - v(vsys) > vreg) ? (v(vbat) - v(vsys) - vreg)/rds : 0
Busb vbus vsys I = (v(vbus) - v(vsys) > vreg) ? (v(vbus) - v(vsys) - vreg)/rds : 0
Cvsys vsys 0 20u ic=8
* averaged bucks: input current = Pout/(Vin*eff); outputs through the real L/C filter with a UVLO
.param eff=0.88
Bin3 vsys 0 I = (v(vsys) > 3.8) ? (3.3*{I3V3})/(v(vsys)*eff) : 0
Bin5 vsys 0 I = (v(vsys) > 3.8) ? (v(v5)*{I5V})/(v(vsys)*eff) : 0
* 3.3 V averaged converter: ideal source behind the inductor DCR, UVLO at 3.8 V, output clamped to vin
B33 n33 0 V = (v(vsys) > 3.8) ? min(3.3, v(vsys) - 0.1) : 0
L33 n33 v33 4.7u
C33 v33 0 26u ic=3.3
R33 v33 0 {{3.3/{I3V3}}}
B5 n5 0 V = (v(vsys) > 3.8) ? min(5.0, v(vsys) - 0.1) : 0
L5 n5 v5 4.7u
C5 v5 0 26u ic=5
R5 v5 0 {{5.0/{I5V}}}
.tran 20u 100m 0 20u uic
.option method=gear reltol=1e-3 abstol=1e-9 vntol=1e-6 itl4=100
.control
run
wrdata {os.path.join(here, 'powerpath.dat')} v(vbat) v(vbus) v(vsys) v(v33) v(v5)
.endc
.end
"""
cir = os.path.join(here, 'powerpath.cir')
open(cir, 'w').write(net)
r = subprocess.run(['ngspice', '-b', cir], capture_output=True, text=True)
open(os.path.join(here, 'powerpath.log'), 'w').write(r.stdout + r.stderr)
rows = []
for line in open(os.path.join(here, 'powerpath.dat')):
    f = line.split()
    if len(f) >= 10:
        rows.append([float(x) for x in f])
t = [r[0] for r in rows]; vbat = [r[1] for r in rows]; vbus = [r[3] for r in rows]; vsys = [r[5] for r in rows]; v33 = [r[7] for r in rows]; v5 = [r[9] for r in rows]
def window(a, t0, t1):
    return [v for tt, v in zip(t, a) if t0 <= tt <= t1]
# after the initial settling (2 ms), through the unplug (30 ms) and replug (60 ms)
vsys_min = min(window(vsys, 2e-3, 100e-3)); v33_min = min(window(v33, 2e-3, 100e-3)); v33_max = max(window(v33, 2e-3, 100e-3))
vsys_batt = sum(window(vsys, 20e-3, 29e-3)) / len(window(vsys, 20e-3, 29e-3))
vsys_usb = sum(window(vsys, 45e-3, 59e-3)) / len(window(vsys, 45e-3, 59e-3))
v5_usb = sum(window(v5, 45e-3, 59e-3)) / len(window(v5, 45e-3, 59e-3))
checks = [
    {"name": "vsys_min_V_during_source_change", "value": round(vsys_min, 3), "limit": {"min": 3.8}, "pass": vsys_min >= 3.8},
    {"name": "v3v3_min_V", "value": round(v33_min, 3), "limit": {"min": 3.3 * 0.97}, "pass": v33_min >= 3.3 * 0.97},
    {"name": "v3v3_max_V", "value": round(v33_max, 3), "limit": 3.3 * 1.03, "pass": v33_max <= 3.3 * 1.03},
    {"name": "vsys_on_battery_V", "value": round(vsys_batt, 3), "limit": {"min": 7.5}, "pass": vsys_batt >= 7.5},
    {"name": "vsys_on_usb_only_V", "value": round(vsys_usb, 3), "limit": {"min": 4.4}, "pass": vsys_usb >= 4.4},
    {"name": "v5_rail_on_usb_only_V", "value": round(v5_usb, 3), "limit": {"min": 4.5}, "pass": v5_usb >= 4.5},
]
res = {"model": "own behavioural ngspice model (LTC4359 as a 30 mV ideal-diode switch, bucks averaged with UVLO 3.8 V); not a vendor model",
       "scenario": "VBAT 8.0 V unplugged at 30 ms and replugged at 60 ms with VBUS 5.0 V present throughout; board loads from calcs.json with the user headers unloaded (I3V3=%.2f A, I5V=%.3f A)" % (I3V3, I5V),
       "checks": checks, "pass": all(c["pass"] for c in checks), "ngspice": r.returncode}
json.dump(res, open(os.path.join(here, 'powerpath.json'), 'w'), indent=1)
print(json.dumps(res, indent=1))
import sys; sys.path.insert(0, here); from svgplot import plot
tm = [x * 1e3 for x in t]
plot(os.path.join(here, 'powerpath.svg'), 'AFS_Adom power path: battery unplug at 30 ms, replug at 60 ms, USB present (own ngspice model, board loads %.2f A / %.2f A)' % (I3V3, I5V), 'ms', 'V',
     [('VBAT', tm, vbat), ('VBUS', tm, vbus), ('VSYS', tm, vsys), ('+3V3', tm, v33), ('+5V', tm, v5)])