seveibar/am3352-dev-board-4layer-dogbone

Defines PCB footprints and pin mappings for a TI AM3352ZCZ100 processor, JST GH connectors, and assembly-part lookup helpers for capacitors, resistors, and headers.

Version
0.1.5
License
unset
Stars
0

scripts/check-ddr-skew.py

"""DDR pin-to-pin planar length validation; excludes via and package delay."""
import json,math,heapq,hashlib,re,os,sys
from pathlib import Path
from collections import defaultdict
from via_travel import via_travel
p=Path(os.environ.get('AUDIT_CIRCUIT','dist/index/circuit.json'));c=json.loads(p.read_text());nets={e['source_net_id']:e['name'] for e in c if e['type']=='source_net'};st={e['source_trace_id']:e for e in c if e['type']=='source_trace'};src={e['source_component_id']:e['name'] for e in c if e['type']=='source_component'};sp={e['source_port_id']:e for e in c if e['type']=='source_port'};ports={e['source_port_id']:e for e in c if e['type']=='pcb_port'};layers=['top',*[f'inner{i}' for i in range(1,next(e for e in c if e['type']=='pcb_board')['num_layers']-1)],'bottom'];results={}
def key(p,l=None):return (round(p['x'],3),round(p['y'],3),l or p['layer'])
for sid,name in nets.items():
 if not re.match(r'DDR_(?:D\d+$|DQM\d|DQS|CK|A\d+$|BA\d|CASn|RASn|WEn|CSn|ODT|RESETn)',name):continue
 graph=defaultdict(dict);viaSites=set();total=0
 def edge(a,b,w):
  if a!=b:graph[a][b]=min(w,graph[a].get(b,float('inf')));graph[b][a]=min(w,graph[b].get(a,float('inf')))
 for t in c:
  if t['type']!='pcb_trace' or sid not in st.get(t.get('source_trace_id'),{}).get('connected_source_net_ids',[]):continue
  r=t['route']
  for a,b in zip(r,r[1:]):
   if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:
    w=math.hypot(a['x']-b['x'],a['y']-b['y']);edge(key(a),key(b),w);total+=w
  for a in r:
   if a['route_type']=='via':
    viaSites.add((a['x'],a['y']));ls=layers[layers.index(a['from_layer']):layers.index(a['to_layer'])+1]
    for l in ls[1:]:edge(key(a,ls[0]),key(a,l),0)
 ends={}
 for t in st.values():
  if sid not in t.get('connected_source_net_ids',[]):continue
  for s in t['connected_source_port_ids']:
   ref=src[sp[s]['source_component_id']]
   if ref in ['U1','U3']:
    pp=ports[s];node=key(pp,pp['layers'][0]);ends[ref]=node
 assert set(ends)=={'U1','U3'},(name,ends)
 start,end=ends['U1'],ends['U3'];dist={start:0};prev={};q=[(0,start)]
 while q:
  d,u=heapq.heappop(q)
  if d!=dist[u]:continue
  if u==end:break
  for v,w in graph[u].items():
   if d+w<dist.get(v,float('inf')):dist[v]=d+w;prev[v]=u;heapq.heappush(q,(d+w,v))
 if end not in dist:
  results[name]={'status':'INCOMPLETE','pathLengthMm':None,'manhattanMm':round(abs(start[0]-end[0])+abs(start[1]-end[1]),6),'endpoints':{'U1':start,'U3':end},'routedVias':len(viaSites)}
  continue
 path=[end]
 while path[-1]!=start:path.append(prev[path[-1]])
 planar=defaultdict(float);transitions=set()
 for a,b in zip(path,path[1:]):
  if a[2]==b[2]:planar[a[2]]+=math.dist(a[:2],b[:2])
  else:transitions.add(a[:2])
 results[name]={'manhattanMm':round(abs(start[0]-end[0])+abs(start[1]-end[1]),6),'endpoints':{'U1':start,'U3':end},'pathLengthMm':round(dist[end],6),'totalCopperMm':round(total,6),'extraCopperMm':round(total-dist[end],6),'routedVias':len(viaSites),'pathViaSites':len(transitions),'planarLengthByLayerMm':dict(planar)}
 thickness=next(e for e in c if e['type']=='pcb_board')['thickness']
 travel=via_travel(path,layers,thickness)
 results[name].update(travel)
 results[name]['lengthIncludingViasMm']=round(dist[end]+travel['verticalLengthMm'],6) if travel['verticalLengthMm'] is not None else None
expected={f'DDR_D{i}' for i in range(16)}|{f'DDR_A{i}' for i in range(13)}|{f'DDR_BA{i}' for i in range(3)}|{'DDR_DQM0','DDR_DQM1','DDR_DQS0','DDR_DQSn0','DDR_DQS1','DDR_DQSn1','DDR_CK','DDR_CKn','DDR_CKE','DDR_CASn','DDR_RASn','DDR_WEn','DDR_CSn0','DDR_ODT','DDR_RESETn'}
assert set(results)==expected, ('DDR coverage mismatch',expected-set(results),set(results)-expected)
def skew(label,ns,limit):
 missing=[n for n in ns if results[n]['pathLengthMm'] is None]
 if missing:return dict(group=label,skewMm=None,limitMm=limit,status='INCOMPLETE',missingNets=missing,nets=ns)
 ordered=sorted(ns,key=lambda n:results[n]['pathLengthMm']);lo,hi=ordered[0],ordered[-1];value=results[hi]['pathLengthMm']-results[lo]['pathLengthMm'];return dict(group=label,skewMm=round(value,6),limitMm=limit,status='FAIL' if limit is not None and value>limit else 'SCREEN ONLY' if limit is None else 'PASS',shortest=lo,longest=hi,nets=ns)
