seveibar/am3352-dev-board
A 324-ball TI AM3352 ARM processor with DDR3, LCD, USB, Ethernet, storage, serial, analog, clock, reset, power, and JTAG interfaces, paired with configurable JST-GH board connectors and constrained PCB routing.
- Version
- 1.0.2
- License
- unset
- Stars
- 0
scripts/finish-power-fanout.py
"""Connect isolated supply groups to their existing supply copper fills with local fanout.
Uses the physical geometry audit; every added path and via is clearance checked.
"""
import runpy,json,math
from pathlib import Path
from shapely.geometry import Point,LineString
from shapely.ops import unary_union
from shapely.prepared import prep
ctx=runpy.run_path('scripts/audit-copper.py');c=ctx['c'];data=json.loads(Path('design/routed-copper.json').read_text())
meta=ctx['meta'];shapes=ctx['shapes'];component=ctx['component'];ports=ctx['ports'];padnodes=ctx['padnodes'];layers=ctx['layers'];extras=[];missed=[];added=0
for h in c:
if h['type']=='pcb_hole':extras.extend((Point(h['x'],h['y']).buffer(h['hole_diameter']/2), '__mechanical_hole', l, True) for l in layers)
assignments={'VDD_MPU':'inner4','VDD_CORE':'inner3','DDR_1V5':'inner2','V1V8':'inner5','V3V3':'bottom','A3V3':'inner2','GND':'inner1'}
offsets=sorted([(x,y) for x in [-4,-3.2,-2.4,-2,-1.6,-1.2,-.8,-.6,-.4,0,.4,.6,.8,1.2,1.6,2,2.4,3.2,4] for y in [-4,-3.2,-2.4,-2,-1.6,-1.2,-.8,-.6,-.4,0,.4,.6,.8,1.2,1.6,2,2.4,3.2,4] if x or y],key=lambda p:p[0]**2+p[1]**2)
for name,targetLayer in assignments.items():
sid=next(x['source_net_id'] for x in c if x['type']=='source_net' and x['name']==name);net=ctx['find'](sid);groups={}
for port in set(ctx['netports'][net]):groups.setdefault(component(padnodes[port][0]),[]).append(port)
if len(groups)<2:continue
planeIndices=[i for i,m in enumerate(meta) if m[0]==targetLayer and m[1]==net and m[2].startswith('pcb_copper_pour')]
if not planeIndices:continue
main=component(max(planeIndices,key=lambda i:shapes[i].area))
plane=[shapes[i] for i,m in enumerate(meta) if m[0]==targetLayer and m[1]==net and m[2].startswith('pcb_copper_pour') and component(i)==main]
if not plane:print('No usable supply plane for',name);continue
area=prep(unary_union(plane));existing=[]
for i,m in enumerate(meta):
if m[1]==net and m[2].endswith('_via') and component(i)==main:
point=tuple(shapes[i].centroid.coords[0])
if not any(math.dist(point,v)<.001 for v in existing):existing.append(point)
routeList=data['nets'].setdefault(sid,[])
data['sourceTraceIds'][sid]=next(t['source_trace_id'] for t in c if t['type']=='source_trace' and sid in t.get('connected_source_net_ids',[]))
vb=prep(unary_union([g for g,m in zip(shapes,meta) if not m[2].startswith('pcb_copper_pour') and (m[1]!=net or m[2].startswith(('pcb_smtpad','pcb_plated_hole')) or m[2].endswith('_via'))]+[g for g,n,l,via in extras if n!=net or via]).buffer(.27))
wb={l:prep(unary_union([g for g,m in zip(shapes,meta) if m[0]==l and m[1]!=net and not m[2].startswith('pcb_copper_pour')]+[g for g,n,el,via in extras if n!=net and el==l]).buffer(.155)) for l in ['top','bottom']}
for group,ps in groups.items():
if group==main:continue
found=False
for pid in ps:
p=ports[pid];x,y=p['x'],p['y'];layer=next((l for l in p['layers'] if l in ['top','bottom']),'top')
candidates=[(vx,vy,False) for vx,vy in sorted(existing,key=lambda v:math.dist((x,y),v)) if math.dist((x,y),(vx,vy))<=5]+[(round(x+dx,3),round(y+dy,3),True) for dx,dy in offsets]
for vx,vy,newVia in candidates:
if abs(vx)>34.5 or abs(vy)>29.5 or not area.covers(Point(vx,vy)):continue
if newVia and (vb.intersects(Point(vx,vy)) or any(math.dist((vx,vy),v)<.405 for v in existing)):continue
dx,dy=vx-x,vy-y;t=min(abs(dx),abs(dy));sx=1 if dx>=0 else -1;sy=1 if dy>=0 else -1
choices=[[(x,y),(x+sx*t,y+sy*t),(vx,vy)],[(x,y),(vx-sx*t,vy-sy*t),(vx,vy)],[(x,y),(vx,y),(vx,vy)],[(x,y),(x,vy),(vx,vy)]]
points=next((q for q in choices if not wb[layer].intersects(LineString(q))),None)
if points is None:continue
clean=[points[0]]
for q in points[1:]:
if math.dist(q,clean[-1])>1e-7:clean.append(q)
if len(clean)<2:continue
routeList.append([{'route_type':'wire','x':px,'y':py,'width':.1,'layer':layer} for px,py in clean]);extras.append((LineString(clean).buffer(.05),net,layer,False))
if newVia:
routeList.append([{'route_type':'wire','x':vx,'y':vy,'width':.1,'layer':'top'},{'route_type':'via','x':vx,'y':vy,'from_layer':'top','to_layer':'bottom','via_diameter':.3,'via_hole_diameter':.15},{'route_type':'wire','x':vx,'y':vy,'width':.1,'layer':'bottom'}]);existing.append((vx,vy))
extras.extend((Point(vx,vy).buffer(.15),net,l,True) for l in layers)
found=True;added+=1;break
if found:break
if not found:missed.append({'net':name,'ports':ps})
Path('design/routed-copper.json').write_text(json.dumps(data,separators=(',',':'))+'\n');Path('design/power-fanout-report.json').write_text(json.dumps({'connectedGroups':added,'remainingGroups':missed},indent=2)+'\n')
print('Power fanout:',added,'groups connected;',len(missed),'groups remain')