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')