molecule
AFS_Adom flight computer molecule
Public Unreviewedby ae007
Active fin stabilization flight computer for a sounding rocket, a clean-room AI redesign of the Rice Eclipse AFS board: RP2350B, ICM-45686, KX134-1211, MMC5983MA, MS5611, TMP117, INA219A, 8 servos, 2S LiPo, USB-C; 4-layer Adom molecule, designed end to end by Claude through KiCad and adom-aiflow
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
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
"""AFS_Adom buck simulation (ngspice): cycle-by-cycle switching model of the AP63203 (3.3 V) and AP63205 (5 V) with the
real L/C filter, at VIN 6.0, 7.4 and 8.4 V (2S range) and 5.0 V (USB only), 1 A load, plus a 0.5 A load step.
Own behavioural model: fixed 1.1 MHz clock, peak-current-mode SR latch, switches with the datasheet on-resistances
(AP632xx: HS 80 mOhm / LS 40 mOhm typ.), an error amplifier with an internal-compensation equivalent fitted so the loop
crosses over near the datasheet's typical bandwidth. It compares candidates and shows ripple/overshoot trends; it is not the
vendor model. Reads design/calcs.json; writes buck_<rail>_<vin>.json, buck_ripple.json and PNGs.
"""
import json, subprocess, os
here = os.path.dirname(os.path.abspath(__file__))
calcs = json.load(open(os.path.join(here, '..', 'design', 'calcs.json')))
FSW = 1.1e6
def run(rail, vout, vin, iload, L, cout, istep):
tstop = 2.0e-3
net = f"""* AP632xx behavioural buck {rail} vin={vin}
.param vin={vin} vout={vout} L={L} cout={cout} fsw={FSW} iload={iload}
.param ri=0.3 rhs=0.08 rls=0.04 vref=0.8 gm=600u rc=50k cc=1n
Vin vin 0 DC {{vin}}
* clock
Vclk clk 0 PULSE(0 1 0 2n 2n 40n {{1/fsw}})
* latch set by clock, reset when (iL*ri + ramp) >= vc or iL > 3 A
Aclk [clk] [dclk] adc
Arst [rst] [drst] adc
alatch dclk drst den NULL NULL dq dqb latch
apu den pullup
Aq [dq] [q] dac
.model adc adc_bridge(in_low=0.5 in_high=0.5)
.model dac dac_bridge(out_low=0 out_high=1)
.model latch d_srlatch(set_delay=1n reset_delay=1n)
.model pullup d_pullup(load=1p)
* slope ramp synced to the clock
Bramp ramp 0 V = 0.4*(time*fsw - floor(time*fsw))
Brst rst 0 V = ((i(Vsense)*ri + v(ramp) >= v(vc)) || (i(Vsense) >= 3.0)) ? 1 : 0
* power stage
Shs vin sw q 0 swhs
Sls sw 0 qb 0 swls
Bqb qb 0 V = 1 - v(q)
.model swhs SW(Ron={{rhs}} Roff=1e6 Vt=0.5 Vh=0.1)
.model swls SW(Ron={{rls}} Roff=1e6 Vt=0.5 Vh=0.1)
Lout sw nl {{L}}
Vsense nl vo DC 0
Cout vo 0 {{cout}}
Resr vo 0 1e9
Rload vo 0 {{vout/iload}}
* load step: extra current from 1.0 ms to 1.5 ms
Bstep vo 0 I = (time > 1.0m && time < 1.5m) ? {istep} : 0
* error amp: gm stage with type-II compensation equivalent, soft start on vref
Bvfb fb 0 V = v(vo)*vref/vout
Bss ss 0 V = (time < 0.4m) ? vref*time/0.4m : vref
Gea 0 vc fb ss {{-gm}}
Rea vc vcz {{rc}}
Cea vcz 0 {{cc}}
Cp vc 0 20p
Dclamp vc vcl dclamp
Vcl vcl 0 2.0
.model dclamp D
.tran 10n {tstop} 0 10n uic
.option method=gear reltol=1e-3 abstol=1e-9 vntol=1e-6
.control
run
wrdata {os.path.join(here, f'buck_{rail}_{vin}.dat')} v(vo) i(Vsense) v(sw)
.endc
.end
"""
cir = os.path.join(here, f'buck_{rail}_{vin}.cir')
open(cir, 'w').write(net)
r = subprocess.run(['ngspice', '-b', cir], capture_output=True, text=True, timeout=600)
open(os.path.join(here, f'buck_{rail}_{vin}.log'), 'w').write(r.stdout + r.stderr)
rows = [[float(x) for x in l.split()] for l in open(os.path.join(here, f'buck_{rail}_{vin}.dat')) if len(l.split()) >= 6]
t = [x[0] for x in rows]; vo = [x[1] for x in rows]; il = [x[3] for x in rows]
def win(a, t0, t1): return [v for tt, v in zip(t, a) if t0 <= tt <= t1]
ss = win(vo, 0.8e-3, 0.98e-3); ils = win(il, 0.8e-3, 0.98e-3)
ripple = max(ss) - min(ss); dIL = max(ils) - min(ils); vmean = sum(ss) / len(ss)
step_min = min(win(vo, 1.0e-3, 1.5e-3)); step_max = max(win(vo, 1.5e-3, 1.9e-3))
peak_start = max(win(vo, 0, 0.8e-3)); ipk = max(il)
res = {"rail": rail, "vin": vin, "vout_mean_V": round(vmean, 4), "ripple_mVpp": round(1e3 * ripple, 2), "dIL_App": round(dIL, 3),
"startup_overshoot_V": round(peak_start - vout, 3), "step_deviation_mV": round(1e3 * (vmean - step_min), 1),
"step_recovery_overshoot_mV": round(1e3 * (step_max - vmean), 1), "iL_peak_A": round(ipk, 3), "ngspice": r.returncode}
json.dump(res, open(os.path.join(here, f'buck_{rail}_{vin}.json'), 'w'), 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, f'buck_{rail}_{vin}.svg'), f'{rail} buck, VIN {vin} V, 1 A, +{istep} A step 1.0-1.5 ms (own behavioural model, fitted compensation)', 'ms', 'V / A',
[('Vout (V)', tm, vo), ('IL (A)', tm, il)])
return res
b33 = calcs['blocks']['buck_3v3']; b5 = calcs['blocks']['buck_5v']
cout = b33['Cout_derated_uF'] * 1e-6
results = []
for vin in (6.0, 7.4, 8.4, 5.0):
results.append(run('3v3', 3.3, vin, 1.0, b33['L_uH'] * 1e-6, cout, 0.5))
for vin in (6.0, 7.4, 8.4):
results.append(run('5v', 5.0, vin, 1.1, b5['L_uH'] * 1e-6, cout, 0.5))
checks = []
for r in results:
nom = 3.3 if r['rail'] == '3v3' else 5.0
lim = 20.0 if r['rail'] == '3v3' else 30.0
checks.append({"name": f"{r['rail']}_vin{r['vin']}_ripple_mVpp", "value": r['ripple_mVpp'], "limit": lim, "pass": r['ripple_mVpp'] <= lim})
checks.append({"name": f"{r['rail']}_vin{r['vin']}_regulation_pct", "value": round(100 * (r['vout_mean_V'] - nom) / nom, 2), "limit": {"min": -3, "max": 3}, "pass": abs(r['vout_mean_V'] - nom) / nom <= 0.03})
checks.append({"name": f"{r['rail']}_vin{r['vin']}_iL_peak_A", "value": r['iL_peak_A'], "limit": 4.5, "pass": r['iL_peak_A'] <= 4.5})
# cross-check inductor ripple with calcs.json (within 25 % for the behavioural model)
calc = next((row for row in (b33 if r['rail'] == '3v3' else b5)['rows'] if abs(row['vin'] - r['vin']) < 0.01), None)
if calc and calc['dIL_A'] > 0:
err = abs(r['dIL_App'] - calc['dIL_A']) / calc['dIL_A']
checks.append({"name": f"{r['rail']}_vin{r['vin']}_dIL_vs_calcs_pct", "value": round(100 * err, 1), "limit": 25, "pass": err <= 0.25})
out = {"model": "own behavioural ngspice model: 1.1 MHz peak-current-mode SR latch, datasheet on-resistances, gm error amp with a fitted type-II equivalent (internal compensation not published); not a vendor model",
"calibration": "gm/rc/cc chosen for ~40 kHz crossover; ripple and dIL cross-checked against calcs.json",
"results": results, "checks": checks, "pass": all(c['pass'] for c in checks)}
json.dump(out, open(os.path.join(here, 'buck_ripple.json'), 'w'), indent=1)
print(json.dumps(out['checks'], indent=1)); print('PASS' if out['pass'] else 'FAIL')