#!/usr/bin/env python3 """v6 T3a - generate the ZED-F9P and generic SOT-23-5 LDO symbols into Drogheda-sym.kicad_sym. ZED-F9P pin assignment is transcribed from the u-blox datasheet Table 11 (ZED-F9P-15B_DataSheet_UBX-23009090, fetched 2026-08-24). All 54 pins present. !! PRE-FAB CHECK: confirm the -04B datasheet (UBX-...) carries the SAME pin assignment. u-blox ZED-F9P variants are believed pin-identical but this MUST be verified on paper. Usage: python3 2026-08-24_v6_T3a_gen_symbols.py [--apply] """ import os, sys, shutil LIB = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '03-Architecture-Hardware', 'kicad', 'Drogheda-sym.kicad_sym') APPLY = '--apply' in sys.argv # (number, name, electrical type) - u-blox datasheet Table 11 F9P = [ (1,'GND','power_in'), (2,'RF_IN','input'), (3,'GND','power_in'), (4,'ANT_DETECT','input'), (5,'ANT_OFF','output'), (6,'ANT_SHORT_N','input'), (7,'VCC_RF','power_out'), (8,'~','passive'), (9,'~','passive'), (10,'~','passive'), (11,'~','passive'), (12,'GND','power_in'), (13,'~','passive'), (14,'GND','power_in'), (15,'~','passive'), (16,'~','passive'), (17,'~','passive'), (18,'~','passive'), (19,'GEOFENCE_STAT','output'),(20,'RTK_STAT','output'), (21,'~','passive'), (22,'~','passive'), (23,'~','passive'), (24,'~','passive'), (25,'~','passive'), (26,'RXD2','input'), (27,'TXD2','output'), (28,'~','passive'), (29,'~','passive'), (30,'~','passive'), (31,'~','passive'), (32,'GND','power_in'), (33,'VCC','power_in'), (34,'VCC','power_in'), (35,'~','passive'), (36,'V_BCKP','power_in'), (37,'GND','power_in'), (38,'V_USB','power_in'), (39,'USB_DM','bidirectional'), (40,'USB_DP','bidirectional'),(41,'GND','power_in'), (42,'TXD','output'), (43,'RXD','input'), (44,'SDA','bidirectional'), (45,'SCL','bidirectional'), (46,'TX_READY','output'), (47,'D_SEL','input'), (48,'GND','power_in'), (49,'RESET_N','input'), (50,'SAFEBOOT_N','input'), (51,'EXTINT','input'), (52,'~','passive'), (53,'TIMEPULSE','output'), (54,'~','passive'), ] LEFT = [33,34,36,38,7,2,4,5,6,1,3,12,14,32,37,41,48] # power / RF / antenna RIGHT = [42,43,26,27,44,45,39,40,46,47,49,50,51,53,19,20] # interfaces / control / status BOTTOM = [n for n,_,_ in F9P if n not in LEFT and n not in RIGHT] # the 21 reserved pins assert len(LEFT)+len(RIGHT)+len(BOTTOM) == 54, (len(LEFT), len(RIGHT), len(BOTTOM)) NAME = {n: nm for n, nm, _ in F9P} TYPE = {n: t for n, _, t in F9P} # reserved pins get a unique display name so KiCad does not merge them visually def disp(n): return 'RSVD%d' % n if NAME[n] == '~' else NAME[n] P = 2.54 HW, HH = 27.94, 25.4 # half width / half height of the body def pin(n, x, y, rot): return (' (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 "%d"\n (effects (font (size 1.27 1.27)))\n )\n )\n' ) % (TYPE[n], fmt(x), fmt(y), rot, disp(n), n) def fmt(v): v = 0.0 if abs(v) < 1e-9 else v # never emit 3.55e-15 - KiCad may reject exponents return ('%.10g' % v) body = [] y = (len(LEFT)-1)*P/2 for n in LEFT: body.append(pin(n, -(HW+5.08), y, 0)); y -= P y = (len(RIGHT)-1)*P/2 for n in RIGHT: body.append(pin(n, HW+5.08, y, 180)); y -= P x = -(len(BOTTOM)-1)*P/2 for n in BOTTOM: body.append(pin(n, x, -(HH+5.08), 90)); x += P f9p = ''' (symbol "ZED-F9P" (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "U" (at 0 %s 0) (effects (font (size 1.27 1.27))) ) (property "Value" "ZED-F9P-04B" (at 0 %s 0) (effects (font (size 1.27 1.27))) ) (property "Footprint" "Drogheda:u-blox_ZED-F9P_LGA54" (at 0 -5.08 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "Datasheet" "https://content.u-blox.com/sites/default/files/documents/ZED-F9P-15B_DataSheet_UBX-23009090.pdf" (at 0 -7.62 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "Description" "u-blox ZED-F9P multi-band L1/L2 GNSS receiver with RTK, 54-pin LGA 22.0 x 17.0 x 2.4 mm. NO internal LNA - an active antenna is mandatory. VCC 2.7-3.6 V, VCC ramp rate <= 20 us/V (damage limit). VCC_RF = VCC-0.1 V, 50 mA typ / 300 mA abs max. RESET_N needs a 7-13 kOhm pull-up. D_SEL open/high = UART. Reserved pins: leave open. LCSC C5120257." (at 0 -10.16 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "LCSC" "C5120257" (at 0 -12.7 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "ki_keywords" "ublox zed f9p gnss rtk gps l1 l2 receiver module" (at 0 0 0) (effects (font (size 1.27 1.27))) (hide yes) ) (symbol "ZED-F9P_0_1" (rectangle (start %s %s) (end %s %s) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "ZED-F9P_1_1" %s ) ) ''' % (fmt(HH+2.54), fmt(HH+5.08), fmt(-HW), fmt(HH), fmt(HW), fmt(-HH), ''.join(body)) ldo = ''' (symbol "LDO_SOT23-5" (exclude_from_sim no) (in_bom yes) (on_board yes) (property "Reference" "U" (at 0 6.35 0) (effects (font (size 1.27 1.27))) ) (property "Value" "LDO_SOT23-5" (at 0 3.81 0) (effects (font (size 1.27 1.27))) ) (property "Footprint" "Package_TO_SOT_SMD:SOT-23-5" (at 0 -6.35 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "Datasheet" "" (at 0 -8.89 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "Description" "Generic SOT-23-5 LDO, AP2112K / TPS7A20 pinout: 1=VIN 2=GND 3=EN 4=NC/NR 5=VOUT. Used on v6 as the low-noise GNSS rail regulator (U21). Verify the chosen part matches this pinout AND meets the ZED-F9P VCC ramp limit of 20 us/V." (at 0 -11.43 0) (effects (font (size 1.27 1.27))) (hide yes) ) (property "ki_keywords" "ldo regulator linear sot23-5 low noise" (at 0 0 0) (effects (font (size 1.27 1.27))) (hide yes) ) (symbol "LDO_SOT23-5_0_1" (rectangle (start -7.62 2.54) (end 7.62 -5.08) (stroke (width 0.254) (type default)) (fill (type background)) ) ) (symbol "LDO_SOT23-5_1_1" (pin power_in line (at -12.7 0 0) (length 5.08) (name "VIN" (effects (font (size 1.27 1.27))) ) (number "1" (effects (font (size 1.27 1.27))) ) ) (pin power_in line (at 0 -10.16 90) (length 5.08) (name "GND" (effects (font (size 1.27 1.27))) ) (number "2" (effects (font (size 1.27 1.27))) ) ) (pin input line (at -12.7 -2.54 0) (length 5.08) (name "EN" (effects (font (size 1.27 1.27))) ) (number "3" (effects (font (size 1.27 1.27))) ) ) (pin passive line (at 12.7 -2.54 180) (length 5.08) (name "NC_NR" (effects (font (size 1.27 1.27))) ) (number "4" (effects (font (size 1.27 1.27))) ) ) (pin power_out line (at 12.7 0 180) (length 5.08) (name "VOUT" (effects (font (size 1.27 1.27))) ) (number "5" (effects (font (size 1.27 1.27))) ) ) ) ) ''' src = open(LIB, encoding='utf-8').read() for nm in ('"ZED-F9P"', '"LDO_SOT23-5"'): if '(symbol %s' % nm in src: sys.exit('REFUSING: symbol %s already present in the library' % nm) end = src.rstrip() assert end.endswith(')'), 'library does not end with a closing paren' new = end[:-1].rstrip('\n') + '\n' + f9p + ldo + ')\n' print('ZED-F9P pins: left=%d right=%d bottom(reserved)=%d total=%d' % (len(LEFT), len(RIGHT), len(BOTTOM), len(LEFT)+len(RIGHT)+len(BOTTOM))) print('reserved pin numbers:', BOTTOM) print('body %g x %g mm, symbol bytes=%d' % (HW*2, HH*2, len(f9p))) 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 print('paren balance of new library:', bal(new)) if not APPLY: print('\nDRY RUN - nothing written. Re-run with --apply.') sys.exit(0) if bal(new) != 0: sys.exit('REFUSING: paren imbalance') shutil.copy2(LIB, LIB + '.T3abak') open(LIB, 'w', encoding='utf-8').write(new) print('\nAPPLIED. Backup: Drogheda-sym.kicad_sym.T3abak')