Add GPS + PPS distribution sheet (all rev-A blocks captured)
- New carrier/GPS_PPS.kicad_sch via tools/build_gps.py: J_GPS 5-pin (PPS + UART2 NMEA + 3V3_AUX + GND) per Pinout_BOM section 6 - PPS chain: GPS_PPS_RAW -> PESD3V3L1BA TVS -> 74LVC1G17 Schmitt (+3V3_AUX) -> PPS_CLEAN -> 33R series -> SYNC_OUT (CM5 pin 18 + J2.6), plus J_PPS1 (UWB) / J_PPS2 (scope) 2-pin taps; board is PPS sink only - NMEA on UART2 (GPIO4/GPIO5) per correction C2; debug console keeps UART0 - Converted SYNC_OUT/GPIO4/GPIO5 local labels on CM5_GPIO to global (names unchanged) to reach the new sheet; root sheet-symbol needs no pins - J_GPS pins 1/2 typed output (GPS drives PPS/TX) for driver-aware checks - Verify: ERC 133->133 zero new; analyzer +73/-0 zero regressions; netlist confirms all cross-sheet nets; SPICE 24 pass / 2 benign warn / 0 fail Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
166
tools/build_gps.py
Normal file
166
tools/build_gps.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""build_gps.py — generate the CM5-carrier GPS + PPS distribution sheet.
|
||||
|
||||
Same connectivity model as build_power.py: every pin gets a coincident named
|
||||
label / GND symbol / no_connect; nets form by name (grid-snapped 1.27mm).
|
||||
|
||||
Frozen spec (CM5_Carrier_Pinout_BOM.md §6, CM5_Carrier_Design.md §5a):
|
||||
J_GPS(5-pin): 1=PPS 2=NMEA_RX(GPS TX -> CM5 GPIO5/UART2 RX)
|
||||
3=NMEA_TX(CM5 GPIO4/UART2 TX -> GPS RX) 4=+3V3_AUX 5=GND
|
||||
GPS_PPS_RAW -> TVS -> 74LVC1G17 Schmitt (3V3) -> PPS_CLEAN
|
||||
-> Rs 33R -> SYNC_OUT (CM5 pin 18, Ethernet_SYNC_OUT, 3.3V bidi, cfg input)
|
||||
-> J_PPS1 (UWB tap) / J_PPS2 (scope tap), 2-pin each
|
||||
Board is always a PPS SINK (GPS-sourced).
|
||||
|
||||
Cross-sheet nets are GLOBAL: +3V3_AUX (Power sheet flag drives it), and
|
||||
GPIO4/GPIO5/SYNC_OUT (their CM5_GPIO local labels were converted to global —
|
||||
net names unchanged; nothing else in the design used those names).
|
||||
J_GPS pins 1/2 are re-typed 'output' (the GPS module drives PPS and TX into
|
||||
the board) so the analyzer's no_driver gate sees real drivers (E-key lesson).
|
||||
"""
|
||||
import re, sys, os, uuid as uuidlib
|
||||
|
||||
ROOT_UUID = "e63e39d7-6ac0-4ffd-8aa3-1841a4541b55"
|
||||
PROJECT = "CM5IO"
|
||||
KSS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
CARRIER = os.path.join(HERE, "..", "carrier")
|
||||
|
||||
def U(label): return str(uuidlib.uuid5(uuidlib.NAMESPACE_DNS, "cm5carrier-gps-" + label))
|
||||
SHEET_SYMBOL_UUID = U("sheet-symbol")
|
||||
SHEET_FILE_UUID = U("sheet-file")
|
||||
|
||||
def g(v): return round(round(v / 1.27) * 1.27, 2)
|
||||
|
||||
def paren_end(t, s):
|
||||
d = 0
|
||||
for i in range(s, len(t)):
|
||||
if t[i] == '(': d += 1
|
||||
elif t[i] == ')':
|
||||
d -= 1
|
||||
if d == 0: return i + 1
|
||||
raise ValueError("unbalanced")
|
||||
|
||||
def get_symbol(text, name):
|
||||
m = re.search(r'\(symbol\s+"' + re.escape(name) + r'"', text)
|
||||
return text[m.start():paren_end(text, m.start())] if m else None
|
||||
|
||||
def lib_pins(seg):
|
||||
out = {}
|
||||
for pm in re.finditer(r'\(pin\b', seg):
|
||||
s = pm.start(); ps = seg[s:paren_end(seg, s)]
|
||||
at = re.search(r'\(at\s+([\d.-]+)\s+([\d.-]+)\s+([\d.-]+)\)', ps)
|
||||
nu = re.search(r'\(number\s+"([^"]+)"', ps)
|
||||
if at and nu:
|
||||
out.setdefault(nu.group(1), (float(at.group(1)), float(at.group(2)), float(at.group(3))))
|
||||
return out
|
||||
|
||||
def set_pin_type(seg, number, newtype):
|
||||
for pm in re.finditer(r'\(pin\s+(\w+)\s+(\w+)', seg):
|
||||
s = pm.start(); blk = seg[s:paren_end(seg, s)]
|
||||
if re.search(r'\(number\s+"' + re.escape(number) + r'"', blk):
|
||||
head = re.match(r'\(pin\s+\w+', blk).group(0)
|
||||
return seg[:s] + blk.replace(head, f'(pin {newtype}', 1) + seg[s + len(blk):]
|
||||
return seg
|
||||
|
||||
# ---- lib_symbol sources -----------------------------------------------------------
|
||||
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
|
||||
LIBS = {}
|
||||
def add_from(lib_id, srctext, srcname=None, retype=None):
|
||||
srcname = srcname or lib_id.split(':')[-1]
|
||||
blk = get_symbol(srctext, lib_id) or get_symbol(srctext, srcname)
|
||||
if not blk: sys.exit("missing lib_symbol " + lib_id)
|
||||
if ('"' + lib_id + '"') not in blk[:60]:
|
||||
blk = blk.replace('(symbol "' + srcname + '"', '(symbol "' + lib_id + '"', 1)
|
||||
for num, ty in (retype or {}).items():
|
||||
blk = set_pin_type(blk, num, ty)
|
||||
LIBS[lib_id] = (blk, lib_pins(blk))
|
||||
|
||||
for lid in ["Device:R", "Device:C", "power:GND"]:
|
||||
add_from(lid, carrier_hs)
|
||||
add_from("Device:D_TVS", open(f"{KSS}/Device.kicad_sym").read(), "D_TVS")
|
||||
# GPS module DRIVES pins 1 (PPS) and 2 (its TX) -> 'output' for driver-aware checks
|
||||
add_from("Connector_Generic:Conn_01x05", open(f"{KSS}/Connector_Generic.kicad_sym").read(),
|
||||
"Conn_01x05", retype={'1': 'output', '2': 'output'})
|
||||
add_from("Connector_Generic:Conn_01x02", open(f"{KSS}/Connector_Generic.kicad_sym").read(), "Conn_01x02")
|
||||
add_from("74xGxx:74LVC1G17", open(f"{KSS}/74xGxx.kicad_sym").read(), "74LVC1G17")
|
||||
|
||||
GLOBAL_NETS = {"+3V3_AUX", "GPIO4", "GPIO5", "SYNC_OUT"}
|
||||
HIER_NETS = set()
|
||||
|
||||
# ---- placement engine (same as build_power.py, + ('nc',) pins) --------------------
|
||||
body = []
|
||||
def emit_label(net, x, y, ang=0):
|
||||
body.append(f'\t(label "{net}"\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("l-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_glabel(net, x, y, ang=0):
|
||||
body.append(f'\t(global_label "{net}"\n\t\t(shape input)\n\t\t(at {x} {y} {ang})\n'
|
||||
f'\t\t(effects (font (size 1.27 1.27)) (justify left))\n\t\t(uuid "{U("gl-"+net+f"-{x}-{y}")}")\n\t)\n')
|
||||
def emit_nc(x, y, tag):
|
||||
body.append(f'\t(no_connect (at {x} {y}) (uuid "{U("nc-"+tag)}"))\n')
|
||||
def emit_gnd(x, y, tag):
|
||||
body.append(f'\t(symbol\n\t\t(lib_id "power:GND")\n\t\t(at {x} {y} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("gnd-"+tag)}")\n'
|
||||
f'\t\t(property "Reference" "#PWR{tag}" (at {x} {y+3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Value" "GND" (at {x} {y+2} 0) (effects (font (size 1.27 1.27))))\n'
|
||||
f'\t\t(property "Footprint" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(pin "1" (uuid "{U("gp-"+tag)}"))\n'
|
||||
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#PWR{tag}") (unit 1))))\n\t)\n')
|
||||
|
||||
def place(lib_id, ref, value, footprint, ox, oy, pin_nets):
|
||||
ox, oy = g(ox), g(oy)
|
||||
_, pins = LIBS[lib_id]
|
||||
extra = ('\t\t(property "Description" "" (at %g %g 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' % (ox, oy))
|
||||
sym = (f'\t(symbol\n\t\t(lib_id "{lib_id}")\n\t\t(at {ox} {oy} 0)\n\t\t(unit 1)\n'
|
||||
f'\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)\n\t\t(uuid "{U("sym-"+ref)}")\n'
|
||||
f'\t\t(property "Reference" "{ref}" (at {ox+2.54} {oy-1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Value" "{value}" (at {ox+2.54} {oy+1} 0) (effects (font (size 1.27 1.27)) (justify left)))\n'
|
||||
f'\t\t(property "Footprint" "{footprint}" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
||||
f'\t\t(property "Datasheet" "" (at {ox} {oy} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n' + extra)
|
||||
for num in sorted(pins, key=lambda n: int(re.sub(r'\D', '', n) or 0)):
|
||||
sym += f'\t\t(pin "{num}" (uuid "{U("p-"+ref+"-"+num)}"))\n'
|
||||
sym += f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "{ref}") (unit 1))))\n\t)\n'
|
||||
body.append(sym)
|
||||
for num, net in pin_nets.items():
|
||||
lx, ly, _ = pins[num]; px, py = round(ox + lx, 2), round(oy - ly, 2)
|
||||
if net == ('gnd',): emit_gnd(px, py, ref + num)
|
||||
elif net == ('nc',): emit_nc(px, py, ref + num)
|
||||
elif isinstance(net, tuple) and net[0] == 'global': emit_glabel(net[1], px, py)
|
||||
elif net in GLOBAL_NETS: emit_glabel(net, px, py)
|
||||
else: emit_label(net, px, py)
|
||||
return ox, oy
|
||||
|
||||
GND = ('gnd',); NC = ('nc',)
|
||||
|
||||
# ---- the GPS + PPS block ----------------------------------------------------------
|
||||
place("Connector_Generic:Conn_01x05", "J_GPS", "GPS_5pin_PPS_UART", "Connector_PinHeader_2.54mm:PinHeader_1x05_P2.54mm_Vertical",
|
||||
60, 80, {'1': 'GPS_PPS_RAW', '2': 'GPIO5', '3': 'GPIO4', '4': '+3V3_AUX', '5': GND})
|
||||
place("Device:D_TVS", "TVS_PPS", "PESD3V3L1BA", "Diode_SMD:D_SOD-323", 85, 95, {'1': 'GPS_PPS_RAW', '2': GND})
|
||||
# 74LVC1G17: 1=NC 2=A 3=GND 4=Y 5=VCC (KiCad lib types: input/power_in/output/power_in)
|
||||
place("74xGxx:74LVC1G17", "U_PPS", "74LVC1G17", "Package_TO_SOT_SMD:SOT-23-5",
|
||||
110, 80, {'2': 'GPS_PPS_RAW', '4': 'PPS_CLEAN', '5': '+3V3_AUX', '3': GND, '1': NC})
|
||||
place("Device:C", "C_PPS", "100n", "Capacitor_SMD:C_0402_1005Metric", 100, 95, {'1': '+3V3_AUX', '2': GND})
|
||||
place("Device:C", "C_GPS", "10u", "Capacitor_SMD:C_0603_1608Metric", 70, 95, {'1': '+3V3_AUX', '2': GND})
|
||||
# series R guards the LVC1G17 output against a driver fight (CM5 pin 18 is bidi;
|
||||
# board policy configures it as input — spec Rs 33-100R, pick 33R)
|
||||
place("Device:R", "R_PPS", "33", "Resistor_SMD:R_0402_1005Metric", 140, 80, {'1': 'PPS_CLEAN', '2': 'SYNC_OUT'})
|
||||
place("Connector_Generic:Conn_01x02", "J_PPS1", "PPS_tap_UWB", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
170, 75, {'1': 'PPS_CLEAN', '2': GND})
|
||||
place("Connector_Generic:Conn_01x02", "J_PPS2", "PPS_tap_scope", "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical",
|
||||
170, 90, {'1': 'PPS_CLEAN', '2': GND})
|
||||
|
||||
# ---- file scaffold ----------------------------------------------------------------
|
||||
libsyms = "\n".join(blk for blk, _ in LIBS.values())
|
||||
sheet = ('(kicad_sch\n\t(version 20231120)\n\t(generator "eeschema")\n\t(generator_version "8.0")\n'
|
||||
f'\t(uuid "{SHEET_FILE_UUID}")\n\t(paper "A4")\n'
|
||||
'\t(title_block\n\t\t(title "CM5 Carrier - GPS + PPS distribution")\n\t\t(rev "A")\n'
|
||||
'\t\t(comment 1 "GPS conn (PPS+UART2+3V3_AUX) -> TVS -> LVC1G17 -> CM5 SYNC + 2 taps; PPS sink only")\n\t)\n'
|
||||
'\t(lib_symbols\n' + libsyms + '\n\t)\n' + "".join(body) +
|
||||
'\t(sheet_instances\n\t\t(path "/"\n\t\t\t(page "6")\n\t\t)\n\t)\n)\n')
|
||||
assert sheet.count('(') == sheet.count(')'), f"UNBALANCED {sheet.count('(')} vs {sheet.count(')')}"
|
||||
print("sheet-symbol uuid:", SHEET_SYMBOL_UUID)
|
||||
print("parens balanced:", sheet.count('('))
|
||||
out = os.path.join(CARRIER, "GPS_PPS.kicad_sch")
|
||||
if '--apply' in sys.argv: open(out, 'w').write(sheet); print("WROTE " + out)
|
||||
else: print("DRY RUN (--apply to write)")
|
||||
Reference in New Issue
Block a user