Drogheda/scripts/2026-08-24_v6_T3a_gen_symbols.py
Claude (Cowork) 3494413eaf v6 T3: Block N - ZED-F9P (U18) + low-noise LDO (U21) placed and wired
Leo generated the vendor part with easyeda2kicad (C5120257) -> Drogheda-v6.kicad_sym /
.pretty / .3dshapes, now registered in sym-lib-table and fp-lib-table.

CROSS-CHECK: the EasyEDA/LCSC symbol independently CONFIRMS the datasheet transcription -
pads 1-54 match the u-blox pin assignment function for function, and the 21 Reserved pads
are identical. Pads 55-102 are 48 extra GND pads (the ground array under the module), which
our symbol was missing; T3a2 adds them stacked on the pin-1 coordinate so one GND label
serves all. Symbol is now 102/102 pins, footprint = Drogheda-v6:LCC-LGA-102_L22.0-W17.0_FM660.

Block N as wired (verified by rebuilding connectivity from label-on-pin geometry):
  GNSS_TX      U16.16 + U18.43        GNSS_RX  U16.17 + U18.42
  DOUT_4G      U18.26 (RXD2, RTCM in - modem end lands in T4)
  USART1_RX    U16.43 + Rdfu1.1 + U18.27 (TXD2 uplink; K1.1 also on this net per the PCB)
  GNSS_RESET_N U16.53 (PC12) + U18.49 + R105 10k pull-up to 3V3_GNSS
  GNSS_TIMEPULSE  U16.23 (PA7) + U18.53
  GNSS_ANT_DETECT U16.9 (PC1) + U18.4
  GNSS_ANT_SHORT_N U16.10 (PC2) + U18.6
  GNSS_RTK_STAT   U16.11 (PC3) + U18.20  <- hardware answer to are-corrections-being-used
  3V3_GNSS = U21.5 + U18.33/34 + C120-C124 + R105; U21 fed from SW_5V (stays MCU-gated)
  V_BCKP   = U18.36 on the UNGATED 3V3 so ephemeris survives a power-cycle
  33 no-connects: the 21 reserved pads plus V_USB, USB_DM/DP, ANT_OFF, SDA, SCL, TX_READY,
  D_SEL (open = UART mode), SAFEBOOT_N, EXTINT, GEOFENCE_STAT and the LDO NC/NR pin.

T3c fixes two coordinates the generators emitted as 3.552713679e-15 (i.e. zero); KiCad is
not guaranteed to accept exponent notation. See the script header for the lesson: a first
attempt used a bare numeric regex over the whole file and corrupted 264 UUIDs, because
segments such as 895e-4153 parse as scientific notation. Reverted from backup and redone
scoped to (at ...) forms only, with a UUID-integrity guard. Verified 2298 UUIDs, 0
malformed, 0 exponent coordinates.

STILL OPEN: the PCB has no U18/U21/passive footprints yet - that is a KiCad
Update-PCB-from-Schematic. And confirm the -04B datasheet pin assignment on paper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJ4eAB6zjQT49RVwpgfKdt
2026-08-24 23:25:44 +00:00

250 lines
8.8 KiB
Python

#!/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')