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/extract-reference.py

"""Run with KiCad's bundled Python. Extract original geometry without re-drawing connectors."""
import pcbnew as k, json, pathlib, re, hashlib
ROOT=pathlib.Path(__file__).resolve().parents[1]
b=k.LoadBoard(str(ROOT/'references/CM5IO-rev2.kicad_pcb'))
def mm(p): return [round(v,6) for v in k.ToMM(p)]
def extract(f):
 f.SetOrientationDegrees(0); f.SetPosition(k.VECTOR2I(0,0))
 pads=[]; holes=[]; lines=[]
 for p in f.Pads():
  x,y=mm(p.GetPosition()); w,h=mm(p.GetSize()); dw,dh=mm(p.GetDrillSize())
  if int(round(p.GetOrientationDegrees()))%180: w,h=h,w; dw,dh=dh,dw
  d=dict(pin=p.GetNumber(),x=x,y=-y,w=w,h=h)
  if not p.GetNumber(): holes.append(dict(x=x,y=-y,d=dw)); continue
  d.update(kind='smd' if p.GetAttribute()==k.PAD_ATTRIB_SMD else 'th',dw=dw,dh=dh)
  pads.append(d)
 for g in f.GraphicalItems():
  if g.GetLayer()==k.F_SilkS and isinstance(g,k.PCB_SHAPE) and g.GetShape()==k.SHAPE_T_SEGMENT:
   a=mm(g.GetStart()); c=mm(g.GetEnd()); lines.append([[a[0],-a[1]],[c[0],-c[1]]])
 return dict(pads=pads,holes=holes,lines=lines)
D={}
for ref,id in [('Module1','cm5'),('J11','usb-c'),('J10','hdmi'),('U12','rt9742')]:
 f=next(f for f in b.GetFootprints() if f.GetReference()==ref)
 D[id]=extract(f)
 k.FootprintSave(str(ROOT/'references'),f)
for id,path in [('0402','Resistor_SMD.pretty/R_0402_1005Metric.kicad_mod'),('0805','Capacitor_SMD.pretty/C_0805_2012Metric.kicad_mod'),('1206','Capacitor_SMD.pretty/C_1206_3216Metric.kicad_mod'),('sma','Diode_SMD.pretty/D_SMA.kicad_mod')]:
 f=k.FootprintLoad('/Applications/KiCad/KiCad.app/Contents/SharedSupport/footprints/'+path.rsplit('/',1)[0],path.rsplit('/',1)[1][:-10]); D[id]=extract(f)
D['esd']={'pads':[dict(pin=str(n),x=(-.4175 if n<=5 else .4175),y=(1-(n-1)*.5 if n<=5 else -1+(n-6)*.5),w=.565,h=(.4 if n in (3,8) else .2),kind='smd') for n in range(1,11)],'holes':[],'lines':[]}
# Official combined footprint origin is a mounting hole; normalize to module centre.
for p in D['cm5']['pads']+D['cm5']['holes']: p['x']=round(p['x']-16.5,6); p['y']=round(p['y']-24,6)
for line in D['cm5']['lines']:
 for a in line: a[0]-=16.5; a[1]-=24
# Split the official combined footprint into its two physical 100-contact sockets.
for index in (1,2):
 dx=-17 if index==1 else 17
 D['cm5-'+str(index)]={'pads':[dict(p,pin=str(int(p['pin'])-100*(index-1)),x=round(p['x']-dx,6),y=round(p['y']+2.5,6)) for p in D['cm5']['pads'] if (int(p['pin'])<=100)==(index==1)],'holes':[],'lines':[[[a[0]-dx,a[1]+2.5] for a in line] for line in D['cm5']['lines'] if all(abs(a[0]-dx)<3 for a in line)]}
(ROOT/'lib/footprints.json').write_text(json.dumps(D,indent=2)+'\n')
s=(ROOT/'references/cm5-datasheet.txt').read_text().split('\n4.2. Pinout\n',1)[1].split('\n4.3.',1)[0]
p={int(a):c for a,c in re.findall(r'^\s*(\d{1,3}) {2,}(\S+)(?: \((?:Input|Output)\))? {2,}',s,re.M) if 1<=int(a)<=200}
assert len(p)==200, len(p)
old=json.loads((ROOT/'lib/cm5-pinout.json').read_text())
assert all(old[str(n)]==v for n,v in p.items()),[(n,v,old[str(n)]) for n,v in p.items() if old[str(n)]!=v]
(ROOT/'references/pinout-verification.json').write_text(json.dumps({'document':'CM5 datasheet Release 3, 2026-06-08, table 4','sha256':hashlib.sha256((ROOT/'references/cm5-datasheet.pdf').read_bytes()).hexdigest(),'pinsVerified':200,'differences':[]},indent=2)+'\n')
print('Extracted',list(D),'and verified all 200 CM5 pins')