631 lines
27 KiB
Python
631 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
EinarBoard 3.0 — Phase A generator: Block P (on-board STM32F411RET6) into hbox-watchdog-v5.kicad_sch
|
|
File-level edit per project standing rule (no KiCad MCP backend writes).
|
|
Surgical text splice: deletions by byte-span, insertions at anchors. Independent re-parse + netlist diff after.
|
|
Run: python3 gen_blockP.py <schematic path>
|
|
"""
|
|
import re, sys, math, uuid, collections, json
|
|
|
|
FN = sys.argv[1] if len(sys.argv) > 1 else 'hbox-watchdog-v5.kicad_sch'
|
|
src = open(FN, encoding='utf-8').read()
|
|
if 'STM32F411RETx' in src:
|
|
print('ABORT: U16/STM32F411RETx already present — idempotence guard'); sys.exit(1)
|
|
|
|
# ---------------- sexp parser with byte spans ----------------
|
|
def tokenize(s):
|
|
toks = []; i = 0; n = len(s)
|
|
while i < n:
|
|
c = s[i]
|
|
if c in ' \t\r\n': i += 1; continue
|
|
if c in '()': toks.append((c, i, i+1)); i += 1; continue
|
|
if c == '"':
|
|
j = i+1; buf = []
|
|
while j < n:
|
|
if s[j] == '\\' and j+1 < n: buf.append(s[j:j+2]); j += 2; continue
|
|
if s[j] == '"': break
|
|
buf.append(s[j]); j += 1
|
|
toks.append((('STR', ''.join(buf)), i, j+1)); i = j+1; continue
|
|
j = i
|
|
while j < n and s[j] not in ' \t\r\n()"': j += 1
|
|
toks.append((('ATOM', s[i:j]), i, j)); i = j
|
|
return toks
|
|
|
|
def parse(toks):
|
|
pos = [0]
|
|
def rd():
|
|
t, a, b = toks[pos[0]]
|
|
if t == '(':
|
|
pos[0] += 1; lst = []
|
|
while toks[pos[0]][0] != ')': lst.append(rd())
|
|
_, _, e = toks[pos[0]]; pos[0] += 1
|
|
return ('L', lst, a, e)
|
|
pos[0] += 1
|
|
return ('T', t, a, b)
|
|
out = []
|
|
while pos[0] < len(toks): out.append(rd())
|
|
return out
|
|
|
|
toks = tokenize(src)
|
|
doc = parse(toks)[0]
|
|
assert doc[0] == 'L'
|
|
def kids(e): return e[1] if e[0] == 'L' else []
|
|
def tag(e):
|
|
k = kids(e)
|
|
if k and k[0][0] == 'T': return k[0][1][1]
|
|
return None
|
|
def sval(e): return e[1][1] if e[0] == 'T' else None
|
|
def span(e): return (e[2], e[3])
|
|
def find_all(e, t): return [c for c in kids(e) if c[0] == 'L' and tag(c) == t]
|
|
def find1(e, t):
|
|
r = find_all(e, t); return r[0] if r else None
|
|
def fnum(e):
|
|
try: return float(sval(e))
|
|
except: return None
|
|
|
|
top = kids(doc)
|
|
root_uuid = sval(kids(find1(doc, 'uuid'))[1])
|
|
libsym_el = find1(doc, 'lib_symbols')
|
|
|
|
# ---------------- lib pin tables ----------------
|
|
def lib_pins(libname):
|
|
for s in find_all(libsym_el, 'symbol'):
|
|
if sval(kids(s)[1]) == libname:
|
|
pins = []
|
|
def walk(e):
|
|
for c in kids(e):
|
|
if c[0] == 'L':
|
|
if tag(c) == 'pin':
|
|
at = find1(c, 'at'); num = find1(c, 'number'); nm = find1(c, 'name')
|
|
pins.append(dict(x=fnum(kids(at)[1]), y=fnum(kids(at)[2]),
|
|
num=sval(kids(num)[1]), name=sval(kids(nm)[1]) if nm else ''))
|
|
else: walk(c)
|
|
walk(s)
|
|
return pins
|
|
raise KeyError(libname)
|
|
|
|
# ---------------- collect elements ----------------
|
|
symbols = find_all(doc, 'symbol')
|
|
wires = find_all(doc, 'wire')
|
|
labels = find_all(doc, 'label')
|
|
ncs = find_all(doc, 'no_connect')
|
|
juncs = find_all(doc, 'junction')
|
|
texts = find_all(doc, 'text')
|
|
|
|
def sym_info(s):
|
|
lid = sval(kids(find1(s, 'lib_id'))[1]); at = kids(find1(s, 'at'))
|
|
ref = value = fp = None
|
|
for p in find_all(s, 'property'):
|
|
pk = kids(p)
|
|
if sval(pk[1]) == 'Reference': ref = sval(pk[2])
|
|
elif sval(pk[1]) == 'Value': value = sval(pk[2])
|
|
elif sval(pk[1]) == 'Footprint': fp = sval(pk[2])
|
|
mir = find1(s, 'mirror')
|
|
return dict(lib_id=lid, ref=ref, value=value, fp=fp, x=fnum(at[1]), y=fnum(at[2]),
|
|
rot=(fnum(at[3]) if len(at) > 3 else 0) or 0,
|
|
mirror=sval(kids(mir)[1]) if mir else None, el=s)
|
|
SYMS = [sym_info(s) for s in symbols]
|
|
def by_ref(r):
|
|
m = [s for s in SYMS if s['ref'] == r]
|
|
assert len(m) == 1, f'ref {r}: {len(m)} matches'
|
|
return m[0]
|
|
|
|
def R2(v): return round(v, 2)
|
|
def pin_world(inst, pins=None):
|
|
pins = pins or lib_pins(inst['lib_id'])
|
|
out = {}
|
|
for p in pins:
|
|
x, y = p['x'], -p['y']
|
|
if inst['mirror'] == 'y': x = -x
|
|
if inst['mirror'] == 'x': y = -y
|
|
r = math.radians(inst['rot'])
|
|
out[p['num']] = (R2(inst['x'] + x*math.cos(r) + y*math.sin(r)),
|
|
R2(inst['y'] - x*math.sin(r) + y*math.cos(r)))
|
|
return out
|
|
|
|
adj = collections.defaultdict(set)
|
|
wire_pts = {}
|
|
for w in wires:
|
|
pts = [(R2(fnum(kids(xy)[1])), R2(fnum(kids(xy)[2]))) for xy in find_all(find1(w, 'pts'), 'xy')]
|
|
wire_pts[id(w)] = pts
|
|
for a, b in zip(pts, pts[1:]): adj[a].add(b); adj[b].add(a)
|
|
lab_at = collections.defaultdict(list) # pos -> [(name, element)]
|
|
for L in labels:
|
|
at = kids(find1(L, 'at'))
|
|
lab_at[(R2(fnum(at[1])), R2(fnum(at[2])))].append((sval(kids(L)[1]), L))
|
|
nc_at = {}
|
|
for n in ncs:
|
|
at = kids(find1(n, 'at')); nc_at[(R2(fnum(at[1])), R2(fnum(at[2])))] = n
|
|
junc_at = {}
|
|
for jn in juncs:
|
|
at = kids(find1(jn, 'at')); junc_at[(R2(fnum(at[1])), R2(fnum(at[2])))] = jn
|
|
|
|
# all pin positions of every symbol (for safety asserts + netlist)
|
|
ALLPINS = [] # (ref, pinnum, pos)
|
|
for s in SYMS:
|
|
try:
|
|
for num, p in pin_world(s).items(): ALLPINS.append((s['ref'], num, p))
|
|
except KeyError:
|
|
pass
|
|
pin_pos_map = collections.defaultdict(list)
|
|
for ref, num, p in ALLPINS: pin_pos_map[p].append((ref, num))
|
|
|
|
# ---------------- netlist (union-find over positions + label names) ----------------
|
|
def build_netlist(adjacency, labels_at, pinlist):
|
|
parent = {}
|
|
def find(a):
|
|
while parent.setdefault(a, a) != a:
|
|
parent[a] = parent[parent[a]]; a = parent[a]
|
|
return a
|
|
def union(a, b): parent[find(a)] = find(b)
|
|
for a, nbrs in adjacency.items():
|
|
for b in nbrs: union(('P', a), ('P', b))
|
|
for pos, lst in labels_at.items():
|
|
for nm, _ in lst: union(('P', pos), ('N', nm))
|
|
for ref, num, p in pinlist: union(('P', p), ('PIN', ref, num))
|
|
groups = collections.defaultdict(lambda: dict(names=set(), pins=set()))
|
|
for a in list(parent):
|
|
r = find(a); g = groups[r]
|
|
if a[0] == 'N': g['names'].add(a[1])
|
|
elif a[0] == 'PIN': g['pins'].add((a[1], a[2]))
|
|
nets = {}
|
|
for g in groups.values():
|
|
if not g['names'] and not g['pins']: continue
|
|
nm = '/'.join(sorted(g['names'])) if g['names'] else None
|
|
if nm: nets.setdefault(nm, set()).update(g['pins'])
|
|
return nets
|
|
NET_BEFORE = build_netlist(adj, lab_at, ALLPINS)
|
|
|
|
# ---------------- deletion set ----------------
|
|
J1 = by_ref('J1'); J2 = by_ref('J2'); P6 = by_ref('P6'); Rts1 = by_ref('Rts1'); Rtp1 = by_ref('Rtp1')
|
|
assert J1['value'] == 'Blackpill-L' and J2['value'] == 'Blackpill-R'
|
|
assert P6['lib_id'].endswith('Conn_01x04') and P6['value'] == 'FAN'
|
|
del_syms = [J1, J2, P6, Rts1, Rtp1]
|
|
del_sym_ids = {id(s['el']) for s in del_syms}
|
|
seed_pins = []
|
|
for s in del_syms: seed_pins += list(pin_world(s).values())
|
|
seed_set = set(seed_pins)
|
|
|
|
kept_pin_pos = {p for ref, num, p in ALLPINS if not any(by is s for s in [] )}
|
|
kept_pin_pos = set()
|
|
for s in SYMS:
|
|
if id(s['el']) in del_sym_ids: continue
|
|
try: kept_pin_pos.update(pin_world(s).values())
|
|
except KeyError: pass
|
|
|
|
# graph walk from seeds
|
|
gnodes = set()
|
|
stack = [p for p in seed_pins if p in adj]
|
|
while stack:
|
|
p = stack.pop()
|
|
if p in gnodes: continue
|
|
gnodes.add(p)
|
|
for q in adj[p]:
|
|
if q not in gnodes: stack.append(q)
|
|
# safety: deletion graph must not touch kept pins
|
|
touch = gnodes & kept_pin_pos
|
|
assert not touch, f'deletion graph touches kept pins at {sorted(touch)[:5]}'
|
|
|
|
del_spans = []
|
|
report = collections.defaultdict(list)
|
|
for s in del_syms:
|
|
del_spans.append(span(s['el'])); report['symbols_deleted'].append(f"{s['ref']} ({s['value']})")
|
|
for w in wires:
|
|
pts = wire_pts[id(w)]
|
|
if pts and all(p in gnodes for p in pts):
|
|
del_spans.append(span(w)); report['wires_deleted'].append(pts)
|
|
node_del = gnodes | seed_set
|
|
for pos, lst in lab_at.items():
|
|
if pos in node_del:
|
|
for nm, el in lst:
|
|
del_spans.append(span(el)); report['labels_deleted'].append((nm, pos))
|
|
# any FAN_TACH-family label anywhere (retired nets)
|
|
for pos, lst in lab_at.items():
|
|
if pos not in node_del:
|
|
for nm, el in lst:
|
|
if nm in ('FAN_TACH', 'FAN_TACH_RAW'):
|
|
del_spans.append(span(el)); report['labels_deleted'].append((nm, pos, 'tach-sweep'))
|
|
for pos, el in nc_at.items():
|
|
if pos in node_del:
|
|
del_spans.append(span(el)); report['nc_deleted'].append(pos)
|
|
for pos, el in junc_at.items():
|
|
if pos in node_del:
|
|
del_spans.append(span(el)); report['junctions_deleted'].append(pos)
|
|
|
|
# text patch: BLOCK N note (ETH_INT moved)
|
|
text_patches = []
|
|
for t in texts:
|
|
tv = sval(kids(t)[1])
|
|
if tv and 'INT PB2' in tv:
|
|
a, b = span(t)
|
|
text_patches.append((a, b, src[a:b].replace('INT PB2', 'INT PC0 (3.0: PB2 = BOOT1 strap — never repurpose)')))
|
|
report['texts_patched'].append('BLOCK N note: INT PB2 -> INT PC0')
|
|
|
|
# ---------------- templates from live file ----------------
|
|
def label_template(pos):
|
|
for nm, el in lab_at[pos]:
|
|
a, b = span(el); return src[a:b], nm
|
|
return None, None
|
|
# LEFT-side sample: label on J1 pin15 (NTC_TEMP); RIGHT-side: label on J2 pin1 (ETH_CS)
|
|
J1p = pin_world(J1); J2p = pin_world(J2)
|
|
tmplL, _ = label_template(J1p['15'])
|
|
tmplR, _ = label_template(J2p['1'])
|
|
assert tmplL and tmplR
|
|
|
|
def NU(): return str(uuid.uuid4())
|
|
def mk_label(name, x, y, side):
|
|
"""side: 'L' extends left, 'R' extends right, 'U' vertical up, 'D' vertical down"""
|
|
t = tmplL if side == 'L' else tmplR
|
|
# replace name, position, uuid in template
|
|
def rep(m):
|
|
return f'(at {x:g} {y:g} {ANG})'
|
|
ANG = {'L': 180, 'R': 0, 'U': 90, 'D': 270}[side]
|
|
out = re.sub(r'\(label "[^"]*"', f'(label "{name}"', t, count=1)
|
|
out = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} {ANG})', out, count=1)
|
|
out = re.sub(r'\(uuid "[^"]*"\)', f'(uuid "{NU()}")', out, count=1)
|
|
if side in ('R',):
|
|
out = re.sub(r'\(justify [^)]*\)', '(justify left bottom)', out, count=1)
|
|
elif side in ('L',):
|
|
out = re.sub(r'\(justify [^)]*\)', '(justify right bottom)', out, count=1)
|
|
else:
|
|
out = re.sub(r'\(justify [^)]*\)', '(justify left bottom)', out, count=1)
|
|
return out
|
|
def mk_nc(x, y):
|
|
return f'(no_connect\n\t\t(at {x:g} {y:g})\n\t\t(uuid "{NU()}")\n\t)'
|
|
def mk_text(txt, x, y):
|
|
e = texts[0]; a, b = span(e); t = src[a:b]
|
|
t = re.sub(r'^\(text "[^"]*(?:\\.[^"]*)*"', '(text ' + json.dumps(txt), t, count=1)
|
|
t = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} 0)', t, count=1)
|
|
t = re.sub(r'\(uuid "[^"]*"\)', f'(uuid "{NU()}")', t, count=1)
|
|
return t
|
|
|
|
# generic instance factory from template R20
|
|
R20 = by_ref('R20'); a, b = span(R20['el']); INST_TMPL = src[a:b]
|
|
def mk_instance(lib_id, ref, value, footprint, x, y, rot=0, pin_nums=None, extra_props=None):
|
|
t = INST_TMPL
|
|
t = re.sub(r'\(lib_id "[^"]*"\)', f'(lib_id "{lib_id}")', t, count=1)
|
|
t = re.sub(r'\(at [^)]*\)', f'(at {x:g} {y:g} {rot:g})', t, count=1) # first (at = instance
|
|
t = re.sub(r'\(property "Reference" "[^"]*"', f'(property "Reference" "{ref}"', t, count=1)
|
|
t = re.sub(r'\(property "Value" "[^"]*"', f'(property "Value" {json.dumps(value)}', t, count=1)
|
|
t = re.sub(r'\(property "Footprint" "[^"]*"', f'(property "Footprint" {json.dumps(footprint)}', t, count=1)
|
|
# property positions: drop to instance pos (KiCad re-autoplaces visually; positions cosmetic)
|
|
# fix property (at ...) occurrences AFTER the first: leave — cosmetic only.
|
|
# pins: template has pins "1","2" — rebuild pin uuid list for pin_nums
|
|
pin_nums = pin_nums or ['1', '2']
|
|
pin_block = '\n'.join(f'\t\t(pin "{p}"\n\t\t\t(uuid "{NU()}")\n\t\t)' for p in pin_nums)
|
|
t = re.sub(r'(\t\t\(pin "1".*?\)\n)+.*?\t\t\(pin "2"\n\t\t\t\(uuid "[^"]*"\)\n\t\t\)',
|
|
pin_block, t, count=1, flags=re.S)
|
|
t = re.sub(r'\(reference "[^"]*"\)', f'(reference "{ref}")', t, count=1)
|
|
t = re.sub(r'\(uuid "a2b3658d[^"]*"\)', f'(uuid "{NU()}")', t, count=1)
|
|
return t
|
|
|
|
# ---------------- MCU lib symbol (built from official pin map) ----------------
|
|
MCU_PINS = [
|
|
(1,'VBAT','power_in',-5.08,48.26,270),(2,'PC13','bidirectional',-17.78,-15.24,0),
|
|
(3,'PC14','bidirectional',-17.78,-17.78,0),(4,'PC15','bidirectional',-17.78,-20.32,0),
|
|
(5,'PH0','bidirectional',-17.78,30.48,0),(6,'PH1','bidirectional',-17.78,27.94,0),
|
|
(7,'NRST','input',-17.78,40.64,0),(8,'PC0','bidirectional',-17.78,17.78,0),
|
|
(9,'PC1','bidirectional',-17.78,15.24,0),(10,'PC2','bidirectional',-17.78,12.7,0),
|
|
(11,'PC3','bidirectional',-17.78,10.16,0),(12,'VSSA','power_in',2.54,-45.72,90),
|
|
(13,'VDDA','power_in',7.62,48.26,270),(14,'PA0','bidirectional',17.78,40.64,180),
|
|
(15,'PA1','bidirectional',17.78,38.1,180),(16,'PA2','bidirectional',17.78,35.56,180),
|
|
(17,'PA3','bidirectional',17.78,33.02,180),(18,'VSS','power_in',0,-45.72,90),
|
|
(19,'VDD','power_in',-2.54,48.26,270),(20,'PA4','bidirectional',17.78,30.48,180),
|
|
(21,'PA5','bidirectional',17.78,27.94,180),(22,'PA6','bidirectional',17.78,25.4,180),
|
|
(23,'PA7','bidirectional',17.78,22.86,180),(24,'PC4','bidirectional',-17.78,7.62,0),
|
|
(25,'PC5','bidirectional',-17.78,5.08,0),(26,'PB0','bidirectional',17.78,-2.54,180),
|
|
(27,'PB1','bidirectional',17.78,-5.08,180),(28,'PB2','bidirectional',17.78,-7.62,180),
|
|
(29,'PB10','bidirectional',17.78,-27.94,180),(30,'VCAP1','power_out',-17.78,-25.4,0),
|
|
(31,'VSS','passive',0,-45.72,90),(32,'VDD','power_in',0,48.26,270),
|
|
(33,'PB12','bidirectional',17.78,-30.48,180),(34,'PB13','bidirectional',17.78,-33.02,180),
|
|
(35,'PB14','bidirectional',17.78,-35.56,180),(36,'PB15','bidirectional',17.78,-38.1,180),
|
|
(37,'PC6','bidirectional',-17.78,2.54,0),(38,'PC7','bidirectional',-17.78,0,0),
|
|
(39,'PC8','bidirectional',-17.78,-2.54,0),(40,'PC9','bidirectional',-17.78,-5.08,0),
|
|
(41,'PA8','bidirectional',17.78,20.32,180),(42,'PA9','bidirectional',17.78,17.78,180),
|
|
(43,'PA10','bidirectional',17.78,15.24,180),(44,'PA11','bidirectional',17.78,12.7,180),
|
|
(45,'PA12','bidirectional',17.78,10.16,180),(46,'PA13','bidirectional',17.78,7.62,180),
|
|
(47,'VSS','passive',0,-45.72,90),(48,'VDD','power_in',2.54,48.26,270),
|
|
(49,'PA14','bidirectional',17.78,5.08,180),(50,'PA15','bidirectional',17.78,2.54,180),
|
|
(51,'PC10','bidirectional',-17.78,-7.62,0),(52,'PC11','bidirectional',-17.78,-10.16,0),
|
|
(53,'PC12','bidirectional',-17.78,-12.7,0),(54,'PD2','bidirectional',-17.78,22.86,0),
|
|
(55,'PB3','bidirectional',17.78,-10.16,180),(56,'PB4','bidirectional',17.78,-12.7,180),
|
|
(57,'PB5','bidirectional',17.78,-15.24,180),(58,'PB6','bidirectional',17.78,-17.78,180),
|
|
(59,'PB7','bidirectional',17.78,-20.32,180),(60,'BOOT0','input',-17.78,35.56,0),
|
|
(61,'PB8','bidirectional',17.78,-22.86,180),(62,'PB9','bidirectional',17.78,-25.4,180),
|
|
(63,'VSS','passive',0,-45.72,90),(64,'VDD','power_in',5.08,48.26,270),
|
|
]
|
|
def mcu_libsym():
|
|
pins_txt = []
|
|
for num, name, etype, x, y, ang in MCU_PINS:
|
|
hide = '\n\t\t\t\t\t(hide yes)' if etype == 'passive' and name == 'VSS' and num != 18 else ''
|
|
pins_txt.append(f'''\t\t\t\t(pin {etype} line
|
|
\t\t\t\t\t(at {x:g} {y:g} {ang}){hide}
|
|
\t\t\t\t\t(length 2.54)
|
|
\t\t\t\t\t(name "{name}"
|
|
\t\t\t\t\t\t(effects
|
|
\t\t\t\t\t\t\t(font
|
|
\t\t\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t\t\t)
|
|
\t\t\t\t\t\t)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(number "{num}"
|
|
\t\t\t\t\t\t(effects
|
|
\t\t\t\t\t\t\t(font
|
|
\t\t\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t\t\t)
|
|
\t\t\t\t\t\t)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t)''')
|
|
pins = '\n'.join(pins_txt)
|
|
return f'''\t\t(symbol "MCU_ST_STM32F4:STM32F411RETx"
|
|
\t\t\t(pin_names
|
|
\t\t\t\t(offset 1.016)
|
|
\t\t\t)
|
|
\t\t\t(exclude_from_sim no)
|
|
\t\t\t(in_bom yes)
|
|
\t\t\t(on_board yes)
|
|
\t\t\t(property "Reference" "U"
|
|
\t\t\t\t(at -15.24 49.53 0)
|
|
\t\t\t\t(effects
|
|
\t\t\t\t\t(font
|
|
\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(justify left)
|
|
\t\t\t\t)
|
|
\t\t\t)
|
|
\t\t\t(property "Value" "STM32F411RETx"
|
|
\t\t\t\t(at -15.24 -46.99 0)
|
|
\t\t\t\t(effects
|
|
\t\t\t\t\t(font
|
|
\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(justify left)
|
|
\t\t\t\t)
|
|
\t\t\t)
|
|
\t\t\t(property "Footprint" "Package_QFP:LQFP-64_10x10mm_P0.5mm"
|
|
\t\t\t\t(at 0 0 0)
|
|
\t\t\t\t(effects
|
|
\t\t\t\t\t(font
|
|
\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(hide yes)
|
|
\t\t\t\t)
|
|
\t\t\t)
|
|
\t\t\t(property "Datasheet" "https://www.st.com/resource/en/datasheet/stm32f411re.pdf"
|
|
\t\t\t\t(at 0 0 0)
|
|
\t\t\t\t(effects
|
|
\t\t\t\t\t(font
|
|
\t\t\t\t\t\t(size 1.27 1.27)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(hide yes)
|
|
\t\t\t\t)
|
|
\t\t\t)
|
|
\t\t\t(symbol "STM32F411RETx_0_1"
|
|
\t\t\t\t(rectangle
|
|
\t\t\t\t\t(start -15.24 45.72)
|
|
\t\t\t\t\t(end 15.24 -43.18)
|
|
\t\t\t\t\t(stroke
|
|
\t\t\t\t\t\t(width 0.254)
|
|
\t\t\t\t\t\t(type default)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t\t(fill
|
|
\t\t\t\t\t\t(type background)
|
|
\t\t\t\t\t)
|
|
\t\t\t\t)
|
|
\t\t\t)
|
|
\t\t\t(symbol "STM32F411RETx_1_1"
|
|
{pins}
|
|
\t\t\t)
|
|
\t\t)'''
|
|
|
|
# ---------------- Block P content ----------------
|
|
INS = [] # element text blocks to append at end of doc
|
|
def add(t): INS.append(t)
|
|
|
|
# U16 at grid
|
|
U16X, U16Y = 500.38, 434.34
|
|
mcu_world = {}
|
|
for num, name, etype, x, y, ang in MCU_PINS:
|
|
mcu_world[num] = (R2(U16X + x), R2(U16Y - y))
|
|
MCU_NETS = {1:'3V3',2:'LED_C13',3:'LSE_IN',4:'LSE_OUT',5:'HSE_IN',6:'HSE_OUT',7:'NRST',8:'ETH_INT',
|
|
12:'GND',13:'VDDA',14:'BAT_SENSE',15:'PWR_CTRL',16:'GNSS_TX',17:'GNSS_RX',18:'GND',19:'3V3',
|
|
20:'5V_SENSE',21:'IGN_SENSE',22:'FAN_PWM',26:'EXT_LED',27:'NTC_TEMP',28:'BOOT1_STRAP',30:'VCAP1',
|
|
32:'3V3',33:'ETH_CS',34:'ETH_SCK',35:'ETH_MISO',36:'ETH_MOSI',41:'CHRG_EN',42:'USART1_TX',
|
|
43:'USART1_RX',44:'DFU_DM',45:'DFU_DP',46:'SWDIO',48:'3V3',49:'SWCLK',50:'ETH_RST',
|
|
55:'CHRG_STAT',56:'CHRG_FAULT',57:'BYP_CTRL',58:'I2C_SCL',59:'I2C_SDA',60:'BOOT0',
|
|
61:'SER_IRQ',62:None,64:'3V3'}
|
|
MCU_NC = [9,10,11,23,24,25,29,37,38,39,40,51,52,53,54,62]
|
|
add(mk_instance('MCU_ST_STM32F4:STM32F411RETx', 'U16', 'STM32F411RET6',
|
|
'Package_QFP:LQFP-64_10x10mm_P0.5mm', U16X, U16Y, 0,
|
|
[str(n) for n, *_ in MCU_PINS]))
|
|
for num, name, etype, x, y, ang in MCU_PINS:
|
|
wx, wy = mcu_world[num]
|
|
net = MCU_NETS.get(num)
|
|
if num in MCU_NC:
|
|
add(mk_nc(wx, wy)); continue
|
|
if net is None: continue
|
|
side = {0: 'L', 180: 'R', 270: 'U', 90: 'D'}[ang]
|
|
if num in (31, 47, 63): continue # stacked VSS share pin18's point/label
|
|
add(mk_label(net, wx, wy, side))
|
|
|
|
# passives / support parts: label-on-pin style (v4 house pattern)
|
|
PART_LIBS = {}
|
|
def two_pin(lib_id, ref, value, fp, x, y, net1, net2, rot=0):
|
|
pins = lib_pins(lib_id)
|
|
inst = dict(lib_id=lib_id, x=x, y=y, rot=rot, mirror=None)
|
|
pw = pin_world(inst, pins)
|
|
nums = sorted(pw, key=lambda n: int(re.sub(r'\D', '', n) or 0))
|
|
add(mk_instance(lib_id, ref, value, fp, x, y, rot, nums))
|
|
for num, net in zip(nums, (net1, net2)):
|
|
px, py = pw[num]
|
|
side = 'R' if px > x else ('L' if px < x else ('U' if py < y else 'D'))
|
|
if net: add(mk_label(net, px, py, side))
|
|
|
|
FP_R = 'Resistor_SMD:R_0603_1608Metric'
|
|
FP_C = 'Capacitor_SMD:C_0603_1608Metric'
|
|
FP_C0805 = 'Capacitor_SMD:C_0805_2012Metric'
|
|
FP_LED = 'LED_SMD:LED_0603_1608Metric'
|
|
FP_TVS = 'Diode_SMD:D_SOD-323'
|
|
D2fp = by_ref('D2')['fp'] # SS34 footprint in use
|
|
Y1fp = by_ref('Y1')['fp'] # 25MHz crystal footprint in use
|
|
SW1fp = by_ref('SW1')['fp']
|
|
TP1fp = by_ref('TP1')['fp']
|
|
J9 = by_ref('J9')
|
|
C23fp = by_ref('C23')['fp']
|
|
P6fp_old = P6['fp'] or ''
|
|
P6fp_new = P6fp_old.replace('1x04', '1x02') if '1x04' in P6fp_old else P6fp_old
|
|
|
|
ROWA, ROWB = 502.92, 530.86
|
|
two_pin('Switch:SW_Push', 'SW2', 'BOOT0', SW1fp, 436.88, ROWA, '3V3', 'BOOT0')
|
|
two_pin('Device:R', 'R98', '10k', FP_R, 449.58, ROWA, 'BOOT0', 'GND')
|
|
two_pin('Device:R', 'R99', '10k', FP_R, 462.28, ROWA, 'BOOT1_STRAP', 'GND')
|
|
two_pin('Device:C', 'C104', '100nF', FP_C, 474.98, ROWA, 'NRST', 'GND')
|
|
for i, xx in enumerate([487.68, 500.38, 513.08, 525.78]):
|
|
two_pin('Device:C', f'C{105+i}', '100nF', FP_C, xx, ROWA, '3V3', 'GND')
|
|
two_pin('Device:C', 'C109', '100nF', FP_C, 538.48, ROWA, 'VDDA', 'GND')
|
|
two_pin('Device:C', 'C110', '4.7uF', FP_C, 551.18, ROWA, '3V3', 'GND')
|
|
two_pin('Device:C', 'C111', '4.7uF', FP_C, 563.88, ROWA, 'VCAP1', 'GND')
|
|
two_pin('Device:C', 'C112', '1uF', FP_C, 576.58, ROWA, 'VDDA', 'GND')
|
|
two_pin('Device:R', 'R104', '10R', FP_R, 589.28, ROWA, '3V3', 'VDDA')
|
|
|
|
two_pin('Device:Crystal', 'Y3', '25MHz', Y1fp, 436.88, ROWB, 'HSE_IN', 'HSE_OUT')
|
|
two_pin('Device:C', 'C113', '15pF', FP_C, 449.58, ROWB, 'HSE_IN', 'GND')
|
|
two_pin('Device:C', 'C114', '15pF', FP_C, 462.28, ROWB, 'HSE_OUT', 'GND')
|
|
two_pin('Device:Crystal', 'Y4', '32.768kHz', 'Crystal:Crystal_SMD_3215-2Pin_3.2x1.5mm',
|
|
474.98, ROWB, 'LSE_IN', 'LSE_OUT')
|
|
two_pin('Device:C', 'C115', '12pF', FP_C, 487.68, ROWB, 'LSE_IN', 'GND')
|
|
two_pin('Device:C', 'C116', '12pF', FP_C, 500.38, ROWB, 'LSE_OUT', 'GND')
|
|
# LEDs: Device:LED pin1=K pin2=A
|
|
two_pin('Device:R', 'R102', '1k', FP_R, 513.08, ROWB, '3V3', 'LED_C13_A')
|
|
two_pin('Device:LED', 'D17', 'LED C13 (blue)', FP_LED, 525.78, ROWB, 'LED_C13', 'LED_C13_A')
|
|
two_pin('Device:R', 'R103', '1k', FP_R, 538.48, ROWB, '3V3', 'LED_PWR_A')
|
|
two_pin('Device:LED', 'D18', 'LED PWR (green)', FP_LED, 551.18, ROWB, 'GND', 'LED_PWR_A')
|
|
for ref, net, xx in [('TP3', 'SWDIO', 563.88), ('TP4', 'SWCLK', 576.58), ('TP5', 'NRST', 589.28),
|
|
('TP6', 'GND', 601.98), ('TP7', '3V3', 614.68)]:
|
|
pins = lib_pins('Connector:TestPoint')
|
|
inst = dict(lib_id='Connector:TestPoint', x=xx, y=ROWB, rot=0, mirror=None)
|
|
pw = pin_world(inst, pins)
|
|
add(mk_instance('Connector:TestPoint', ref, net, TP1fp, xx, ROWB, 0, list(pw)))
|
|
(px, py) = list(pw.values())[0]
|
|
add(mk_label(net, px, py, 'D' if py > ROWB else 'U'))
|
|
|
|
# LDO block
|
|
two_pin('Device:D_Schottky', 'D12', 'SS34', D2fp, 627.38, ROWA, 'LDO_IN', 'SYS5V') # pin1=K pin2=A
|
|
two_pin('Device:D_Schottky', 'D13', 'SS34', D2fp, 640.08, ROWA, 'LDO_IN', 'DFU_VBUS')
|
|
u17pins = lib_pins('Regulator_Linear:AMS1117-3.3')
|
|
U17X, U17Y = 665.48, ROWA
|
|
inst = dict(lib_id='Regulator_Linear:AMS1117-3.3', x=U17X, y=U17Y, rot=0, mirror=None)
|
|
pw = pin_world(inst, u17pins)
|
|
add(mk_instance('Regulator_Linear:AMS1117-3.3', 'U17', 'AMS1117-3.3',
|
|
'Package_TO_SOT_SMD:SOT-223-3_TabPin2', U17X, U17Y, 0, [p['num'] for p in u17pins]))
|
|
name_by_num = {p['num']: p['name'].upper() for p in u17pins}
|
|
for num, (px, py) in pw.items():
|
|
nm = name_by_num[num]
|
|
net = 'LDO_IN' if nm in ('VI', 'VIN', 'IN') else ('3V3' if nm in ('VO', 'VOUT', 'OUT') else 'GND')
|
|
side = 'R' if px > U17X else ('L' if px < U17X else ('U' if py < U17Y else 'D'))
|
|
add(mk_label(net, px, py, side))
|
|
two_pin('Device:C', 'C117', '10uF', FP_C0805, 690.88, ROWA, 'LDO_IN', 'GND')
|
|
two_pin('Device:C', 'C118', '22uF/16V', C23fp or FP_C0805, 703.58, ROWA, '3V3', 'GND')
|
|
for ref, net, xx in [('PF12', 'LDO_IN', 716.28), ('PF13', 'DFU_VBUS', 728.98)]:
|
|
pins = lib_pins('power:PWR_FLAG')
|
|
inst = dict(lib_id='power:PWR_FLAG', x=xx, y=ROWA, rot=0, mirror=None)
|
|
pw = pin_world(inst, pins)
|
|
add(mk_instance('power:PWR_FLAG', ref, 'PWR_FLAG', '', xx, ROWA, 0, list(pw)))
|
|
(px, py) = list(pw.values())[0]
|
|
add(mk_label(net, px, py, 'D' if py > ROWA else 'U'))
|
|
|
|
# USB-C DFU port: clone J9 geometry+pattern with DFU_* nets
|
|
J9pins = pin_world(J9)
|
|
J11X, J11Y = 660.4, 430.53
|
|
NETMAP = {'USB_VBUS': 'DFU_VBUS', 'USB_DP': 'DFU_DP', 'USB_DM': 'DFU_DM',
|
|
'USB_CC1': 'DFU_CC1', 'USB_CC2': 'DFU_CC2'}
|
|
# what J9 has per pin: label at pin OR nc
|
|
a9, b9 = span(J9['el']); j9txt = src[a9:b9]
|
|
j9txt = re.sub(r'\(property "Reference" "J9"', '(property "Reference" "J11"', j9txt, count=1)
|
|
j9txt = re.sub(r'\(property "Value" "[^"]*"', '(property "Value" "USB-C DFU"', j9txt, count=1)
|
|
j9txt = re.sub(r'\(at ([\-0-9.]+) ([\-0-9.]+)( [\-0-9.]+)?\)',
|
|
lambda m, base=[0]: m.group(0), j9txt) # keep; we shift below numerically
|
|
# shift all coordinates by delta
|
|
DX, DY = R2(J11X - J9['x']), R2(J11Y - J9['y'])
|
|
def shift_coords(txt, dx, dy):
|
|
def r(m):
|
|
vals = m.group(1).split()
|
|
x = float(vals[0]) + dx; y = float(vals[1]) + dy
|
|
rest = (' ' + ' '.join(vals[2:])) if len(vals) > 2 else ''
|
|
return f'(at {x:g} {y:g}{rest})'
|
|
return re.sub(r'\(at ([^)]*)\)', r, txt)
|
|
j9txt = shift_coords(j9txt, DX, DY)
|
|
j9txt = re.sub(r'\(uuid "[^"]*"\)', lambda m: f'(uuid "{NU()}")', j9txt)
|
|
j9txt = re.sub(r'\(reference "J9"\)', '(reference "J11")', j9txt)
|
|
add(j9txt)
|
|
# labels/nc for J11 pins mirroring J9's connection style
|
|
for num, pos in J9pins.items():
|
|
npos = (R2(pos[0] + DX), R2(pos[1] + DY))
|
|
if pos in nc_at:
|
|
add(mk_nc(*npos)); continue
|
|
here = [nm for nm, _ in lab_at.get(pos, [])]
|
|
if not here and pos in adj:
|
|
# pin connected via a wire run in the original (e.g. SH -> wire -> GND label):
|
|
# resolve the net name by graph walk and label the cloned pin directly.
|
|
seen = {pos}; st = [pos]; found = None
|
|
while st and not found:
|
|
q = st.pop()
|
|
lbls = [nm for nm, _ in lab_at.get(q, [])]
|
|
if lbls: found = lbls[0]; break
|
|
for r_ in adj[q]:
|
|
if r_ not in seen: seen.add(r_); st.append(r_)
|
|
if found: here = [found]
|
|
if here:
|
|
nm = here[0]
|
|
newnm = NETMAP.get(nm, nm)
|
|
side = 'R' if pos[0] > J9['x'] else 'L'
|
|
add(mk_label(newnm, npos[0], npos[1], side))
|
|
# CC pulldowns + TVS
|
|
two_pin('Device:R', 'R100', '5.1k', FP_R, 704.85, 430.53, 'DFU_CC1', 'GND')
|
|
two_pin('Device:R', 'R101', '5.1k', FP_R, 717.55, 430.53, 'DFU_CC2', 'GND')
|
|
two_pin('Device:D_TVS', 'D14', 'PESD5V0S1BA', FP_TVS, 704.85, 458.47, 'DFU_DP', 'GND')
|
|
two_pin('Device:D_TVS', 'D15', 'PESD5V0S1BA', FP_TVS, 717.55, 458.47, 'DFU_DM', 'GND')
|
|
two_pin('Device:D_TVS', 'D16', 'PESD5V0S1BA', FP_TVS, 730.25, 458.47, 'DFU_VBUS', 'GND')
|
|
|
|
# 2-pin fan connector at old P6 site (reuse ref P6)
|
|
oldp = pin_world(P6)
|
|
np6x, np6y = P6['x'], P6['y']
|
|
two_pin('Connector_Generic:Conn_01x02', 'P6', 'FAN 2-pin', P6fp_new, np6x, np6y,
|
|
'5V_FAN', 'FAN_RET', rot=P6['rot'])
|
|
|
|
# notes
|
|
add(mk_text('BLOCK P - ON-BOARD MCU (EinarBoard 3.0). STM32F411RET6 LQFP64 replaces Blackpill+J1/J2. DFU: hold BOOT0, tap NRST -> USB. HSE 25MHz REQUIRED for ROM-bootloader USB clock (AN2606). PB2=BOOT1 strapped LOW via R99 - NEVER repurpose PB2. Bench flash: USB powers 3V3 domain only via D13 (SYS5V dark, K1 stays NC).', 420, 372))
|
|
add(mk_text('PC6/PC7 reserved: USART6 (3rd UART, future). Free: PA7, PB9, PB10, PC1-PC5, PC8-PC12, PD2. LED_C13 active-LOW (Blackpill parity). SWD: TP3-TP7 (SWDIO/SWCLK/NRST/GND/3V3).', 420, 377))
|
|
|
|
# ---------------- splice ----------------
|
|
new_src = src
|
|
# apply text patches + deletions together, sorted desc by start
|
|
ops = [(a, b, '') for (a, b) in del_spans] + [(a, b, t) for (a, b, t) in text_patches]
|
|
ops.sort(key=lambda o: -o[0])
|
|
spans_sorted = sorted([(a, b) for a, b, _ in ops])
|
|
for (a1, b1), (a2, b2) in zip(spans_sorted, spans_sorted[1:]):
|
|
assert b1 <= a2, f'overlapping spans {a1,b1} {a2,b2}'
|
|
for a, b, t in ops:
|
|
# widen deletion to swallow leading whitespace
|
|
a0 = a
|
|
while a0 > 0 and new_src[a0-1] in ' \t\n': a0 -= 1
|
|
if t == '':
|
|
new_src = new_src[:a0] + '\n\t' + new_src[b:]
|
|
else:
|
|
new_src = new_src[:a] + t + new_src[b:]
|
|
# insert lib symbol before lib_symbols closing paren
|
|
ls_a, ls_b = span(libsym_el)
|
|
# recompute shift: all deletions were AFTER lib_symbols? assert to be safe
|
|
assert all(a > ls_b for a, b, _ in ops), 'a deletion overlaps lib_symbols - unhandled'
|
|
new_src = new_src[:ls_b-1] + '\n' + mcu_libsym() + '\n\t' + new_src[ls_b-1:]
|
|
# append new elements before final closing paren
|
|
end = new_src.rfind(')')
|
|
body = '\n\t' + '\n\t'.join(INS) + '\n'
|
|
new_src = new_src[:end] + body + new_src[end:]
|
|
|
|
open(FN, 'w', encoding='utf-8').write(new_src)
|
|
print('WROTE', FN, len(new_src), 'bytes (was', len(src), ')')
|
|
for k, v in report.items():
|
|
print(f'-- {k}: {len(v)}')
|
|
for item in v[:40]: print(' ', item)
|