#!/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): tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation) body.append(f'\t(no_connect (at {x} {y}) (uuid "{U("nc-"+tag)}"))\n') def emit_gnd(x, y, tag): tag = tag if tag[-1].isdigit() else tag + "1" # refs must end in a digit (KiCad annotation) 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_GPS1", "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_PPS1", "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_PPS1", "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_PPS1", "100n", "Capacitor_SMD:C_0402_1005Metric", 100, 95, {'1': '+3V3_AUX', '2': GND}) place("Device:C", "C_GPS1", "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_PPS1", "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)")