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
"""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)))