pixalynx/cm5-minimal-carrier
A 4-layer Raspberry Pi CM5 carrier board routes USB‑C 5V power through an RT9742 load switch with TVS protection and capacitive filtering, while providing ESD-protected HDMI output, CM5 connectors, test points, and ground/power planes.
- Version
- 0.1.1
- License
- unset
- Stars
- 0
scripts/route.py
"""Explicit routing. No generic autorouting of HDMI. All polylines are reviewable millimetres."""
import json,pathlib,math
R=pathlib.Path(__file__).resolve().parents[1]; D=json.loads((R/'lib/design.json').read_text()); F=json.loads((R/'lib/footprints.json').read_text()); P={p['ref']:p for p in D['parts']};routes=[]
M=json.loads((R/'lib/manufacturing.json').read_text());S=json.loads((R/'lib/stackup.json').read_text())
def phys(fp,n):
if fp=='usb-c':return '25' if n=='S1' else str(int(n[1:])+(12 if n[0]=='B' else 0))
if fp=='hdmi' and n.startswith('SH'):return str(19+int(n[2:]))
return n
def pos(ref,n):
p=P[ref];q=next(q for q in F[p['footprint']]['pads'] if phys(p['footprint'],q['pin'])==str(n));a=math.radians(p['rotation']);x=q['x']*(-1 if p['layer']=='bottom' else 1);y=q['y'];return (round(p['x']+x*math.cos(a)-y*math.sin(a),6),round(p['y']+x*math.sin(a)+y*math.cos(a),6))
def v(x,y,fr='top',to='bottom'):return {'x':x,'y':y,'via':True,'fromLayer':fr,'toLayer':to}
def route(fr,to,pts,w=.15):
path=[dict(x=round(p[0],6),y=round(p[1],6)) if isinstance(p,tuple) else p for p in pts];routes.append(dict(from_=fr,to=to,width=w,path=path)); routes[-1]['from']=routes[-1].pop('from_')
def port(r,n):return f'{r}.pin{n}'
def plane(ref,n,x,y,net,w=.2):
q=pos(ref,n);route(port(ref,n),'net.'+net,[q,(x,y),v(x,y,P[ref]['layer'],'inner1' if net=='GND' else 'inner2')],w)
# HDMI coupled-run geometry comes from the recorded JLC calculator result.
pair_width=S['traceWidthMm']; half_pitch=(pair_width+S['traceGapMm'])/2
# Each leg goes across both active and NC ESD lands: never assume internal continuity.
for i,lane in enumerate(['TX2','TX1','TX0','CLK']):
esd='U'+str(2+i//2);en=[1,2] if i%2==0 else [4,5];pn=[70,72] if i==0 else [76,78] if i==1 else [82,84] if i==2 else [88,90];hn=[1+3*i,3+3*i]
starts=[pos('J2',n) for n in pn];ed=[pos(esd,n) for n in en];ends=[pos('J4',n) for n in hn];sc=sum(q[1] for q in starts)/2;ec=sum(q[1] for q in ed)/2;hc=sum(q[1] for q in ends)/2
for pol,cn,ep,hp,s,e,h in zip([1,-1],pn,en,hn,starts,ed,ends):
nc=11-ep;out=pos(esd,nc); sy=sc+pol*half_pitch; ey=ec+pol*half_pitch; hy=hc+pol*half_pitch
pts=[s,(9.5,s[1]),(10.3,sy),(12,sy),(12+abs(ec-sc),ey),(16.5,ey),e,out,(19.05,ey),(19.2,ey),(19.2+abs(hc-ec),hy),(20.35,h[1]),h]
route(port('J2',cn),port('J4',hp),pts,pair_width)
# One top escape and plane via for every ground contact on both sockets.
for ref in ['J1','J2']:
for n,net in P[ref]['nets'].items():
if net!='GND':continue
x,y=pos(ref,n);plane(ref,n,x+(-.8 if int(n)%2 else .8),y,'GND',.15)
# Six 5 V contacts, two vias per contact. The broad L3 plane carries the long run.
for n in [77,79,81,83,85,87]:
x,y=pos('J1',n)
plane('J1',n,x-.8-(.3 if n%4==3 else 0),y,'CM5_5V',.2);plane('J1',n,x-1.45-(.3 if n%4==3 else 0),y,'CM5_5V',.2)
# Connector and ESD grounds; all shield stakes are directly tied to the ground plane.
for n in [2,5,8,11,17]:plane('J4',n,23.6,pos('J4',n)[1],'GND',.25)
for ref in ['U2','U3','U4']:
for n,x in ([(3,15.7),(8,17.9)] if ref=='U4' else [(3,16.8),(8,18.9)]):plane(ref,n,x,pos(ref,n)[1],'GND',.18)
for n in [1,12]:plane('J3',n,24.6,pos('J3',n)[1],'GND',.35)
for n in [4,9]:
for x in [24.6,24.0,23.4]:plane('J3',n,x,pos('J3',n)[1],'CM5_5V',.45)
for ref,n,x,y,net,w in [('U1',1,12.8,.95,'CM5_5V',.5),('U1',3,19,0,'GND',.5),('D1',1,16.3,23,'CM5_5V',.8),('D1',2,24,23,'GND',.8),('C1',1,19.5,17,'CM5_5V',.5),('C1',2,24.1,17,'GND',.5),('C2',1,-22.2,-9,'CM5_5V',.5),('C2',2,-27.8,-9,'GND',.5),('C3',1,-20.4,-12,'CM5_5V',.2),('C3',2,-23.4,-12,'GND',.2),('C4',1,10,3,'CM5_5V',.4),('C4',2,14,3,'GND',.4),('C5',2,17.3,-3,'GND',.4),('C6',2,-24.5,-5,'GND',.2)]:plane(ref,n,x,y,net,w)
# USB-C CC: direct to the on-module PD controller. No external Rd, no USB data.
for n,un,cx,yy,oy in [(94,5,-24.65,-21,25.0),(96,17,-24.2,-20.4,24.5)]:
s=pos('J1',n);e=pos('J3',un);vx=23.0 if n==96 else oy
pts=[s,v(cx,s[1]),((-25.7 if n==94 else cx),s[1]),((-25.7 if n==94 else cx),yy),(oy,yy),*(([(oy,8),(vx,8)]) if n==96 else []),(vx,e[1]),v(vx,e[1],'bottom','top'),e]
route(port('J1',n),port('J3',un),pts)
# GPIO reference is mandatory even with no GPIO header. It comes from CM5, never an external 3.3 V supply.
for n in [84,86]:
s=pos('J1',78);e=pos('J1',n);route(port('J1',78),port('J1',n),[s,(-23,s[1]),(-23,e[1]),e],.2)
s=pos('J1',78);e=pos('C6',1);route('J1.pin78','C6.pin1',[s,(-23,s[1]),(-22,-6),v(-22,-6),e],.2)
# Low-speed HDMI controls use the back; keep vias out of the TMDS channel.
for n,u,cx,mid,ey in [(51,1,4.6,14,-15.5),(53,5,4.1,10,-17.5),(100,2,10.5,13,-16)]:
s=pos('J2',n);e=pos('U4',u);route(port('J2',n),port('U4',u),[s,v(cx,s[1]),(mid,s[1]),(mid,ey),v(16.0,ey,'bottom','top'),e])
s=pos('J2',99);e=pos('U4',4);route('J2.pin99','U4.pin4',[s,v(3.9,s[1]),(9.5,s[1]),v(9.5,-13.7,'bottom','top'),(12.8,-17),e])
for u,h in [(1,13),(2,15),(4,16),(5,19)]:
s=pos('U4',u);o=pos('U4',11-u);e=pos('J4',h);route(port('U4',u),port('J4',h),[s,o,(18.0,s[1]),(20.35,e[1]),e])
# HDMI +5 V: protected reference switch, output capacitor and supply pin 18.
s=pos('U1',2);e=pos('C5',1);route('U1.pin2','C5.pin1',[s,(13,-.95),(13,-3),e],.4)
s=pos('C5',1);e=pos('J4',18);route('C5.pin1','J4.pin18',[s,(14.7,-2),(25.4,-2),(25.4,-15),e],.3)
# Bare, paste-free probe pads. No branches are added to any HDMI signal or CC line.
for tp in M['testPoints']:
s=(tp['x'],tp['y'])
if tp['net'] in ['GND','CM5_5V']:
x=tp['x']-.8;route(port(tp['ref'],1),'net.'+tp['net'],[s,(x,tp['y']),v(x,tp['y'],'bottom','inner1' if tp['net']=='GND' else 'inner2')],.25)
elif tp['net']=='CM5_3V3':
route(port(tp['ref'],1),'C6.pin1',[s,(-21,-6),pos('C6',1)],.2)
else:
route(port(tp['ref'],1),'C5.pin1',[s,(13,-1),v(13,-1,'bottom','top'),(13,-3),pos('C5',1)],.25)
(R/'lib/routes.json').write_text(json.dumps(routes,indent=2)+'\n')
print('Explicit copper paths:',len(routes))