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)