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-matched.py
"""Development helper: reroute selected DDR nets, preserving BGA escapes. Requires a geometry checkpoint and the compiled maze helper in ../../work. Run after releasing redundant plane tracks; rebuild and audit all planes afterward."""
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(',')
if requested!=['']:selected={s:n for s,n in selected.items() if n in requested}
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=float(os.environ.get('DDR_GRID','.05'));ox=-34.4;oy=-29.4;W=round(68.8/step)+1;H=round(58.8/step)+1;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
# Long data paths first, then their shorter neighbors; address bus last.
def priority(s):
n=selected[s];data=n.startswith(('DDR_D','DDR_DQS','DDR_DQM'));pair=n.startswith(('DDR_DQS','DDR_CK')) and not n.startswith('DDR_CKE')
byte1=(n.startswith(('DDR_DQS','DDR_DQM')) and n[-1]=='1') or (n.startswith('DDR_D') and n[5:].isdigit() and int(n[5:])>=8)
return (0 if byte1 else 1 if data else 2,0 if pair else 1,-math.dist(*esc[s]))
for sid in sorted(selected,key=priority):
n=selected[sid];a,b=esc[sid];best=None
data=n.startswith('DDR_D');byte1=(n.startswith(('DDR_DQS','DDR_DQM')) and n[-1]=='1') or (n.startswith('DDR_D') and n[5:].isdigit() and int(n[5:])>=8)
target=(12.1 if byte1 else 18.5) if data else 35.02
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=target-escapeLength
for l in ['inner2','inner5','inner3','inner4','bottom','top']:
obs=obstacles(sid,l);block=raster(obs.buffer(.151 if step<.04 else .172));start,end=idx(a),idx(b)
if block.ravel()[start] or block.ravel()[end]:continue
path=maze(block,start,end)
if not path:continue
coords=simplify([a]+[xy(i) for i in path]+[b]);line=LineString(coords)
if line.distance(obs)<.149:continue
if line.length>wanted+.001:continue
coords=tune(coords,obs,wanted);line=LineString(coords)
if best is None or line.length>best[0]:best=(line.length,l,coords)
if wanted-line.length<.01:break
if best:
length,l,coords=best;d['nets'][sid].append([dict(route_type='wire',x=a,y=b,width=.1,layer=l) for a,b in coords]);print(n,l,round(length+escapeLength,4),flush=True)
else:print('FAILED',n,flush=True)
Path('design/routed-copper.json').write_text(json.dumps(d,separators=(',',':'))+'\n')