molecule
AFS_Adom flight computer molecule
Public Unreviewedby ae007
Active fin stabilization flight computer for a sounding rocket, a clean-room AI redesign of the Rice Eclipse AFS board: RP2350B, ICM-45686, KX134-1211, MMC5983MA, MS5611, TMP117, INA219A, 8 servos, 2S LiPo, USB-C; 4-layer Adom molecule, designed end to end by Claude through KiCad and adom-aiflow
main
ae007
Project page: AFS_Adom rev A (Rice Eclipse AFS clean-room AI redesign): highlight reel and narrated process video with…
cbe460e
7d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
"""QFN-80 fan-out for U1 (RP2350B, 0.4 mm pitch), appended to the generated board as plain copper, plus the hand-routed
crystal and DVDD(32) traces.
Three-row via fan-out (standard for 0.4 mm QFN): every pin gets a 0.15 mm stub straight out of its pad; via pins take
0.46/0.2 vias in three rows (5.8 / 6.4 / 7.0 mm from the chip centre, pin k mod 3) so same-row vias sit 1.2 mm apart. A
stub that passes a neighbour's via row jogs 0.05 mm away from that via (0.46 via + 0.127 clearance + 0.075 half-track
needs 0.432 mm centre distance; the pitch gives 0.4). Each via then continues on B.Cu outward through the fence
(snaking between the outer rows' vias) to 7.6 mm (7.5 on the crystal side), so the router starts outside the fence.
Pins whose partner sits on F.Cu right outside the fan-out (crystal, VREG parts, DVDD/ADC/USB-OTP/QSPI-IOVDD caps,
USB pair) get the F.Cu stub only, ending at 7.3 mm, and the router continues on F.Cu. Plane pins (+3V3 / GND) end at
their via (In2 / In1). XIN, XOUT, XOUT_R and DVDD(32)->C26 are hand-routed here (RP2350 datasheet: crystal traces
short, on the top layer, no vias).
Rules: PCBWay 4-layer (track 0.1016 min, clearance 0.127, via 0.454/0.2 min, hole-to-hole 0.2794).
"""
import re, uuid
PCB = '/tmp/claude-1001/-home-adom-project/8819fb5f-c2c6-4524-9528-99428cd6b88e/scratchpad/AFS_Adom/AFS_Adom.kicad_pcb'
NS = uuid.UUID('6f1c2a3e-7b1d-4a4e-9c2f-aaf5d1e2c0de')
_n = [0]
def nuid():
_n[0] += 1; return str(uuid.uuid5(NS, 'AFS_Adom:fanout:%d' % _n[0]))
W = 0.15; VIA = 0.46; DRILL = 0.2
ROW_A, ROW_C = 5.8, 7.0; END_DIRECT = 7.3; END_DIRECT_TOP = 8.0; JOG = 0.05 # top side: room for a via at the stub end clear of the escape ends
# roles repeat D (F.Cu direct), A (via row 5.8), C (via row 7.0) along each side; the phase per side keeps the plane
# pins on via slots. A row-A via escapes on B.Cu through the row-C gap between pins k+1 and k-2 (one escape per gap),
# a row-C via straight out; a direct stub passes both rows on F.Cu with 0.05 mm jogs away from the adjacent vias.
PHASE = {1: 0, 21: 1, 41: 2, 61: 2}
DIRECT = {'30', '31', '61', '63', '65', '66', '67', '10', '32', '51', '59', '68', '69'}
PLANE = {'+3V3', 'GND'}
s = open(PCB).read()
nets = {name: int(n) for n, name in re.findall(r'^\t\(net (\d+) "([^"]*)"\)', s, re.M)}
i = s.find('(property "Reference" "U1"'); j = s.rfind('(footprint "', 0, i); k = s.find('(footprint "', i); seg = s[j:k]
cx, cy, rot = map(float, re.search(r'\(at ([\d.\-]+) ([\d.\-]+) ([\d.\-]+)\)', seg).groups())
assert rot == 180.0
pads = {}
for m in re.finditer(r'\(pad "(\d+)" smd (\w+)\s*\(at ([\d.\-]+) ([\d.\-]+)(?: [\d.\-]+)?\)\s*\(size ([\d.]+) ([\d.]+)\)([\s\S]*?)\n\t\t\)', seg):
num = m.group(1); px, py = float(m.group(3)), float(m.group(4))
nm = re.search(r'\(net \d+ "([^"]+)"\)', m.group(7))
pads[num] = (px, py, nm.group(1) if nm else None)
assert len(pads) == 81, len(pads)
sides = [(list(range(1, 21)), 'y', (-1, 0)), (list(range(21, 41)), 'x', (0, 1)), (list(range(41, 61)), 'y', (1, 0)), (list(range(61, 81)), 'x', (0, -1))]
def to_abs(x, y):
return round(cx - x, 4), round(cy - y, 4)
out = []
def seg_abs(net, a, b, layer='F.Cu', w=W):
(x1, y1), (x2, y2) = a, b
out.append(f'\t(segment\n\t\t(start {x1} {y1})\n\t\t(end {x2} {y2})\n\t\t(width {w})\n\t\t(layer "{layer}")\n\t\t(net {nets[net]})\n\t\t(uuid "{nuid()}")\n\t)')
def seg_out(net, a, b, layer='F.Cu', w=W):
seg_abs(net, to_abs(*a), to_abs(*b), layer, w)
def via_out(net, a):
x, y = to_abs(*a)
out.append(f'\t(via\n\t\t(at {x} {y})\n\t\t(size {VIA})\n\t\t(drill {DRILL})\n\t\t(layers "F.Cu" "B.Cu")\n\t\t(net {nets[net]})\n\t\t(uuid "{nuid()}")\n\t)')
nvia = ndirect = nesc = 0
roles_all = {}
for pins, axis, (ox, oy) in sides:
def place(a, d):
return (d * ox, a) if axis == 'y' else (a, d * oy)
ph = PHASE[pins[0]]
role = {}
for kk, p in enumerate(pins):
r = (kk - ph) % 3 # 0 D, 1 A, 2 C
if str(p) in DIRECT or pads[str(p)][2] is None:
r = 0
role[p] = r
roles_all.update(role)
along = lambda p: (pads[str(p)][1] if axis == 'y' else pads[str(p)][0])
rowd = {1: ROW_A, 2: ROW_C}
end_esc = 7.5 if pins[0] == 21 else 7.6
end_direct = END_DIRECT_TOP if pins[0] == 21 else END_DIRECT
for kk, p in enumerate(pins):
px, py, net = pads[str(p)]
if net is None:
continue
a = along(p); r = role[p]
pts = [place(a, 4.95)]
last_a = a
passes = [1, 2] if r == 0 else ([1] if r == 2 else [])
for q in passes:
jog = 0.0
for nb in (pins[kk - 1] if kk > 0 else None, pins[kk + 1] if kk < 19 else None):
if nb is None or role[nb] != q:
continue
jog = -JOG if along(nb) > a else JOG
if jog != last_a - a:
pts.append(place(a + jog, rowd[q] - 0.3))
pts.append(place(a + jog, rowd[q] + 0.3))
last_a = a + jog
if r == 0:
pts.append(place(a, end_direct)); ndirect += 1
else:
pts.append(place(a, rowd[r])); nvia += 1
for u, v in zip(pts, pts[1:]):
if u != v:
seg_out(net, u, v)
if r != 0:
via_out(net, pts[-1])
if net not in PLANE:
esc = [place(a, rowd[r])]
if r == 1:
offs = [along(pins[kk + dk]) - a for dk in (-2, -1, 1, 2) if 0 <= kk + dk < 20 and role[pins[kk + dk]] == 2]
if len(offs) == 2:
g = sum(offs) / 2.0
elif len(offs) == 1:
g = -0.2 if offs[0] > 0 else 0.2
else:
g = 0.0
esc.append(place(a + g, ROW_C))
esc.append(place(a, end_esc))
for u, v in zip(esc, esc[1:]):
if u != v:
seg_out(net, u, v, 'B.Cu', 0.2)
nesc += 1
# hand routes (board coordinates, F.Cu, 0.2 mm): crystal nets and DVDD(32) -> C26
H = [('XIN', [(147.2, 128.0), (147.2, 127.5), (148.75, 127.5), (149.05, 127.2), (149.05, 125.9), (151.2, 125.9)]),
('XOUT', [(146.8, 128.0), (146.8, 126.9), (144.1, 126.9), (143.775, 126.575), (143.775, 124.4)]),
('XOUT_R', [(145.425, 124.4), (146.1, 124.4), (146.8, 123.7), (147.35, 123.7)]),
('XOUT_R', [(145.425, 124.4), (145.425, 123.3), (143.525, 123.3), (143.225, 123.0), (143.225, 121.4)]),
('DVDD', [(146.4, 128.0), (146.4, 127.3), (141.2, 127.3)])]
for net, pts in H:
for u, v in zip(pts, pts[1:]):
seg_abs(net, u, v, 'F.Cu', 0.2)
# duplicate-numbered pads: KiCad needs copper between same-numbered pads (tactile switch legs) and every shield pad of
# the microSD socket needs its own GND via; the grid router treats each pad number as one cluster, so bridge them here.
def fp_info(ref):
i = s.find('(property "Reference" "%s"' % ref); j = s.rfind('(footprint "', 0, i); k = s.find('(footprint "', i)
seg = s[j:k]
cx_, cy_, r_ = re.search(r'\(at ([\d.\-]+) ([\d.\-]+)(?: ([\d.\-]+))?\)', seg).groups()
return float(cx_), float(cy_), float(r_ or 0)
import math
def pad_abs(ref, px, py):
cx_, cy_, r_ = fp_info(ref); a = math.radians(r_)
return round(cx_ + px * math.cos(a) + py * math.sin(a), 4), round(cy_ - px * math.sin(a) + py * math.cos(a), 4)
for sw, net1, net2 in (('SW1', 'BOOTSEL_BTN', 'GND'), ('SW2', 'RUN', 'GND')):
seg_abs(net1, pad_abs(sw, -3.98, -2.25), pad_abs(sw, 3.98, -2.25), 'F.Cu', 0.3)
seg_abs(net2, pad_abs(sw, -3.98, 2.25), pad_abs(sw, 3.98, 2.25), 'F.Cu', 0.3)
for px, py, dx, dy in ((-6.825, -3.425, 0, 1.6), (-6.825, 6.925, 1.4, 0), (4.325, -7.725, -1.4, 0), (6.675, 7.375, 1.5, 0)):
a = pad_abs('J11', px, py); b = (round(a[0] + dx, 4), round(a[1] + dy, 4))
seg_abs('GND', a, b, 'F.Cu', 0.3)
out.append(f'\t(via\n\t\t(at {b[0]} {b[1]})\n\t\t(size {VIA})\n\t\t(drill {DRILL})\n\t\t(layers "F.Cu" "B.Cu")\n\t\t(net {nets["GND"]})\n\t\t(uuid "{nuid()}")\n\t)')
body = '\n'.join(out)
s2 = s.rstrip()
assert s2.endswith(')')
s2 = s2[:-1].rstrip() + '\n' + body + '\n)\n'
open(PCB, 'w').write(s2)
print('fan-out: %d vias, %d B.Cu escapes, %d direct stubs, %d hand-routed nets, %d copper items' % (nvia, nesc, ndirect, len(H), len(out)))
print('roles:', ''.join('DAC'[roles_all[p]] for p in range(1, 81)))