astra/am3352-sbc

A 4-layer AM3352 single-board computer with 128‑MiB DDR3, PMIC power regulation, USB‑C 5‑V PD input, dual USB host ports, HDMI output with ESD protection, microSD/UART/JTAG interfaces, addressable RGB indicators, and a PWM-driven buzzer.

Version
0.1.19
License
unset
Stars
0

scripts/diagnose-compact-cap-placement.py

"""Try standard 0201 core decouplers while preserving their existing power feeds.
All results require fresh native placement/copper/connectivity/reference audits.
"""
from pathlib import Path
exec(compile((Path(__file__).parent/'route-power-distribution.py').read_text().split("print('geometry loaded'")[0],str(Path(__file__).parent/'route-power-distribution.py'),'exec'))
from shapely.affinity import translate
places=json.loads((ROOT/'design/decoupling-placement.json').read_text());footprints=json.loads((ROOT/'design/decoupler-footprints.json').read_text());report=[]
def pad(e):return rotate(box(e['x']-e['width']/2,e['y']-e['height']/2,e['x']+e['width']/2,e['y']+e['height']/2),e.get('ccw_rotation',0),origin=(e['x'],e['y']))
courts={}
for e in c:
 if e['type']=='pcb_courtyard_rect' and e['layer']=='bottom':
  x,y=e['center']['x'],e['center']['y'];courts[e['pcb_component_id']]=rotate(box(x-e['width']/2,y-e['height']/2,x+e['width']/2,y+e['height']/2),e.get('ccw_rotation',0),origin=(x,y))
ref=json.loads((ROOT/'output/reference-baseline.audit.json').read_text());planes=[];lines=[]
for e in ref:
 if e['type']=='pcb_copper_pour' and e['layer']=='bottom':
  b=e['brep_shape'];planes.append(Polygon([(p['x'],p['y']) for p in b['outer_ring']['vertices']], [[(p['x'],p['y']) for p in h['vertices']] for h in b['inner_rings']]))
 if e['type']=='pcb_trace' and e['pcb_trace_id'].startswith('saved_phase_'):
  for a,b in zip(e['route'],e['route'][1:]):
   if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']=='inner2':lines.append(LineString([(a['x'],a['y']),(b['x'],b['y'])]))