groups=[]
for b in [0,1]:
 dq=[f'DDR_D{i}' for i in range(b*8,b*8+8)]+[f'DDR_DQM{b}'];dqs=[f'DDR_DQS{b}',f'DDR_DQSn{b}'];groups.append(skew(f'Byte {b}: DQ + DM',dq,.635));groups.append(skew(f'Byte {b}: DQ + DM + DQS envelope',dq+dqs,.635));groups.append(skew(f'DQS{b} differential pair',dqs,.127))
groups.append(skew('CK differential pair',['DDR_CK','DDR_CKn'],.127));addr=[n for n in results if re.match(r'DDR_(A\d+$|BA\d|CASn|RASn|WEn|CSn|CKE|ODT)',n)];groups.append(skew('Address/control + CK envelope',addr+['DDR_CK','DDR_CKn'],.635))
# Conservative placement limit: maximum actual pin-pair Manhattan distance.
# Single x16 device, direct point-to-point routes (no fly-by branch/stub).
for b in [0,1]:
 ns=[f'DDR_D{i}' for i in range(b*8,b*8+8)]+[f'DDR_DQM{b}',f'DDR_DQS{b}',f'DDR_DQSn{b}']
 if any(results[n]['pathLengthMm'] is None for n in ns):
  groups.append(dict(group=f'Byte {b}: placement-derived maximum length',status='INCOMPLETE',nets=ns));continue
 limit=max(results[n]['manhattanMm'] for n in ns);value=max(results[n]['pathLengthMm'] for n in ns)
 groups.append(dict(group=f'Byte {b}: placement-derived maximum length',maximumLengthMm=value,limitMm=limit,status='PASS' if value<=limit+.001 else 'FAIL',nets=ns))
ns=addr+['DDR_CK','DDR_CKn'];nominal=max(results[n]['manhattanMm'] for n in ns)+7.62
if any(results[n]['pathLengthMm'] is None for n in ns):
 groups.append(dict(group='Address/control + CK nominal length',status='INCOMPLETE',nets=ns))
else:
 lo=min(results[n]['pathLengthMm'] for n in ns);hi=max(results[n]['pathLengthMm'] for n in ns)
 groups.append(dict(group='Address/control + CK nominal length',minimumLengthMm=lo,maximumLengthMm=hi,nominalMm=nominal,allowedMinimumMm=nominal-1.27,allowedMaximumMm=nominal+1.27,status='PASS' if lo>=nominal-1.27 and hi<=nominal+1.27 else 'FAIL',nets=ns))
# These eight skew groups additionally include the used vertical barrel spans.
# Absolute placement-derived limits above remain explicitly planar checks.
for planar_group in list(groups[:8]):
 ns=planar_group['nets'];missing=[n for n in ns if results[n].get('lengthIncludingViasMm') is None]
 if missing:groups.append(dict(group=planar_group['group']+' including vias',status='INCOMPLETE',missingNets=missing,nets=ns));continue
 values=[results[n]['lengthIncludingViasMm'] for n in ns];value=max(values)-min(values);limit=planar_group['limitMm']
 groups.append(dict(group=planar_group['group']+' including vias',skewMm=round(value,6),limitMm=limit,status='PASS' if value<=limit else 'FAIL',nets=ns))
report={'circuitSha256' :hashlib.sha256(p.read_bytes()).hexdigest(),'method':'Shortest processor-ball-center to RAM-ball-center path through routed centerlines, joining graph nodes at 1 micrometre resolution and weighting edges by unrounded geometry. Planar path lengths exclude vias. Separate skew groups include used vertical barrel travel: collapse zero-length barrel graph nodes, sum layer-depth differences, and report a value only when unspecified inner-layer depths cancel algebraically. Package delay and layer-dependent propagation are not included. For this single-device point-to-point topology the complete address/control paths are matched within 0.635 mm. Nominal lengths use the maximum actual pin-pair Manhattan distance; no cross-net bounding-box allowance is assumed.','source':'https://www.ti.com/lit/ds/symlink/am3352.pdf','groups':groups,'nets':results}
Path(os.environ.get('DDR_REPORT_PATH','design/ddr-skew-check.json')).write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(groups,indent=2));print('Largest excess total copper over point-to-point path:',max((r['extraCopperMm'] for r in results.values() if 'extraCopperMm' in r),default=0))

if "--strict" in sys.argv:
 failures=[g for g in groups if g["status"] in ["FAIL","INCOMPLETE"]]
 if failures:raise SystemExit("DDR length matching failed: "+", ".join(g["group"] for g in failures))