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/audit-switching-nodes.py

"""Measure native PMIC-to-inductor switching copper.
Require short top-only routes and bounded narrow pin exits.
This geometry check does not replace regulator loop/thermal review.
"""
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'))
import sys
names=['SW1','SW2','SW3']
reports=[]
for name in names:
 owner=next(k for k,v in labels.items() if v==name)
 traces=[e for e in c if e['type']=='pcb_trace' and net(e)==owner]
 vias=[e for e in c if e['type']=='pcb_via' and net(e)==owner]
 ts=[t for t in terminals if t['net']==owner and not t['selector'].startswith('.PG_VIA_')]
 lines=[];layers=set();minimum_width=math.inf;neck_length=0
 for e in traces:
  for a,b in zip(e['route'],e['route'][1:]):
   if a['route_type']==b['route_type']=='wire' and a['layer']==b['layer']:
    layers.add(a['layer'])
    span=math.hypot(a['x']-b['x'],a['y']-b['y'])
    if span>1e-8:
     minimum_width=min(minimum_width,a['width'])
     if a['width']<.3499:neck_length+=span
    if math.hypot(a['x']-b['x'],a['y']-b['y'])>1e-8:lines.append(LineString([(a['x'],a['y']),(b['x'],b['y'])]))
 union=unary_union(lines);graph={}
 def key(p):return tuple(round(v,7) for v in p)
 def edge(a,b):
  a=key(a);b=key(b);d=math.dist(a,b)
  graph.setdefault(a,{})[b]=d;graph.setdefault(b,{})[a]=d
 for line in union.geoms if hasattr(union,'geoms') else [union]:
  if line.is_empty:continue
  for a,b in zip(line.coords,list(line.coords)[1:]):edge(a,b)
 # Pad copper joins route endpoints to its pin center. Keep this distance in
 # the measured path instead of making the pad an artificial zero-length hop.
 for t in ts:
  center=(t['x'],t['y']);pads=[]
  for e in c:
   if e['type']!='pcb_smtpad' or e.get('pcb_port_id')!=t['port_id']:continue
   if e['shape']=='circle':g=Point(e['x'],e['y']).buffer(e['radius'],quad_segs=32)
   elif e['shape']=='rect':g=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']))
   else:raise ValueError('Unsupported switching pad '+e['shape'])
   pads.append(g)
  pad=unary_union(pads)
  vertices=list(graph)
  for v in vertices:
   if pad.buffer(1e-7).covers(LineString([center,v])):edge(center,v)
  graph.setdefault(key(center),{})
 cpu=next(t for t in ts if t['selector'].startswith('.U2 >'))
 start=key((cpu['x'],cpu['y']));distances={start:0};queue=[(0,start)]
 while queue:
  d,p=heapq.heappop(queue)
  if d>distances[p]:continue
  for q,w in graph[p].items():
   if d+w<distances.get(q,math.inf):distances[q]=d+w;heapq.heappush(queue,(d+w,q))
 paths=[{'to':t['selector'],'lengthMm':distances.get(key((t['x'],t['y'])))} for t in ts if t is not cpu]
 limits=[e['max_length'] for e in c if e['type']=='source_trace' and net(e)==owner and 'max_length' in e]
 limit=6.0
 passed=all(p['lengthMm'] is not None and p['lengthMm']<=limit+1e-6 for p in paths) and not vias and layers=={'top'} and minimum_width>=.1499 and neck_length<=1.5+1e-7
 reports.append({'net':name,'from':cpu['selector'],'paths':paths,'maxLengthMm':limit,'vias':len(vias),'minimumWidthMm':minimum_width,'narrowExitLengthMm':neck_length,'maxNarrowExitLengthMm':1.5,'layers':sorted(layers),'pass':passed})
result={'pass':all(r['pass'] for r in reports),'switchingNodes':reports,'method':'Native centerline graph; pad connections include Euclidean distance. Physical connectivity is independently audited.'}
(ROOT/'output/switching-native-audit.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps(result))
if not result['pass']:raise SystemExit(1)