seveibar/am3352-ram-dogbone-and-single-layer-route-test

AM3352BZCZ100 processor connected to a W631GG6MB-12 DDR3 memory chip through a routed, length-matched 16-bit DDR interface with clock/data-strobe pairs and control/address signals.

Version
0.0.9
License
unset
Stars
0

scripts/audit-pair-coupling.py

"""Bidirectional sampled coupling audit, separate from skew and copper clearance.

The first/last 6.2 mm of each signal-layer route are allowed pin escapes.
Every interior sample must stay within 0.10–0.155 mm edge gap of its mate.
A fixed escape budget prevents independent tuning loops being exempted.
"""
import hashlib,json,sys
from pathlib import Path
from shapely.geometry import LineString
STEP=.01
ESCAPE=6.2
MIN_GAP=.0999
MAX_GAP=.155
PAIRS=[('DDR_DQS_PAIR0',15,16),('DDR_DQS_PAIR1',27,26),('DDR_CK_PAIR',32,33)]
def audit(circuit):
 traces={t['source_trace_id']:t for t in circuit if t['type']=='pcb_trace'}
 result=[]
 for name,x,y in PAIRS:
  members=[];lines=[];widths=[];total=[]
  for n in [x,y]:
   route=traces[f'source_trace_{n}']['route'];vi=[i for i,p in enumerate(route) if p['route_type']=='via'];assert len(vi)==2
   mid=route[vi[0]+1:vi[1]];assert len({p['layer'] for p in mid})==1
   lines.append(LineString([(p['x'],p['y']) for p in mid]));widths.append(mid[0]['width'])
   total.append(sum(((b['x']-a['x'])**2+(b['y']-a['y'])**2)**.5 for a,b in zip(route,route[1:]) if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']))
  failures=0
  for n,a,b in [(x,lines[0],lines[1]),(y,lines[1],lines[0])]:
   count=int(a.length/STEP)+1;ss=[a.length*i/count for i in range(count+1)];gaps=[a.interpolate(s).distance(b)-sum(widths)/2 for s in ss]
   interior=[g for s,g in zip(ss,gaps) if ESCAPE<=s<=a.length-ESCAPE]
   bad=[s for s,g in zip(ss,gaps) if g>MAX_GAP];violations=sum(g<MIN_GAP or g>MAX_GAP for g in interior);failures+=violations
   start=max((s for s in bad if s<ESCAPE),default=0);end=a.length-min((s for s in bad if s>a.length-ESCAPE),default=a.length)
   members.append(dict(sourceTraceId=f'source_trace_{n}',signalLengthMm=a.length,mainMinEdgeGapMm=min(interior),mainMaxEdgeGapMm=max(interior),mainP95EdgeGapMm=sorted(interior)[int(.95*(len(interior)-1))],wholeRouteMaxEdgeGapMm=max(gaps),fractionWithinGapBand=sum(MIN_GAP<=g<=MAX_GAP for g in gaps)/len(gaps),startEscapeExtentMm=start,endEscapeExtentMm=end,interiorViolatingSamples=violations))
  skew=abs(total[0]-total[1]);result.append(dict(name=name,planarSkewMm=skew,maxSkewMm=.127,members=members,status='PASS' if failures==0 and skew<=.127 else 'FAIL'))
 return dict(status='PASS' if all(p['status']=='PASS' for p in result) else 'FAIL',method='Bidirectional nearest-centerline sampling; edge gap subtracts half of each trace width. Copper audit independently checks continuous clearance.',sampleStepMm=STEP,endpointEscapeBudgetMm=ESCAPE,mainGapBandMm=[.1,MAX_GAP],pairs=result)
if __name__=='__main__':
 root=Path(__file__).resolve().parents[1];path=Path(sys.argv[1]) if len(sys.argv)>1 else root/'dist/index/circuit.json';raw=path.read_bytes();report=audit(json.loads(raw));report['circuitSha256']=hashlib.sha256(raw).hexdigest();out=Path(sys.argv[2]) if len(sys.argv)>2 else root/'validation/pair-coupling.json';out.write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report,indent=2));sys.exit(report['status']!='PASS')