protected=unary_union(planes).intersection(unary_union(lines).buffer(.1));ground=next(k for k,v in labels.items() if v=='GND');drills=unary_union([Point(e['x'],e['y']).buffer(e['hole_diameter']/2+.103) for e in c if e['type']=='pcb_via']);sites=[Point(*p) for p in json.loads(os.environ.get('RESERVED_SITES','[[-0.1,-4.85],[-0.1,-4.05]]'))]
for refname in os.environ.get('CAP_REFS','C_U1_H11').split(','):
 cid=next(k for k,v in pc.items() if sc.get(v['source_component_id'])==refname);pads=[e for e in c if e['type']=='pcb_smtpad' and e['pcb_component_id']==cid];power=next(e for e in pads if '1' in e['port_hints']);gnd=next(e for e in pads if '2' in e['port_hints']);supply=net(power);assert net(gnd)==ground;old=(power['x'],power['y']);old_feed=next(e for e in c if e['type']=='source_trace' and e.get('name')==f'T_{refname}_pin1');old_trace=next(e for e in c if e['type']=='pcb_trace' and e.get('source_trace_id')==old_feed['source_trace_id']);old=tuple([old_trace['route'][-1][k] for k in ['x','y']]);old_feed_shapes=[LineString([(a['x'],a['y']),(b['x'],b['y'])]).buffer(max(a['width'],b['width'])/2,quad_segs=12) for a,b in zip(old_trace['route'],old_trace['route'][1:]) if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']];center=pc[cid]['center'];keys={pad(e).wkb for e in pads}|{g.wkb for g in old_feed_shapes};fixed=[(g,ls,n) for g,ls,n in geoms if g.wkb not in keys];trees={n:STRtree([g for g,ls,k in fixed if k!=n and 'bottom' in ls]) for n in [supply,ground]};othercourt=unary_union([g for k,g in courts.items() if k!=cid]);selected=None
 candidates=sorted([(angle,x*.1,y*.1) for angle in [0,90,180,270] for x in range(-30,31) for y in range(-30,31)],key=lambda q:q[1]**2+q[2]**2+(0 if q[0]==0 else .01))
 counts={}
 for angle,dx,dy in candidates:
  ox,oy=center['x']+dx,center['y']+dy
  trans=lambda g:translate(rotate(g,angle,origin=(0,0)),ox,oy)
  court=trans(box(-.7,-.35,.7,.35))
  if court.intersection(othercourt).area>1e-8:counts['reject_line_28']=counts.get('reject_line_28',0)+1;continue
  power_shape=trans(box(-.56,-.2,-.1,.2));ground_shape=trans(box(.1,-.2,.56,.2));point=trans(Point(-.33,0));feed=None;via_choice=None
  for v in sorted([v for v in esc['vias'] if v['net']==labels[supply]],key=lambda v:math.hypot(v['x']-point.x,v['y']-point.y)):
   if math.hypot(v['x']-point.x,v['y']-point.y)>4:continue
   candidate=LineString([(point.x,point.y),(v['x'],v['y'])]).buffer(.05,quad_segs=16)
   if any(candidate.distance(trees[supply].geometries[i])<.103 for i in trees[supply].query(candidate.buffer(.104))):continue
   if candidate.distance(ground_shape)<.103:continue
   if any(candidate.distance(site)<.253 for site in sites):continue
   if candidate.buffer(.15,join_style=2).intersection(protected).area>1e-7:continue
   feed=candidate;via_choice=v;break
  if feed is None:counts['reject_line_38']=counts.get('reject_line_38',0)+1;continue
  changed=[(power_shape,supply),(ground_shape,ground),(feed,supply)]
  if any(g.intersects(drills) for g in [power_shape,ground_shape]):counts['reject_line_40']=counts.get('reject_line_40',0)+1;continue
  if any(g.distance(site)<.253 for g,n in changed for site in sites):counts['reject_line_41']=counts.get('reject_line_41',0)+1;continue
  if any(any(g.distance(trees[n].geometries[i])<.103 for i in trees[n].query(g.buffer(.104))) for g,n in changed):counts['reject_line_42']=counts.get('reject_line_42',0)+1;continue
  if feed.distance(ground_shape)<.103:counts['reject_line_43']=counts.get('reject_line_43',0)+1;continue
  if any(n!=ground and g.buffer(.15,join_style=2).intersection(protected).area>1e-7 for g,n in changed):counts['reject_line_44']=counts.get('reject_line_44',0)+1;continue
  selected=(ox,oy,angle,court,changed,via_choice);break
 print({'component':refname,'rejects':counts},flush=True)
 if not selected:report.append({'component':refname,'found':False});continue
 ox,oy,angle,court,changed,via_choice=selected;places[refname]={'x':ox,'y':oy,'rotation':(angle+180)%360};footprints[refname]='0201';trace=next(t for t in esc['traces'] if t['name']==f'T_{refname}_pin1');assert not trace.get('waypoints');rad=-math.radians((angle+180)%360);vx,vy=old[0]-ox,old[1]-oy;trace['waypoints']=[];trace['via']=via_choice['name'];courts[cid]=court;geoms=fixed+[(g,['bottom'],n) for g,n in changed];report.append({'component':refname,'found':True,'footprint':'0201','center':[ox,oy],'rotation':angle,'replacementVia':via_choice['name']});print(report[-1],flush=True)
if os.environ.get('DRY_RUN'):
 proposal={'report':report,'placement':places,'footprints':footprints,'powerEscapes':esc};(ROOT/'output/compact-decouplers-proposal.json').write_text(json.dumps(proposal,indent=2)+'\n');print(json.dumps(report));raise SystemExit(0)
(ROOT/'design/decoupling-placement.json').write_text(json.dumps(places,indent=2)+'\n');(ROOT/'design/decoupler-footprints.json').write_text(json.dumps(footprints,indent=2)+'\n');(ROOT/'design/power-escapes.json').write_text(json.dumps(esc,indent=2)+'\n');(ROOT/'output/compact-decouplers-report.json').write_text(json.dumps(report,indent=2)+'\n');print(json.dumps(report))
if footprints and 'import decouplerFootprints' not in (ROOT/'design/nets.tsx').read_text():
 p=ROOT/'design/nets.tsx';s=p.read_text();s='import decouplerFootprints from "./decoupler-footprints.json";\n'+s;s=s.replace('name === "C_SYS_LOCAL"','(decouplerFootprints as Record<string, string>)[name] ?? (name === "C_SYS_LOCAL"',1).replace(': "0402"\n        }',': "0402")\n        }',1);p.write_text(s)