diff --git a/03-Architecture-Hardware/kicad b/03-Architecture-Hardware/kicad index ab2e67a..b92a187 160000 --- a/03-Architecture-Hardware/kicad +++ b/03-Architecture-Hardware/kicad @@ -1 +1 @@ -Subproject commit ab2e67ab20986ff3068355eb97bc2acdfd0e92bb +Subproject commit b92a18710b694d80c59c840183378a00efddc3ac diff --git a/scripts/2026-08-31_v6_T9_pwr_input.py b/scripts/2026-08-31_v6_T9_pwr_input.py new file mode 100644 index 0000000..79967a8 --- /dev/null +++ b/scripts/2026-08-31_v6_T9_pwr_input.py @@ -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 = '' + +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')