Files
rpicarrierboard/tools/build_power.py
noise c469238dd5 Power sheet stage 1: 15V input protection (ideal-diode + TVS)
First stage of the power tree. New carrier/Power.kicad_sch (generated by
tools/build_power.py): J_PWR 15V inlet -> D_REV LM74700-Q1 ideal-diode
controller + Q_REV N-FET (reverse-polarity / reverse-current) -> VIN_PROT,
with SMBJ18A input TVS and 100uF/25V || 10uF/50V bulk + VCAP cap.

Part-selection note: the spec's buck candidates miss its own >=20V Vin rule
(TPS54424=17V, TPS54560=async); the 3 sync bucks (stages 2-4) will use
>=30V synchronous parts, datasheet-verified, feeding +5V/+3V3_RF/+3V3_AUX.

tools/build_power.py generalizes the build_ekey approach: generic place()
attaches a net label / GND symbol per pin (by-name connectivity, grid-snapped);
PWR_FLAGs co-located on real pin coords.

Verify: ERC 133->133 (zero new); analyzer +16 added, 0 regressions; netlist
confirms the ideal-diode topology (+15V_IN/VIN_PROT/GATE_REV/VCAP_REV/GND).

Confirm vs LM74700-Q1 datasheet: C_VCAP value, EN tie, FET Vds/SOA, gate R.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 16:41:22 -04:00

161 lines
9.8 KiB
Python

#!/usr/bin/env python3
"""build_power.py — generate the CM5-carrier Power sheet, stage by stage.
Same connectivity model as build_ekey.py: every component pin gets a named label
(or a power:GND symbol) coincident with its connection point, so nets form by name
with no wire routing. Coords are grid-snapped to 1.27mm so labels bind to pins.
STAGE 1 (this file): input protection —
J_PWR(15V) -> D_REV(LM74700 ideal-diode + Q_REV N-FET) -> VIN_PROT, with input TVS
+ bulk. Reverse-polarity + reverse-current via the ideal-diode controller.
Later stages (bucks) append to PLACEMENTS and re-run.
Datasheet items to CONFIRM against LM74700-Q1 (values, not topology):
- C_VCAP value/reference (charge-pump reservoir; ~1uF typ)
- EN handling (tied to +15V_IN for always-on; EN abs-max covers 15V)
- Q_REV FET pick (Vds>=30V, low Rds(on), SOA for inrush) + optional gate resistor
- TVS standoff (SMBJ18A: 18V standoff / ~29V clamp < buck Vin rating)
"""
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-power-" + 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
# ---- lib_symbol sources -----------------------------------------------------------
carrier_hs = open(os.path.join(CARRIER, "CM5_HighSpeed.kicad_sch")).read()
LIBS = {} # lib_id -> (embed_block, {pin:(lx,ly,ang)})
def add_from(lib_id, srctext, srcname=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)
LIBS[lib_id] = (blk, lib_pins(blk))
# reuse R/C/C_Polarized/GND verbatim from the carrier (guaranteed compatible)
for lid in ["Device:R", "Device:C", "Device:C_Polarized", "power:GND"]:
add_from(lid, carrier_hs)
# from KiCad libs (renamed to full lib_id)
add_from("power:PWR_FLAG", open(f"{KSS}/power.kicad_sym").read(), "PWR_FLAG")
add_from("Power_Management:LM74700", open(f"{KSS}/Power_Management.kicad_sym").read(), "LM74700")
add_from("Transistor_FET:Q_NMOS_GDS", open(f"{KSS}/Transistor_FET.kicad_sym").read(), "Q_NMOS_GDS")
add_from("Device:D_TVS", open(f"{KSS}/Device.kicad_sym").read(), "D_TVS")
add_from("Connector:Screw_Terminal_01x02", open(f"{KSS}/Connector.kicad_sym").read(), "Screw_Terminal_01x02")
# ---- placement engine -------------------------------------------------------------
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_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 emit_flag(x, y, tag):
body.append(f'\t(symbol\n\t\t(lib_id "power:PWR_FLAG")\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("flg-"+tag)}")\n'
f'\t\t(property "Reference" "#FLG{tag}" (at {x} {y-3} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
f'\t\t(property "Value" "PWR_FLAG" (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("fp-"+tag)}"))\n'
f'\t\t(instances (project "{PROJECT}" (path "/{ROOT_UUID}/{SHEET_SYMBOL_UUID}" (reference "#FLG{tag}") (unit 1))))\n\t)\n')
def place(lib_id, ref, value, footprint, ox, oy, pin_nets):
"""Place a symbol at grid-snapped (ox,oy); attach each pin's net by name.
pin_nets: {pinnum: 'NET' | ('gnd',) }. GND pins use a power:GND symbol."""
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 isinstance(net, tuple) and net[0] == 'global': emit_glabel(net[1], px, py)
else: emit_label(net, px, py)
return ox, oy
# ---- STAGE 1: input protection ----------------------------------------------------
GND = ('gnd',)
place("Connector:Screw_Terminal_01x02", "J_PWR", "15V_DC_IN", "TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-2_1x02_P5.00mm_Horizontal",
80, 80, {'1': '+15V_IN', '2': GND})
drev = place("Power_Management:LM74700", "D_REV", "LM74700-Q1", "Package_TO_SOT_SMD:SOT-23-6",
120, 80, {'6': '+15V_IN', '3': '+15V_IN', '4': 'VIN_PROT', '5': 'GATE_REV', '1': 'VCAP_REV', '2': GND})
place("Transistor_FET:Q_NMOS_GDS", "Q_REV", "CSD18540 (Vds>=30V)", "Package_TO_SOT_SMD:TO-263-2",
140, 90, {'1': 'GATE_REV', '2': 'VIN_PROT', '3': '+15V_IN'})
place("Device:C", "C_VCAP", "1u", "Capacitor_SMD:C_0402_1005Metric", 120, 100, {'1': 'VCAP_REV', '2': GND})
tvs = place("Device:D_TVS", "TVS_IN", "SMBJ18A", "Diode_SMD:D_SMB", 160, 80, {'1': 'VIN_PROT', '2': GND})
place("Device:C_Polarized", "C_BULK1", "100u/25V", "Capacitor_SMD:CP_Elec_8x10", 170, 80, {'1': 'VIN_PROT', '2': GND})
place("Device:C", "C_BULK2", "10u/50V", "Capacitor_SMD:C_1210_3225Metric", 180, 80, {'1': 'VIN_PROT', '2': GND})
# PWR_FLAGs co-located exactly on real pins so they bind: +15V_IN @ D_REV.6 (ANODE),
# VIN_PROT @ TVS.1 (a power_in driver for VIN_PROT, needed once bucks consume it).
def pin_abs(origin, lib_id, num):
lx, ly, _ = LIBS[lib_id][1][num]
return round(origin[0] + lx, 2), round(origin[1] - ly, 2)
fx, fy = pin_abs(drev, "Power_Management:LM74700", "6"); emit_flag(fx, fy, "15IN")
vx, vy = pin_abs(tvs, "Device:D_TVS", "1"); emit_flag(vx, vy, "VINP")
# ---- 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 - Power")\n\t\t(rev "A")\n'
'\t\t(comment 1 "15V inlet -> ideal-diode -> VIN_PROT -> 3 bucks (+5V, +3V3_RF, +3V3_AUX)")\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 "5")\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, "Power.kicad_sch")
if '--apply' in sys.argv: open(out, 'w').write(sheet); print("WROTE " + out)
else: print("DRY RUN (--apply to write)")