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-maze.py
"""Conservative grid maze router for remaining physical copper islands.
All new vias are checked against copper on every layer; planes are regenerated afterward.
"""
import json,runpy,math,subprocess,struct,sys,time,os
from pathlib import Path
import numpy as np,shapely
from shapely.geometry import Point,LineString,box
from shapely.ops import unary_union
ctx=runpy.run_path('scripts/audit-copper.py');c=ctx['c'];meta=ctx['meta'];shapes=ctx['shapes'];component=ctx['component'];find=ctx['find'];ports=ctx['ports'];padnodes=ctx['padnodes']
data=json.loads(Path('design/routed-copper.json').read_text());step=float(os.environ.get('MAZE_GRID','.05'));ox,oy,xmax,ymax=map(float,os.environ.get('MAZE_BOUNDS','-34.5,-29.5,34.5,29.5').split(','));W=round((xmax-ox)/step)+1;H=round((ymax-oy)/step)+1;N=W*H
xs=ox+np.arange(W)*step;ys=oy+np.arange(H)*step;xx,yy=np.meshgrid(xs,ys);extras=[];report=[]
def raster(g):
out=np.zeros((H,W),dtype=np.uint8)
if g.is_empty:return out
x0,y0,x1,y1=g.bounds;a=max(0,int((x0-ox)/step)-1);b=min(W,int((x1-ox)/step)+2);d=max(0,int((y0-oy)/step)-1);e=min(H,int((y1-oy)/step)+2)
if a<b and d<e:out[d:e,a:b]=shapely.intersects_xy(g,xx[d:e,a:b],yy[d:e,a:b])
return out
for h in c:
if h['type']=='pcb_hole':extras.extend((Point(h['x'],h['y']).buffer(h['hole_diameter']/2),'NC',l,True) for l in ctx['layers'])
nets=list(ctx['netports']);requested=sys.argv[1:]
# Smaller signal nets first; power and ground fanout last.
nets.sort(key=lambda n:(ctx['netnames'].get(n,n) in ['GND','DDR_1V5','VDD_CORE','VDD_MPU','V1V8','V3V3'],len(ctx['netports'][n])))
for net in nets:
name=ctx['netnames'].get(net,net)
if requested and name not in requested:continue
groups={}
for p in set(ctx['netports'][net]):groups.setdefault(component(padnodes[p][0]),[]).append(p)
if len(groups)<2:continue
sid=next((p['source_net_id'] for p in c if p['type']=='source_net' and find(p['source_net_id'])==net),None)
if sid is None:sid=next(t['source_trace_id'] for t in c if t['type']=='source_trace' and find(t['source_trace_id'])==net)
own=[i for i,m in enumerate(meta) if m[1]==net];bycomp={}
for i in own:bycomp.setdefault(component(i),[]).append(i)
main=max(groups,key=lambda k:len(groups[k]));mainShapes=[(shapes[i],meta[i][0]) for i in bycomp[main]]
ls=['top','inner2','inner3','inner4','inner5','bottom'] if name!='GND' else ctx['layers']
if os.environ.get('MAZE_TOP_ONLY')=='1':ls=['top']
planeIds=[i for i in own if meta[i][2].startswith('pcb_copper_pour')]
if planeIds:
main=component(max(planeIds,key=lambda i:shapes[i].area));mainShapes=[(shapes[i],meta[i][0]) for i in bycomp[main]]
if os.environ.get('MAZE_EXPLICIT_CONTACTS')=='1':
mainShapes=[(shapes[i],meta[i][0]) for i in bycomp[main] if not meta[i][2].startswith('pcb_copper_pour')]
print('Routing',name,len(groups)-(1 if main in groups else 0),'islands',flush=True)
blocked=[]
for l in ls:
geoms=[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,v in extras if n!=net and el==l]
blocked.append(raster(unary_union(geoms).buffer(.15+step*.7072)))
blocked=np.asarray(blocked,dtype=np.uint8)
viaObstacles=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')) and os.environ.get('ALLOW_PAD_VIAS')!='1') or m[2].endswith('_via'))]+[g for g,n,l,v in extras if n!=net or v]).buffer(.25+step*.7072)
via=(1-raster(viaObstacles)).astype(np.uint8)
existing=unary_union([shapes[i] for i in own if meta[i][2].endswith('_via') or meta[i][2].startswith('pcb_plated_hole')]).buffer(-.06)
via[raster(existing)>0]=2
for group,ps in sorted(groups.items(),key=lambda p:len(p[1])):
if group==main:continue
target=np.asarray([raster(unary_union([g for g,l in mainShapes if l==layer]).buffer(-.015)) for layer in ls],dtype=np.uint8)
source=np.asarray([raster(unary_union([shapes[i] for i in bycomp[group] if meta[i][0]==layer and (os.environ.get('MAZE_EXPLICIT_CONTACTS')!='1' or not meta[i][2].startswith('pcb_copper_pour'))]).buffer(-.015)) for layer in ls],dtype=np.uint8)
starts=np.flatnonzero((source*(1-blocked)).ravel()).astype(np.int32)
if not len(starts) or not np.any(target*(1-blocked)):
report.append({'net':name,'ports':ps,'status':'blocked endpoints'});print(' blocked endpoints',ps,flush=True);continue
with open('../../work/maze-input.bin','wb') as f:f.write(struct.pack('iiii',W,H,len(ls),len(starts)));f.write(blocked.tobytes());f.write(target.tobytes());f.write(via.tobytes());f.write(starts.tobytes())
result=subprocess.run(['../../work/maze','../../work/maze-input.bin','../../work/maze-path.bin'],capture_output=True,text=True);print(result.stdout.strip(),flush=True)
raw=Path('../../work/maze-path.bin').read_bytes();count=struct.unpack('i',raw[:4])[0]
if not count:report.append({'net':name,'ports':ps,'status':'no path'});continue
ids=np.frombuffer(raw[4:],dtype=np.int32);points=[(round(ox+(int(i)%N%W)*step,5),round(oy+(int(i)%N//W)*step,5),int(i)//N) for i in ids]
routes=[];chunk=[points[0]]
def savechunk():
if len(chunk)<2:return
simple=[chunk[0]]
for i in range(1,len(chunk)-1):
a,b,d=chunk[i-1],chunk[i],chunk[i+1]
if abs((b[0]-a[0])*(d[1]-b[1])-(b[1]-a[1])*(d[0]-b[0]))>1e-7:simple.append(b)
simple.append(chunk[-1]);layer=ls[chunk[0][2]];routes.append([{'route_type':'wire','x':x,'y':y,'width':.1,'layer':layer} for x,y,z in simple]);g=LineString([(x,y) for x,y,z in simple]).buffer(.05);extras.append((g,net,layer,False));mainShapes.append((g,layer))
for a,b in zip(points,points[1:]):
if a[2]==b[2]:chunk.append(b);continue
savechunk();x,y,z=b;idx=round((y-oy)/step)*W+round((x-ox)/step)
if via.ravel()[idx]!=2:
routes.append([{'route_type':'wire','x':x,'y':y,'width':.1,'layer':'top'},{'route_type':'via','x':x,'y':y,'from_layer':'top','to_layer':'bottom','via_diameter':.3,'via_hole_diameter':.15},{'route_type':'wire','x':x,'y':y,'width':.1,'layer':'bottom'}]);g=Point(x,y).buffer(.15)
for layer in ctx['layers']:extras.append((g,net,layer,True));mainShapes.append((g,layer))
via[raster(g.buffer(.285))>0]=0;via[raster(g.buffer(-.06))>0]=2
chunk=[b]
savechunk();mainShapes.extend((shapes[i],meta[i][0]) for i in bycomp[group]);data['nets'].setdefault(sid,[]).extend(routes);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',[])),sid)
report.append({'net':name,'ports':ps,'status':'connected','routeRecords':len(routes)})
Path('design/routed-copper.json').write_text(json.dumps(data,separators=(',',':'))+'\n')
Path('design/maze-report.json').write_text(json.dumps(report,indent=2)+'\n')
print('Maze results:',sum(r['status']=='connected' for r in report),'connected;',sum(r['status']!='connected' for r in report),'remaining',flush=True)