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/ground-escape.py

from pathlib import Path
import json,math,sys
from shapely.geometry import Point, Polygon, LineString
from shapely.ops import unary_union
from shapely.prepared import prep
c=json.loads(Path('dist/index/circuit.json').read_text());padmap=json.loads(Path('../../work/router-pad-map.json').read_text())
netByPort={v['pcb_port_id']:v['net'] for v in padmap.values()};layers=['top',*[f'inner{i}' for i in range(1,next(x for x in c if x['type']=='pcb_board')['num_layers']-1)],'bottom']
blockers={l:[] for l in layers};allpads=[];ground=[]
for p in c:
 if p['type'] not in ['pcb_smtpad','pcb_plated_hole','pcb_hole']:continue
 net=netByPort.get(p.get('pcb_port_id'))
 if net=='GND' and p['type']=='pcb_smtpad' and 'thermalpad' not in p.get('port_hints',[]):ground.append(p)
 shape=p.get('shape',p.get('hole_shape','circle'))
 w=p.get('width',p.get('outer_width',p.get('outer_diameter',p.get('rect_pad_width',p.get('hole_diameter',2*p.get('radius',0))))))
 h=p.get('height',p.get('outer_height',p.get('rect_pad_height',w)))
 if shape=='circle':geo=Point(p['x'],p['y']).buffer(w/2,quad_segs=32)
 else:
  a=math.radians(p.get('ccw_rotation',p.get('rect_ccw_rotation',0)))
  geo=Polygon([(p['x']+x*math.cos(a)-y*math.sin(a),p['y']+x*math.sin(a)+y*math.cos(a)) for x,y in [(-w/2,-h/2),(w/2,-h/2),(w/2,h/2),(-w/2,h/2)]])
 allpads.append(Polygon([(p["x"]+w/2/math.cos(math.pi/8)*math.cos(math.pi/8+i*math.pi/4),p["y"]+w/2/math.cos(math.pi/8)*math.sin(math.pi/8+i*math.pi/4)) for i in range(8)]) if shape=="circle" else geo)
 if net!='GND':
  for l in ([p['layer']] if p['type']=='pcb_smtpad' else layers):blockers[l].append(geo)
viaBlocked=prep(unary_union(allpads).buffer(.34))
wireBlocked={l:prep(unary_union(b).buffer(.185)) for l,b in blockers.items()}
offsets=sorted([(x,y) for x in [-2,-1.6,-1.2,-.8,-.6,-.4,0,.4,.6,.8,1.2,1.6,2] for y in [-2,-1.6,-1.2,-.8,-.6,-.4,0,.4,.6,.8,1.2,1.6,2] if x or y],key=lambda a:a[0]**2+a[1]**2)
wires=[];vias=[];missed=[]
for p in ground:
 x,y=round(p['x'],3),round(p['y'],3);layer=p['layer'];candidates=[v for v in vias if math.hypot(v[0]-x,v[1]-y)<2]+[(x+dx,y+dy) for dx,dy in offsets]
 for vx,vy in candidates:
  vx,vy=round(vx,3),round(vy,3)
  if abs(vx)>34.5 or abs(vy)>29.5 or viaBlocked.intersects(Point(vx,vy)):continue
  dx,dy=vx-x,vy-y;m=min(abs(dx),abs(dy));sx=1 if dx>=0 else -1;sy=1 if dy>=0 else -1
  choices=[[(x,y),(x+sx*m,y+sy*m),(vx,vy)],[(x,y),(vx-sx*m,vy-sy*m),(vx,vy)],[(x,y),(vx,y),(vx,vy)],[(x,y),(x,vy),(vx,vy)]]
  points=next((q for q in choices if not wireBlocked[layer].intersects(LineString(q))),None)
  if points is None:continue
  if any(1e-6<math.hypot(vx-a,vy-b)<.56 for a,b in vias):continue
  wires.append({'layer':layer,'x':x,'y':y,'vx':vx,'vy':vy,'points':points})
  if (vx,vy) not in vias:vias.append((vx,vy))
  break
 else:missed.append(p['pcb_smtpad_id'])
unit=lambda v:round(v*1000,4)
items=[f'(wire (path {w["layer"]} 120 {" ".join(str(unit(v)) for point in w["points"] for v in point)})(net "GND")(type protect))' for w in wires]
thermal=[(p['x']+dx,p['y']+dy) for p in c if p['type']=='pcb_smtpad' and 'thermalpad' in p.get('port_hints',[]) for dx in [-.65,.65] for dy in [-.65,.65]]
items += [f'(via VIA_THERMAL_400_200 {unit(x)} {unit(y)} (net "GND")(type protect))' for x,y in thermal]
items += [f'(via VIA_400_200 {unit(x)} {unit(y)} (net "GND")(type protect))' for x,y in vias]
p=Path('../../work/compact-plane.dsn');s=p.read_text().replace('(wiring))','(wiring '+'\n'.join(items)+'))');s=s.replace('(structure ', '(structure (control (via_at_smd on)) ')
thermalStack='(padstack VIA_THERMAL_400_200 '+' '.join(f'(shape (circle {l} 400))' for l in layers)+' (attach on))'
libEnd=s.index('\n(network');s=s[:libEnd-1]+thermalStack+')'+s[libEnd:]
Path(sys.argv[1] if len(sys.argv)>1 else '../../work/compact-aligned-thermal.dsn').write_text(s)
Path('../../work/ground-escape.json').write_text(json.dumps({'wires':wires,'vias':vias,'missed':missed,'thermalVias':thermal},indent=2)+'\n')
print('Ground escape:',len(wires),'of',len(ground),'SMD pads,',len(vias),'vias;',len(missed),'left for autorouter')