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/route-ddr-multilayer.py
"""Development fallback for selected congested address/control nets. Adds clearance-checked through vias, then tunes planar paths. Requires a geometry checkpoint and the compiled maze helper in ../../work."""
import json,math,struct,subprocess,runpy,os,re
from pathlib import Path
import numpy as np,shapely
from shapely.geometry import Point,LineString
from shapely.ops import unary_union
os.environ['AUDIT_CIRCUIT']='../../work/routing-geometry.json'
x=runpy.run_path('scripts/audit-copper.py');d=json.load(open('design/routed-copper.json'));c=x['c'];names={e['source_net_id']:e['name'] for e in c if e['type']=='source_net'}
selected={s:n for s,n in names.items() if re.match(r'DDR_(?:D[0-9]+$|DQM|DQS|CK|A[0-9]+$|BA|CASn|RASn|WEn|CSn|ODT)',n)}
requested=os.environ.get('DDR_NETS','').split(',')
assert requested!=[''], 'Set DDR_NETS to the remaining address/control nets'
selected={s:n for s,n in selected.items() if n in requested}
assert all(not n.startswith('DDR_D') for n in selected.values()), 'Use the byte targets in route-ddr-matched.py for data nets'
def pos(p):return (p['x'],p['y'])
esc={}
for sid,n in selected.items():
rs=d['nets'][sid];top=[r for r in rs if len(r)>1 and all(p['route_type']=='wire' and p['layer']=='top' for p in r) and sum(math.dist(pos(p),pos(q)) for p,q in zip(r,r[1:]))<1.5];coords={pos(p) for r in top for p in r};vias=[r for r in rs if any(p['route_type']=='via' and pos(p) in coords for p in r)];ends=[pos(next(p for p in r if p['route_type']=='via')) for r in vias]
assert len(ends)==2,(n,ends)
esc[sid]=ends;d['nets'][sid]=top+vias
Path('../../work/ddr-stripped.json').write_text(json.dumps(d))
# Fixed pad geometry only; traces and vias are taken from the evolving route data.
fixed={l:[] for l in ['top','inner2','inner3','inner4','inner5','bottom']}
for g,m in zip(x['shapes'],x['meta']):
if m[0] in fixed and not m[2].startswith(('fr_','pcb_copper_pour','pcb_via')):fixed[m[0]].append((g,m[1]))
step=.05;ox=-34.4;oy=-29.4;W=1377;H=1177;xx,yy=np.meshgrid(ox+np.arange(W)*step,oy+np.arange(H)*step)
def raster(g):return shapely.intersects_xy(g,xx,yy).astype(np.uint8)
def idx(p):return round((p[1]-oy)/step)*W+round((p[0]-ox)/step)
def xy(i):return (round(ox+i%W*step,6),round(oy+i//W*step,6))
def maze(block,a,b):
target=np.zeros((H,W),np.uint8);target.ravel()[b]=1
with open('../../work/full-maze-in.bin','wb') as f:f.write(struct.pack('iiii',W,H,1,1));f.write(block.tobytes());f.write(target.tobytes());f.write(bytes(W*H));f.write(struct.pack('i',a))
subprocess.run(['../../work/maze','../../work/full-maze-in.bin','../../work/full-maze-out.bin'],stdout=subprocess.DEVNULL)
return list(np.frombuffer(Path('../../work/full-maze-out.bin').read_bytes()[4:],np.int32))
def obstacles(sid,l):
gs=[g for g,n in fixed[l] if n!=x['find'](sid)]
for s,rs in d['nets'].items():
if s==sid:continue
for r in rs:
for p in r:
if p['route_type']=='via':gs.append(Point(*pos(p)).buffer(p.get('via_diameter',.3)/2))
for p,q in zip(r,r[1:]):
if p['route_type']==q['route_type']=='wire' and p['layer']==q['layer']==l:gs.append(LineString([pos(p),pos(q)]).buffer(p.get('width',.1)/2))
return unary_union(gs)
def simplify(coords):
out=[coords[0]]
for i in range(1,len(coords)-1):
a,b,z=coords[i-1:i+2]
if abs((b[0]-a[0])*(z[1]-b[1])-(b[1]-a[1])*(z[0]-b[0]))>1e-8:out.append(b)
out.append(coords[-1]);return out
def tune(coords,obs,target):
from shapely.geometry import box
for iteration in range(20):
line=LineString(coords);remaining=target-line.length
if remaining<.01:return coords
best=None
for si,(a,b) in sorted(enumerate(zip(coords,coords[1:])),key=lambda it:-math.dist(*it[1])):
le=math.dist(a,b)
if le<.7:continue
ux=(b[0]-a[0])/le;uy=(b[1]-a[1])/le
others=unary_union([LineString([p,q]) for j,(p,q) in enumerate(zip(coords,coords[1:])) if j!=si])
for span in sorted(set([le-.4,min(le-.4,2),min(le-.4,1),.6]),reverse=True):
if span<.5 or span>le-.1:continue
for t in [.2,(le-span)/2,le-span-.2]:
for sign in [1,-1]:
ch=min(.15,span/4,max(.025,remaining/4))
for h in [(remaining+4*ch*(2-math.sqrt(2)))/2]+[v*.2 for v in range(min(75,int(remaining/.4)+1),1,-1)]:
if h<2*ch or h>16 or t<ch or t+span+ch>le:continue
points=[(0,0),(t-ch,0),(t,ch),(t,h-ch),(t+ch,h),(t+span-ch,h),(t+span,h-ch),(t+span,ch),(t+span+ch,0),(le,0)]
cc=[(a[0]+u*ux-v*uy*sign,a[1]+u*uy+v*ux*sign) for u,v in points];candidate=LineString(cc);extra=candidate.length-le
if extra>remaining+.000001 or extra<.015 or (best and extra<best[0]):continue
if not box(-34.4,-29.4,34.4,29.4).covers(candidate) or candidate.distance(obs)<.155:continue
allowed=Point(*a).buffer(.45).union(Point(*b).buffer(.45))
if candidate.difference(allowed).distance(others)<.30:continue
full=coords[:si]+cc+coords[si+2:]
if not LineString(full).is_simple:continue
best=(extra,full)
break
if best is None:return coords
coords=best[1]
return coords
ls=['inner2','inner5','inner3','inner4','bottom','top'];N=W*H
for sid,n in selected.items():
a,b=esc[sid];obs=[obstacles(sid,l) for l in ls];blocked=np.asarray([raster(g.buffer(.17)) for g in obs],np.uint8)
viaObstacles=unary_union(obs+[g for l in fixed for g,net in fixed[l]]+[Point(*v).buffer(.15) for v in esc[sid]])
via=(1-raster(viaObstacles.buffer(.285))).astype(np.uint8)
starts=np.array([z*N+idx(a) for z in range(len(ls)) if not blocked[z].ravel()[idx(a)]],np.int32);target=np.zeros_like(blocked)
for z in range(len(ls)):target[z].ravel()[idx(b)]=1
with open('../../work/full-maze-in.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())
subprocess.run(['../../work/maze','../../work/full-maze-in.bin','../../work/full-maze-out.bin'],stdout=subprocess.DEVNULL)
path=list(np.frombuffer(Path('../../work/full-maze-out.bin').read_bytes()[4:],np.int32))
if not path:print('FAILED',n,flush=True);continue
chunks=[];newvias=[];coords=[a];z=path[0]//N
for v in path:
l=v//N;p=xy(v%N)
if l!=z:chunks.append([z,simplify(coords)]);coords=[p];newvias.append(p);z=l
elif not coords or p!=coords[-1]:coords.append(p)
if coords[-1]!=b:coords.append(b)
chunks.append([z,simplify(coords)])
escapeLength=sum(math.dist(pos(p),pos(q)) for r in d['nets'][sid] for p,q in zip(r,r[1:]) if p['route_type']==q['route_type']=='wire' and p['layer']==q['layer'])
wanted=35.02-escapeLength
total=sum(LineString(cc).length for z,cc in chunks)
for i in sorted(range(len(chunks)),key=lambda j:-LineString(chunks[j][1]).length):
z,cc=chunks[i];other=unary_union([LineString(q).buffer(.05) for j,(layer,q) in enumerate(chunks) if j!=i and layer==z]);ob=obs[z].union(other)
before=LineString(cc).length;cc=tune(cc,ob,before+wanted-total);total+=LineString(cc).length-before;chunks[i]=[z,cc]
for z,cc in chunks:
assert LineString(cc).distance(obs[z])>=.149,(n,'clearance')
d['nets'][sid].append([dict(route_type='wire',x=x,y=y,width=.1,layer=ls[z]) for x,y in cc])
for vx,vy in newvias:d['nets'][sid].append([dict(route_type='wire',x=vx,y=vy,width=.1,layer='top'),dict(route_type='via',x=vx,y=vy,from_layer='top',to_layer='bottom',via_diameter=.3,via_hole_diameter=.15),dict(route_type='wire',x=vx,y=vy,width=.1,layer='bottom')])
Path('design/routed-copper.json').write_text(json.dumps(d,separators=(',',':'))+'\n')
print(n,'length',round(total+escapeLength,6),'new vias',len(newvias),flush=True)