v6 T9 power input (submodule b92a187) + T9 script

This commit is contained in:
Leopoldo Rodríguez 2026-08-31 21:09:09 +00:00
parent a29f65d953
commit 0823de6c29
2 changed files with 341 additions and 1 deletions

@ -1 +1 @@
Subproject commit ab2e67ab20986ff3068355eb97bc2acdfd0e92bb Subproject commit b92a18710b694d80c59c840183378a00efddc3ac

View File

@ -0,0 +1,340 @@
#!/usr/bin/env python3
"""v6 T9 - Block T: on-board vehicle power input 9-36V -> 5V/3A (replaces external
Mean Well SD-15B-5, 18-36V 5V/3A).
Chain: /24V_IN (P1, existing SMBJ33A D10 at entry) -> F2 2A fuse -> Q23 IRFR5305
reverse-polarity P-FET (D=battery, S=load; R119 100k G->GND, D33 BZX84C15 G->S clamp)
-> /VIN_PROT (SMCJ33A D32 + 2x2.2uF/100V + 100uF/50V bulk) -> U25 TPS54360B (60V 3.5A,
600kHz) -> L22 SRP1265A-100M 10uH -> /VEH5V (LTC4040 input; P2 Meanwell jack stays as
bench backup - populate one source at a time).
Values = TI TPS54360B datasheet 5V/3.5A design example: RT 162k (600kHz), FB 53.6k/10.2k
(5.00V), COMP 13k+6.8nF+39pF, boot 100nF, catch B560C, Cout 2x47uF. EN floating =
enabled (int. UVLO 4.3V, max cranking ride-through; Vin(min) for 5V reg = 5.56V);
optional UVLO divider R124 523k / R125 84.5k fitted DNP.
Also REPAIRS the corrupted D_Zener_ANK pin numbers (@@@A1@@/@@@K3@@ -> 1/3) so DzG1
finally connects to its SOT-23 pads, and re-adds the RF / PWR_MODEM netclasses that were
lost from the .kicad_pro, plus new PWR_IN class.
Prereq: easyeda2kicad --full --lcsc_id C840531 (Bourns SRP1265A-100M) into Drogheda-v6
libs, so the L22 footprint exists. Run with --tbd to skip that check (footprint TBD).
Usage: python3 2026-08-31_v6_T9_pwr_input.py [--apply] [--tbd]
"""
import os, sys, shutil, uuid, re, json, glob
KI = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '03-Architecture-Hardware', 'kicad')
SCH = os.path.join(KI, 'hbox-watchdog-v6.kicad_sch')
DSYM= os.path.join(KI, 'Drogheda-sym.kicad_sym')
PRO = os.path.join(KI, 'hbox-watchdog-v6.kicad_pro')
APPLY = '--apply' in sys.argv
TBD = '--tbd' in sys.argv
ROOT = '/561403b3-895e-4153-a7be-3876ae930d43'
NC = '<NC>'
def f(v):
v = 0.0 if abs(v) < 1e-9 else v
return '%.10g' % v
def bal(t):
d=0; instr=False; esc=False
for ch in t:
if esc: esc=False; continue
if ch=='\\': esc=True; continue
if ch=='"': instr = not instr; continue
if instr: continue
if ch=='(': d+=1
elif ch==')': d-=1
return d
def span_from(text, i):
d=0; instr=False; esc=False; j=i
while j < len(text):
ch=text[j]
if esc: esc=False
elif ch=='\\': esc=True
elif ch=='"': instr = not instr
elif not instr:
if ch=='(': d+=1
elif ch==')':
d-=1
if d==0: return j+1
j+=1
raise ValueError('unbalanced')
PIN_RE2= re.compile(r'\(pin \w+ \w+\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)(?:(?!\(pin ).)*?\(number "([^"]+)"', re.S)
def libpins(libsrc, name):
i = libsrc.find('(symbol "%s"' % name)
if i < 0: return None
j = libsrc.find('\n (symbol "', i+10)
if j < 0: j = libsrc.find('\n\t\t(symbol "', i+10)
body = libsrc[i: j if j>0 else len(libsrc)]
pins = PIN_RE2.findall(body)
return [(num, float(x), float(y)) for x,y,rot,num in pins]
sch = open(SCH, encoding='utf-8').read()
dsym = open(DSYM, encoding='utf-8').read()
# UUID integrity guard: any change to the set of UUIDs already present = abort at write
uuids_before = set(re.findall(r'\(uuid "([0-9a-f-]+)"\)', sch))
# ---------- 0a. repair corrupted D_Zener_ANK pin numbers (cache + DzG1 instance) ----------
zfix = sch.count('"@@@A1@@"') + sch.count('"@@@K3@@"')
sch = sch.replace('"@@@A1@@"', '"1"').replace('"@@@K3@@"', '"3"')
# ---------- 0b. new symbols into Drogheda-sym ----------
def simple_sym(name, desc, fp, pins, w=7.62, h=8.89):
s = ' (symbol "%s"\n (exclude_from_sim no)\n (in_bom yes)\n (on_board yes)\n' % name
for pn,pv,px,py,hide in [('Reference','U',0,h+3.81,''),('Value',name,0,h+1.27,''),
('Footprint',fp,0,-h-2.54,'\n (hide yes)'),
('Datasheet','~',0,-h-5.08,'\n (hide yes)'),
('Description',desc,0,-h-7.62,'\n (hide yes)')]:
s += (' (property "%s" "%s"\n (at %s %s 0)%s\n (effects (font (size 1.27 1.27)))\n )\n'
) % (pn,pv,f(px),f(py),hide)
s += (' (symbol "%s_0_1"\n (rectangle\n (start %s %s)\n (end %s %s)\n'
' (stroke (width 0.254) (type default))\n (fill (type background))\n )\n )\n'
' (symbol "%s_1_1"\n') % (name,f(-w),f(h),f(w),f(-h),name)
for num,nm,tp,px,py,rot in pins:
s += (' (pin %s line\n (at %s %s %d)\n (length 5.08)\n'
' (name "%s"\n (effects (font (size 1.27 1.27)))\n )\n'
' (number "%s"\n (effects (font (size 1.27 1.27)))\n )\n )\n') % (tp,f(px),f(py),rot,nm,num)
s += ' )\n )\n'
return s
TPS_PINS=[('2','VIN','power_in',-12.7,5.08,0),('3','EN','input',-12.7,0,0),('4','RT/CLK','passive',-12.7,-5.08,0),
('1','BOOT','passive',12.7,5.08,180),('8','SW','passive',12.7,2.54,180),
('5','FB','passive',12.7,-2.54,180),('6','COMP','passive',12.7,-5.08,180),
('7','GND','power_in',-2.54,-13.97,90),('9','EP','passive',2.54,-13.97,90)]
TPS = simple_sym('TPS54360B','TI 60V 3.5A step-down, SOIC-8-EP (DDA). LCSC C524806. 4.5-60V in, 0.8V ref.',
'Drogheda-v6:SO-8_L4.9-W3.9-P1.27-LS6.0-BL-EP', TPS_PINS)
FUSE_PINS=[('1','~','passive',0,8.89,270),('2','~','passive',0,-8.89,90)]
FUS = simple_sym('Fuse_CER','Ceramic SMD fuse (one-time). Distinct from PPTC Polyfuse.',
'Fuse:Fuse_1812_4532Metric', FUSE_PINS, w=2.54, h=3.81)
added_syms=[]
for name, body in (('TPS54360B',TPS),('Fuse_CER',FUS)):
if '(symbol "%s"' % name not in dsym:
end = dsym.rstrip(); assert end.endswith(')')
dsym = end[:-1].rstrip('\n') + '\n' + body + ')\n'
added_syms.append(name)
# ---------- 1. cache new symbols into schematic lib_symbols ----------
def grab(libsrc, name, prefix):
i = libsrc.find(' (symbol "%s"\n' % name)
if i < 0: sys.exit('symbol %s not found in source lib' % name)
j = libsrc.index('(', i)
k = span_from(libsrc, j)
return libsrc[j:k].replace('(symbol "%s"' % name, '(symbol "%s:%s"' % (prefix,name), 1)
cache_add = ''
for nm in ('TPS54360B','Fuse_CER'):
if '(symbol "Drogheda-sym:%s"' % nm not in sch:
cache_add += '\n\t\t' + grab(dsym, nm, 'Drogheda-sym')
m = re.search(r'\n\t\(lib_symbols\n', sch)
k = span_from(sch, sch.index('(', m.start()))
sch = sch[:k-1] + cache_add + '\n\t' + sch[k-1:]
lib_cached = sch.split('(lib_symbols',1)[1]
def cachedpins(name): return libpins(lib_cached, name)
# ---------- 2. L22 footprint: resolve from Drogheda-v6.pretty ----------
l22_fp = 'Drogheda-v6:IND-SMD_SRP1265A_TBD'
hits = [p for p in glob.glob(os.path.join(KI,'Drogheda-v6.pretty','*.kicad_mod'))
if re.search(r'1265|SRP|IND-SMD_L13', os.path.basename(p), re.I)]
if hits:
l22_fp = 'Drogheda-v6:' + os.path.basename(hits[0])[:-len('.kicad_mod')]
elif not TBD:
sys.exit('L22 footprint not found in Drogheda-v6.pretty - run on the Mac first:\n'
' easyeda2kicad --full --lcsc_id C840531 --output ~/Documents/Claude/Projects/Drogheda/03-Architecture-Hardware/kicad/Drogheda-v6\n'
'or re-run this script with --tbd to use a placeholder footprint.')
# ---------- 3. guards: refdes free? nets/labels/wires/pins ----------
inst = {}
for b in re.finditer(r'\n\t\(symbol\n(?:(?!\n\t\(symbol\n).)*', sch, re.S):
blk=b.group(0)
r=re.search(r'\(property "Reference" "([^"]+)"',blk); l=re.search(r'\(lib_id "([^"]+)"',blk); a=re.search(r'\(at ([-\d.]+) ([-\d.]+) (\d+)\)',blk)
if r and l and a: inst[r.group(1)]=(l.group(1),float(a.group(1)),float(a.group(2)))
NEWREFS = ['F2','Q23','D32','D33','D34','L22','U25','PF19',
'R119','R120','R121','R122','R123','R124','R125',
'C149','C150','C151','C152','C153','C154','C155','C156']
clash = [r for r in NEWREFS if r in inst]
if clash: sys.exit('refdes already in use: %s' % clash)
def find_labels(text):
d={}
for m in re.finditer(r'\n\t\((label|global_label) "([^"]+)"\s*\n\s*\(at ([-\d.]+) ([-\d.]+) (\d+)\)', text):
d[(round(float(m.group(3)),3), round(float(m.group(4)),3))] = (m.group(2), m.start()+1)
return d
wire_vts = set()
for m in re.finditer(r'\(wire\s*\n\s*\(pts\s*\n((?:\s*\(xy [-\d.]+ [-\d.]+\)\s*\n?)+)', sch):
for a,b in re.findall(r'\(xy ([-\d.]+) ([-\d.]+)\)', m.group(1)):
wire_vts.add((round(float(a),3), round(float(b),3)))
pin_coords = set()
for ref,(lb_,X_,Y_) in inst.items():
for num,px,py in (cachedpins(lb_) or []):
pin_coords.add((round(X_+px,3), round(Y_-py,3)))
# ---------- 4. build additions ----------
add = ''
nlab = nnc = 0
newlabels = []
def add_lab(net,x,y,rot,just):
global add,nlab
add += ('\n\t(label "%s"\n\t\t(at %s %s %d)\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.27 1.27)\n'
'\t\t\t)\n\t\t\t(justify %s)\n\t\t)\n\t\t(uuid "%s")\n\t)') % (net,f(x),f(y),rot,just,uuid.uuid4())
nlab += 1; newlabels.append((net,round(x,3),round(y,3)))
def add_nc(x,y):
global add,nnc
add += '\n\t(no_connect\n\t\t(at %s %s)\n\t\t(uuid "%s")\n\t)' % (f(x),f(y),uuid.uuid4())
nnc += 1
def mksym(lib_id, ref, val, x, y, fp, npins, lcsc='', dnp=False):
props=[('Reference',ref,x-3,y-3,''),('Value',val,x+3,y-3,''),('Footprint',fp,x,y,'\n\t\t\t(hide yes)'),
('Datasheet','~',x,y,'\n\t\t\t(hide yes)'),('Description','',x,y,'\n\t\t\t(hide yes)')]
if lcsc: props.append(('LCSC',lcsc,x,y,'\n\t\t\t(hide yes)'))
p=''
for nm,v,px,py,hide in props:
p += ('\t\t(property "%s" "%s"\n\t\t\t(at %s %s 0)%s\n\t\t\t(show_name no)\n\t\t\t(do_not_autoplace no)\n'
'\t\t\t(effects\n\t\t\t\t(font\n\t\t\t\t\t(size 1.27 1.27)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n') % (nm,v,f(px),f(py),hide)
pn=''.join('\t\t(pin "%s"\n\t\t\t(uuid "%s")\n\t\t)\n' % (n,uuid.uuid4()) for n in npins)
return ('\n\t(symbol\n\t\t(lib_id "%s")\n\t\t(at %s %s 0)\n\t\t(unit 1)\n\t\t(body_style 1)\n'
'\t\t(exclude_from_sim no)\n\t\t(in_bom yes)\n\t\t(on_board yes)\n\t\t(in_pos_files yes)\n'
'\t\t(dnp %s)\n\t\t(uuid "%s")\n%s%s\t\t(instances\n\t\t\t(project "hbox-watchdog-v6"\n'
'\t\t\t\t(path "%s"\n\t\t\t\t\t(reference "%s")\n\t\t\t\t\t(unit 1)\n\t\t\t\t)\n\t\t\t)\n\t\t)\n\t)'
) % (lib_id,f(x),f(y),'yes' if dnp else 'no',uuid.uuid4(),p,pn,ROOT,ref)
def orient(px,py):
if abs(px) < 0.01: return 90,'left bottom'
return (0,'left bottom') if px>0 else (180,'right bottom')
def place(lib, ref, val, x, y, fp, nets, lcsc='', dnp=False, src='cache'):
"""nets: dict pin-number -> net (or NC)."""
pp = cachedpins(lib) if src=='cache' else libpins(dsym, lib.split(':')[1])
if not pp: sys.exit('no pins for %s' % lib)
global add
add += mksym(lib,ref,val,x,y,fp,[num for num,_,_ in pp],lcsc,dnp=dnp)
for num,px,py in pp:
rot,just = orient(px,py)
n = nets[num]
if n == NC: add_nc(x+px,y-py)
else: add_lab(n,x+px,y-py,rot,just)
# ----- Block T title -----
add += ('\n\t(text "BLOCK T - VEHICLE POWER INPUT 9-36V -> 5V/3A (replaces Mean Well SD-15B-5). '
'F2+Q23 rev-pol+SMCJ33A -> TPS54360B 600kHz -> VEH5V. P2 = bench backup, one source at a time."\n'
'\t\t(exclude_from_sim no)\n\t\t(at 40 322 0)\n\t\t(effects\n\t\t\t(font\n\t\t\t\t(size 1.6 1.6)\n'
'\t\t\t)\n\t\t\t(justify left bottom)\n\t\t)\n\t\t(uuid "%s")\n\t)') % uuid.uuid4()
# ----- protection front end -----
place('Drogheda-sym:Fuse_CER','F2','2A/63V-slow-1812',55.0,345.0,'Fuse:Fuse_1812_4532Metric',
{'1':'24V_IN','2':'VIN_FUSED'},'TBD-2A-1812',src='dsym')
place('Device:Q_PMOS_GDS','Q23','IRFR5305',72.0,345.0,'Package_TO_SOT_SMD:TO-252-2',
{'1':'Q23_GATE','2':'VIN_FUSED','3':'VIN_PROT'})
place('Device:R','R119','100k',85.0,352.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'Q23_GATE','2':'GND'})
place('Device:D_Zener_ANK','D33','BZX84C15',92.0,352.0,'Package_TO_SOT_SMD:SOT-23',
{'1':'Q23_GATE','3':'VIN_PROT'}) # repaired ANK symbol: 1=A (gate), 3=K (source)
place('Device:D_TVS','D32','SMCJ33A',100.0,352.0,'Diode_SMD:D_SMC',
{'1':'VIN_PROT','2':'GND'},'C699039')
place('Device:C','C149','2.2uF/100V',108.0,352.0,'Capacitor_SMD:C_1210_3225Metric',
{'1':'VIN_PROT','2':'GND'})
place('Device:C','C150','2.2uF/100V',114.0,352.0,'Capacitor_SMD:C_1210_3225Metric',
{'1':'VIN_PROT','2':'GND'})
place('Device:C_Polarized','C151','100uF/50V',121.0,352.0,'Capacitor_SMD:CP_Elec_8x10',
{'1':'VIN_PROT','2':'GND'})
# ----- buck -----
place('Drogheda-sym:TPS54360B','U25','TPS54360B',145.0,348.0,
'Drogheda-v6:SO-8_L4.9-W3.9-P1.27-LS6.0-BL-EP',
{'1':'BOOT_PWRIN','2':'VIN_PROT','3':'EN_PWRIN','4':'RT_PWRIN','5':'FB_PWRIN',
'6':'COMP_PWRIN','7':'GND','8':'SW_PWRIN','9':'GND'},'C524806',src='dsym')
place('Device:C','C152','100nF',162.0,338.0,'Capacitor_SMD:C_0603_1608Metric',
{'1':'BOOT_PWRIN','2':'SW_PWRIN'})
place('Device:D_Schottky','D34','B560C',166.0,352.0,'Diode_SMD:D_SMC',
{'1':'SW_PWRIN','2':'GND'},'C85100') # 1=K on SW, 2=A on GND
place('Device:L','L22','10uH/SRP1265A',174.0,348.0,l22_fp,
{'1':'SW_PWRIN','2':'VEH5V'},'C840531')
place('Device:C','C153','47uF/16V',182.0,352.0,'Capacitor_SMD:C_1210_3225Metric',
{'1':'VEH5V','2':'GND'})
place('Device:C','C154','47uF/16V',188.0,352.0,'Capacitor_SMD:C_1210_3225Metric',
{'1':'VEH5V','2':'GND'})
# ----- control passives -----
place('Device:R','R120','162k/1%',120.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'RT_PWRIN','2':'GND'})
place('Device:R','R124','523k/1%-DNP',128.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'VIN_PROT','2':'EN_PWRIN'},dnp=True)
place('Device:R','R125','84.5k/1%-DNP',134.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'EN_PWRIN','2':'GND'},dnp=True)
place('Device:R','R123','13k',140.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'COMP_PWRIN','2':'COMP_PWRIN_RC'})
place('Device:C','C155','6.8nF',146.0,368.0,'Capacitor_SMD:C_0402_1005Metric',
{'1':'COMP_PWRIN_RC','2':'GND'})
place('Device:C','C156','39pF',152.0,368.0,'Capacitor_SMD:C_0402_1005Metric',
{'1':'COMP_PWRIN','2':'GND'})
place('Device:R','R121','53.6k/1%',160.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'VEH5V','2':'FB_PWRIN'})
place('Device:R','R122','10.2k/1%',166.0,368.0,'Resistor_SMD:R_0603_1608Metric',
{'1':'FB_PWRIN','2':'GND'})
# ----- power flag on the buck input net -----
pf = cachedpins('power:PWR_FLAG')
add += mksym('power:PWR_FLAG','PF19','PWR_FLAG',60.0,332.0,'',[num for num,_,_ in pf])
add_lab('VIN_PROT',60.0+pf[0][1],332.0-pf[0][2],90,'left bottom')
# ---------- 5. collision check ----------
existing = set(find_labels(sch).keys()) | wire_vts | pin_coords
coll = [(n,x,y) for n,x,y in newlabels if (x,y) in existing]
if coll: sys.exit('LABEL COLLISION with existing labels/wires/pins: %s' % coll[:8])
end = sch.rstrip(); assert end.endswith(')')
sch = end[:-1].rstrip('\n') + add + '\n)\n'
# UUID integrity: all pre-existing uuids still present
uuids_after = set(re.findall(r'\(uuid "([0-9a-f-]+)"\)', sch))
lost = uuids_before - uuids_after
if lost: sys.exit('REFUSING: %d pre-existing UUIDs vanished' % len(lost))
# ---------- 6. .kicad_pro netclasses ----------
pro = json.load(open(PRO, encoding='utf-8'))
ns = pro.setdefault('net_settings', {})
classes = ns.setdefault('classes', [])
have = {c.get('name') for c in classes}
def clone_base():
for c in classes:
if c.get('name')=='PWR_MID': return dict(c)
return dict(classes[0])
pro_added=[]
for nm, tw, vd in (('RF',0.35,0.4),('PWR_MODEM',1.5,0.6),('PWR_IN',1.5,0.6)):
if nm not in have:
c = clone_base(); c['name']=nm; c['track_width']=tw
if 'via_diameter' in c and vd: c['via_diameter']=max(c.get('via_diameter',vd), vd)
classes.append(c); pro_added.append(nm)
pats = ns.setdefault('netclass_patterns', [])
patset = {(p['netclass'],p['pattern']) for p in pats}
WANT = ([('RF',n) for n in ('/RF_GNSS','/RF_ANT_GNSS','/RF_LTE','/RF_ANT_LTE')] +
[('PWR_MODEM',n) for n in ('/VBAT_MODEM','/VIN_MODEM','/SW_MODEM')] +
[('PWR_IN',n) for n in ('/VIN_FUSED','/VIN_PROT','/SW_PWRIN')])
pro_pats=0
for cls,net in WANT:
if (cls,net) not in patset:
pats.append({'netclass':cls,'pattern':net}); pro_pats+=1
# ---------- report ----------
print('D_Zener_ANK pin-number repairs (@@@ -> 1/3): %d replacements (expect 4: 2 cache + 2 DzG1 instance)' % zfix)
print('new lib symbols in Drogheda-sym: %s' % (added_syms or 'none (already present)'))
print('L22 footprint: %s' % l22_fp)
print('symbols placed: F2 Q23 D32 D33 D34 L22 U25 PF19 + 15 passives (R124/R125 DNP)')
print('labels: %d no_connects: %d' % (nlab,nnc))
print('netclasses added to pro: %s ; patterns added: %d' % (pro_added or 'none', pro_pats))
print('paren: sch=%d dsym=%d' % (bal(sch),bal(dsym)))
if not APPLY:
print('\nDRY RUN - nothing written. Re-run with --apply.'); sys.exit(0)
if bal(sch)!=0 or bal(dsym)!=0: sys.exit('REFUSING: paren imbalance')
shutil.copy2(SCH,SCH+'.T9bak'); shutil.copy2(DSYM,DSYM+'.T9bak'); shutil.copy2(PRO,PRO+'.T9bak')
open(SCH,'w',encoding='utf-8').write(sch)
open(DSYM,'w',encoding='utf-8').write(dsym)
json.dump(pro, open(PRO,'w',encoding='utf-8'), indent=2)
print('\nAPPLIED. Backups: *.T9bak')