721 lines
32 KiB
Python
721 lines
32 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Baby Mobile v5 — Integrated Schematic + PCB Generator
|
||
|
||
Fixes from v4 feedback:
|
||
(a) Generates BOTH .kicad_sch and .kicad_pcb
|
||
(b) XIAO USB-C on TOP edge, pin rows 15mm apart,
|
||
carrier pads extended to 20mm with castellated edges
|
||
(c) Each sub-board has unique net names:
|
||
- Carrier: C_S1..C_S8, C_VBAT, C_3V3, SPI_*, FLASH_CS, BOOST_SW, AUDIO_FILT
|
||
- SOP-8 adapter: A8_P1..A8_P8, A8_VBAT, A8_GND
|
||
- SOP-16N adapter: A16N_P1..A16N_P16, A16N_VBAT, A16N_GND
|
||
- SOP-16W adapter: A16W_P1..A16W_P16, A16W_VBAT, A16W_GND
|
||
FPC connects carrier nets to adapter nets (separate pins, separate nets)
|
||
(d) Key-shaped board: wide carrier head, narrow adapter stem
|
||
Adapters stacked vertically in stem, v-scored on narrow (short) edges
|
||
|
||
Board shape:
|
||
┌────────────────────────────┐
|
||
│ CARRIER (26×22mm) │
|
||
│ XIAO, Flash, Boost, │ wide "head"
|
||
│ FPC connector │
|
||
├──────┬─────────────┬───────┤
|
||
│ ADAPTER │ narrow "stem"
|
||
│ STRIP │
|
||
│ (12×30mm) │
|
||
│ SOP-8 │←v-score
|
||
│ SOP-16N │←v-score
|
||
│ SOP-16W │
|
||
└─────────────┘
|
||
"""
|
||
|
||
import uuid, os, json, sys
|
||
|
||
LIB = "bm"
|
||
|
||
def uid():
|
||
return str(uuid.uuid4())
|
||
|
||
# ============================================================
|
||
# NET DEFINITIONS — unique per sub-board
|
||
# ============================================================
|
||
|
||
# We'll build a flat net list. Each net gets a unique integer ID.
|
||
NETS = {}
|
||
_net_counter = [0]
|
||
|
||
def net(name):
|
||
"""Register a net name and return its ID."""
|
||
if name not in NETS:
|
||
_net_counter[0] += 1
|
||
NETS[name] = _net_counter[0]
|
||
return NETS[name]
|
||
|
||
def net_decl():
|
||
"""Generate all (net N "name") declarations."""
|
||
lines = [' (net 0 "")']
|
||
for name, idx in sorted(NETS.items(), key=lambda x: x[1]):
|
||
lines.append(f' (net {idx} "{name}")')
|
||
return "\n".join(lines)
|
||
|
||
# Pre-register all nets
|
||
# Carrier power
|
||
net("GND"); net("C_VBAT"); net("C_3V3"); net("BOOST_SW"); net("AUDIO_FILT")
|
||
# Carrier SPI (on-board, XIAO to flash)
|
||
net("SPI_MOSI"); net("SPI_MISO"); net("SPI_SCK"); net("FLASH_CS")
|
||
# Carrier signal nets (XIAO GPIOs that connect to FPC)
|
||
for i in range(1, 9):
|
||
net(f"C_S{i}")
|
||
# Carrier FPC pins (carrier side of cable)
|
||
for i in range(1, 21):
|
||
net(f"FPC_C{i}")
|
||
|
||
# SOP-8 adapter nets
|
||
for i in range(1, 9):
|
||
net(f"A8_P{i}")
|
||
net("A8_VBAT"); net("A8_GND")
|
||
for i in range(1, 11):
|
||
net(f"FPC_A8_{i}")
|
||
|
||
# SOP-16N adapter nets
|
||
for i in range(1, 17):
|
||
net(f"A16N_P{i}")
|
||
net("A16N_VBAT"); net("A16N_GND")
|
||
for i in range(1, 21):
|
||
net(f"FPC_A16N_{i}")
|
||
|
||
# SOP-16W adapter nets
|
||
for i in range(1, 17):
|
||
net(f"A16W_P{i}")
|
||
net("A16W_VBAT"); net("A16W_GND")
|
||
for i in range(1, 21):
|
||
net(f"FPC_A16W_{i}")
|
||
|
||
|
||
# ============================================================
|
||
# XIAO DIMENSIONS
|
||
# ============================================================
|
||
|
||
XIAO_PCB_W = 17.5 # board width (the short dimension when USB is up)
|
||
XIAO_PCB_H = 21.0 # board height (USB to antenna end)
|
||
XIAO_ROW_SPAN = 15.0 # center-to-center between pin rows
|
||
XIAO_USB_W = 8.9
|
||
XIAO_USB_H = 3.2
|
||
|
||
CARRIER_W = 26.0 # wide enough for 20mm pad span + margin
|
||
CARRIER_H = 24.0 # tall enough for XIAO + components below
|
||
CARRIER_PAD_SPAN = 20.0 # extended pad span for castellated hand soldering
|
||
|
||
STEM_W = 14.0 # narrow stem for adapters
|
||
SOP_PITCH = 1.27
|
||
FPC_PITCH = 0.5
|
||
|
||
# ============================================================
|
||
# KiCad primitives (same as before, abbreviated)
|
||
# ============================================================
|
||
|
||
def gl(x1,y1,x2,y2,layer="Edge.Cuts",w=0.1):
|
||
return f' (gr_line (start {x1:.3f} {y1:.3f}) (end {x2:.3f} {y2:.3f}) (layer "{layer}") (width {w}) (uuid "{uid()}"))'
|
||
|
||
def gr(x1,y1,x2,y2,layer="F.SilkS",w=0.15):
|
||
return f' (gr_rect (start {x1:.3f} {y1:.3f}) (end {x2:.3f} {y2:.3f}) (layer "{layer}") (width {w}) (uuid "{uid()}"))'
|
||
|
||
def gt(s,x,y,sz=0.8,layer="F.SilkS"):
|
||
th=max(0.08,sz*0.13)
|
||
return f' (gr_text "{s}" (at {x:.3f} {y:.3f}) (layer "{layer}") (uuid "{uid()}") (effects (font (size {sz} {sz}) (thickness {th:.3f}))))'
|
||
|
||
def gc(x,y,r=0.3,layer="F.SilkS"):
|
||
return f' (gr_circle (center {x:.3f} {y:.3f}) (end {x+r:.3f} {y:.3f}) (layer "{layer}") (width 0.1) (fill solid) (uuid "{uid()}"))'
|
||
|
||
def fp(name,x,y,layer="F.Cu"):
|
||
return f"""
|
||
(footprint "bm:{name}" (layer "{layer}")
|
||
(at {x:.3f} {y:.3f})
|
||
(uuid "{uid()}")
|
||
(fp_text reference "{name}" (at 0 0) (layer "{layer[0]}.Fab") (uuid "{uid()}")
|
||
(effects (font (size 0.5 0.5) (thickness 0.08)) hide))
|
||
(fp_text value "" (at 0 0) (layer "{layer[0]}.Fab") (uuid "{uid()}")
|
||
(effects (font (size 0.5 0.5) (thickness 0.08)) hide))"""
|
||
|
||
def ps(name,x,y,w,h,nid,nn,layer="F.Cu"):
|
||
ls=f'"{layer}" "{layer[0]}.Paste" "{layer[0]}.Mask"'
|
||
return f' (pad "{name}" smd rect (at {x:.3f} {y:.3f}) (size {w:.3f} {h:.3f}) (layers {ls}) (net {nid} "{nn}") (uuid "{uid()}"))'
|
||
|
||
def pt(name,x,y,pd,dd,nid,nn):
|
||
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size {pd} {pd}) (drill {dd}) (layers "*.Cu" "*.Mask") (net {nid} "{nn}") (uuid "{uid()}"))'
|
||
|
||
def pc(name,x,y,nid,nn):
|
||
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size 1.0 1.0) (drill 0.6) (layers "*.Cu" "*.Mask") (net {nid} "{nn}") (uuid "{uid()}"))'
|
||
|
||
|
||
# ============================================================
|
||
# PCB GENERATOR
|
||
# ============================================================
|
||
|
||
def generate_pcb():
|
||
ox, oy = 100.0, 100.0
|
||
|
||
# Adapter heights in the stem
|
||
a8_h = 8.0 # SOP-8 adapter board height (= row span 3.9mm + FPC tab)
|
||
a16n_h = 10.0 # SOP-16 narrow
|
||
a16w_h = 14.0 # SOP-16 wide (10.3mm span + FPC tab)
|
||
stem_h = a8_h + a16n_h + a16w_h
|
||
|
||
total_h = CARRIER_H + stem_h
|
||
|
||
pcb = []
|
||
pcb.append(f"""(kicad_pcb (version 20221018) (generator "bm_v5")
|
||
(general (thickness 1.6))
|
||
(paper "A4")
|
||
(title_block
|
||
(title "Baby Mobile v5 — Key-Shaped Panel")
|
||
(date "2026-03-11") (rev "5.0")
|
||
(comment 1 "Carrier head + breakaway adapter stem")
|
||
(comment 2 "Unique nets per sub-board, FPC interconnect")
|
||
)
|
||
(layers
|
||
(0 "F.Cu" signal) (31 "B.Cu" signal)
|
||
(34 "B.Paste" user) (35 "F.Paste" user)
|
||
(36 "B.SilkS" user "B.Silkscreen") (37 "F.SilkS" user "F.Silkscreen")
|
||
(38 "B.Mask" user "B.Mask") (39 "F.Mask" user "F.Mask")
|
||
(44 "Edge.Cuts" user)
|
||
(48 "B.Fab" user "B.Fab") (49 "F.Fab" user "F.Fab")
|
||
)
|
||
(setup (pad_to_mask_clearance 0.05)
|
||
(pcbplotparams (layerselection 0x00010fc_ffffffff) (outputdirectory "gerbers/")))
|
||
""")
|
||
|
||
# ---- KEY-SHAPED OUTLINE ----
|
||
# Carrier head: ox to ox+CARRIER_W, oy to oy+CARRIER_H
|
||
# Stem: centered under carrier, STEM_W wide
|
||
stem_l = ox + (CARRIER_W - STEM_W) / 2
|
||
stem_r = stem_l + STEM_W
|
||
stem_top = oy + CARRIER_H
|
||
stem_bot = stem_top + stem_h
|
||
|
||
# Clockwise outline
|
||
pcb.append(gl(ox, oy, ox+CARRIER_W, oy)) # top
|
||
pcb.append(gl(ox+CARRIER_W, oy, ox+CARRIER_W, stem_top)) # right carrier
|
||
pcb.append(gl(ox+CARRIER_W, stem_top, stem_r, stem_top)) # step right
|
||
pcb.append(gl(stem_r, stem_top, stem_r, stem_bot)) # right stem
|
||
pcb.append(gl(stem_r, stem_bot, stem_l, stem_bot)) # bottom stem
|
||
pcb.append(gl(stem_l, stem_bot, stem_l, stem_top)) # left stem
|
||
pcb.append(gl(stem_l, stem_top, ox, stem_top)) # step left
|
||
pcb.append(gl(ox, stem_top, ox, oy)) # left carrier
|
||
|
||
# V-score lines between adapters (horizontal, across stem width)
|
||
vs1_y = stem_top + a8_h
|
||
vs2_y = vs1_y + a16n_h
|
||
pcb.append(gl(stem_l, vs1_y, stem_r, vs1_y))
|
||
pcb.append(gl(stem_l, vs2_y, stem_r, vs2_y))
|
||
pcb.append(gt("SNAP", (stem_l+stem_r)/2, vs1_y-0.3, 0.4))
|
||
pcb.append(gt("SNAP", (stem_l+stem_r)/2, vs2_y-0.3, 0.4))
|
||
|
||
# ---- XIAO (USB on top edge, centered) ----
|
||
xiao_cx = ox + CARRIER_W / 2
|
||
xiao_top = oy + 1.5 # small margin from carrier top
|
||
xiao_bot = xiao_top + XIAO_PCB_H
|
||
|
||
# XIAO board outline
|
||
pcb.append(gr(xiao_cx - XIAO_PCB_W/2, xiao_top,
|
||
xiao_cx + XIAO_PCB_W/2, xiao_bot, "F.SilkS", 0.2))
|
||
# USB-C on top
|
||
pcb.append(gr(xiao_cx - XIAO_USB_W/2, xiao_top - XIAO_USB_H,
|
||
xiao_cx + XIAO_USB_W/2, xiao_top, "F.SilkS", 0.2))
|
||
pcb.append(gt("USB-C", xiao_cx, xiao_top - XIAO_USB_H/2, 0.45))
|
||
pcb.append(gt("XIAO nRF52840", xiao_cx, (xiao_top+xiao_bot)/2, 0.6))
|
||
pcb.append(gt("RST", xiao_cx - XIAO_PCB_W/2 + 2, xiao_top + 1.5, 0.35))
|
||
|
||
# Pin rows: 7 pins each, 2.54mm pitch, running vertically
|
||
# Left row at xiao_cx - 7.5mm, right row at xiao_cx + 7.5mm
|
||
# Pin 1 (D0) at top of left row
|
||
# Extended pads: each pad extends from 15mm span to carrier edge (20mm or board edge)
|
||
|
||
left_x = xiao_cx - XIAO_ROW_SPAN / 2
|
||
right_x = xiao_cx + XIAO_ROW_SPAN / 2
|
||
pin1_y = xiao_top + 3.0 # first pin position (some margin from USB)
|
||
|
||
left_pins = [
|
||
("D0", "C_S1"), ("D1", "C_S2"), ("D2", "C_S3"), ("D3", "C_S4"),
|
||
("D4", "C_S5"), ("D5", "C_S6"), ("D6", "C_S7"),
|
||
]
|
||
right_pins = [
|
||
("D10", "C_S8"), # audio PWM
|
||
("D9", "SPI_SCK"),
|
||
("D8", "SPI_MOSI"),
|
||
("D7", "SPI_MISO"),
|
||
("3V3", "C_3V3"),
|
||
("GND", "GND"),
|
||
("5V", ""), # not connected
|
||
]
|
||
|
||
# Left pin row with extended pads
|
||
pcb.append(fp("XIAO_L", left_x, pin1_y))
|
||
for i, (label, nn) in enumerate(left_pins):
|
||
y = i * 2.54
|
||
nid = net(nn) if nn else 0
|
||
# Through-hole at XIAO position
|
||
pcb.append(pt(label, 0, y, 1.7, 1.0, nid, nn))
|
||
# Extended SMD pad toward board left edge for castellated soldering
|
||
ext_x = -(CARRIER_PAD_SPAN/2 - XIAO_ROW_SPAN/2) # offset toward left edge
|
||
pcb.append(ps(f"{label}_ext", ext_x/2, y, abs(ext_x), 1.2, nid, nn))
|
||
pcb.append(" )")
|
||
|
||
# Right pin row with extended pads
|
||
pcb.append(fp("XIAO_R", right_x, pin1_y))
|
||
for i, (label, nn) in enumerate(right_pins):
|
||
y = i * 2.54
|
||
nid = net(nn) if nn else 0
|
||
if nid > 0:
|
||
pcb.append(pt(label, 0, y, 1.7, 1.0, nid, nn))
|
||
ext_x = (CARRIER_PAD_SPAN/2 - XIAO_ROW_SPAN/2)
|
||
pcb.append(ps(f"{label}_ext", ext_x/2, y, abs(ext_x), 1.2, nid, nn))
|
||
else:
|
||
pcb.append(f' (pad "{label}" thru_hole circle (at 0 {y:.2f}) (size 1.7 1.7) (drill 1.0) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
|
||
pcb.append(" )")
|
||
|
||
# Pin labels
|
||
for i, (label, _) in enumerate(left_pins):
|
||
pcb.append(gt(label, left_x - 3.5, pin1_y + i*2.54, 0.4))
|
||
for i, (label, _) in enumerate(right_pins):
|
||
pcb.append(gt(label, right_x + 3.5, pin1_y + i*2.54, 0.4))
|
||
|
||
# ---- IS25LP128F (SOIC-8, below XIAO on carrier) ----
|
||
fl_cx = ox + 5
|
||
fl_cy = oy + CARRIER_H - 5
|
||
soic_span = 5.4
|
||
|
||
pcb.append(gr(fl_cx-2.5, fl_cy-soic_span/2-0.5, fl_cx+2.5, fl_cy+soic_span/2+0.5))
|
||
pcb.append(gc(fl_cx-1.5, fl_cy+soic_span/2-0.5, 0.25)) # pin 1
|
||
pcb.append(gt("U2 IS25LP128F", fl_cx, fl_cy-soic_span/2-1.5, 0.4))
|
||
pcb.append(gt("●1", fl_cx-2.2, fl_cy+soic_span/2+1, 0.3))
|
||
|
||
flash_pins = [
|
||
(1,-1.905, soic_span/2, "FLASH_CS"),
|
||
(2,-0.635, soic_span/2, "SPI_MISO"),
|
||
(3, 0.635, soic_span/2, "C_3V3"),
|
||
(4, 1.905, soic_span/2, "GND"),
|
||
(5, 1.905,-soic_span/2, "SPI_MOSI"),
|
||
(6, 0.635,-soic_span/2, "SPI_SCK"),
|
||
(7,-0.635,-soic_span/2, "C_3V3"),
|
||
(8,-1.905,-soic_span/2, "C_3V3"),
|
||
]
|
||
pcb.append(fp("U2", fl_cx, fl_cy))
|
||
for pin, x, y, nn in flash_pins:
|
||
pcb.append(ps(str(pin), x, y, 0.6, 1.5, net(nn), nn))
|
||
pcb.append(" )")
|
||
|
||
# C3 flash decoupling
|
||
pcb.append(fp("C3", fl_cx+5, fl_cy))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("C3 100n", fl_cx+5, fl_cy-1.3, 0.35))
|
||
|
||
# R2 CS pullup
|
||
pcb.append(fp("R2", fl_cx+5, fl_cy+3))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("FLASH_CS"),"FLASH_CS"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("R2 10K", fl_cx+5, fl_cy+1.7, 0.35))
|
||
|
||
# ---- TPS61220 + passives (bottom-right of carrier) ----
|
||
bx = ox + CARRIER_W - 6
|
||
by = oy + CARRIER_H - 5
|
||
|
||
pcb.append(gr(bx-1.2, by-1.1, bx+1.2, by+1.1))
|
||
pcb.append(gc(bx-0.8, by+0.6, 0.2))
|
||
pcb.append(gt("U1 TPS61220", bx, by-2.2, 0.4))
|
||
pcb.append(gt("●1", bx-1.5, by+0.7, 0.3))
|
||
|
||
pcb.append(fp("U1", bx, by))
|
||
pcb.append(ps("1",-0.65, 0.7, 0.4,0.55, net("C_VBAT"),"C_VBAT"))
|
||
pcb.append(ps("2", 0, 0.7, 0.4,0.55, net("GND"),"GND"))
|
||
pcb.append(ps("3", 0.65, 0.7, 0.4,0.55, net("C_VBAT"),"C_VBAT"))
|
||
pcb.append(ps("4", 0.65,-0.7, 0.4,0.55, net("C_3V3"),"C_3V3"))
|
||
pcb.append(ps("5",-0.65,-0.7, 0.4,0.55, net("BOOST_SW"),"BOOST_SW"))
|
||
pcb.append(" )")
|
||
|
||
pcb.append(fp("L1", bx-4, by))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("BOOST_SW"),"BOOST_SW"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("C_VBAT"),"C_VBAT"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("L1 4.7u", bx-4, by-1.3, 0.35))
|
||
|
||
pcb.append(fp("C1", bx-4, by+3))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_VBAT"),"C_VBAT"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("C1 10u", bx-4, by+1.7, 0.35))
|
||
|
||
pcb.append(fp("C2", bx+4, by))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_3V3"),"C_3V3"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("C2 10u", bx+4, by-1.3, 0.35))
|
||
|
||
# R1 + C5 audio filter
|
||
pcb.append(fp("R1", bx, by-6))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("C_S8"),"C_S8"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("AUDIO_FILT"),"AUDIO_FILT"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("R1 4.7K", bx, by-7.3, 0.35))
|
||
|
||
pcb.append(fp("C5", bx+4, by-6))
|
||
pcb.append(ps("1",-1.0,0, 1.0,1.2, net("AUDIO_FILT"),"AUDIO_FILT"))
|
||
pcb.append(ps("2", 1.0,0, 1.0,1.2, net("GND"),"GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gt("C5 10n", bx+4, by-7.3, 0.35))
|
||
|
||
# ---- Carrier FPC-20 (at bottom of carrier, connecting to stem) ----
|
||
fpc_cx = ox + CARRIER_W / 2
|
||
fpc_cy = oy + CARRIER_H - 1.5
|
||
n_fpc = 20
|
||
|
||
pcb.append(gr(fpc_cx - n_fpc*FPC_PITCH/2 - 1, fpc_cy-1.5,
|
||
fpc_cx + n_fpc*FPC_PITCH/2 + 1, fpc_cy+1.5))
|
||
pcb.append(gt("J1 FPC-20 0.5mm", fpc_cx, fpc_cy-2.5, 0.4))
|
||
pcb.append(gc(fpc_cx - (n_fpc-1)*FPC_PITCH/2 - 0.6, fpc_cy, 0.15))
|
||
|
||
# FPC pin mapping: carrier side uses FPC_C1..FPC_C20
|
||
# 1=C_VBAT 2=GND 3-10=C_S1..C_S8 11-18=spare 19-20=GND
|
||
fpc_c_map = [
|
||
("FPC_C1","C_VBAT"), ("FPC_C2","GND"),
|
||
] + [(f"FPC_C{i+3}", f"C_S{i+1}") for i in range(8)] + [
|
||
(f"FPC_C{i}", "") for i in range(11,19)
|
||
] + [("FPC_C19","GND"), ("FPC_C20","GND")]
|
||
|
||
pcb.append(fp("J1_FPC", fpc_cx, fpc_cy))
|
||
for j in range(n_fpc):
|
||
x = -((n_fpc-1)*FPC_PITCH/2) + j*FPC_PITCH
|
||
nn = fpc_c_map[j][0]
|
||
nid = net(nn)
|
||
pcb.append(ps(str(j+1), x, 0, 0.3, 1.2, nid, nn))
|
||
pcb.append(ps("SA", -(n_fpc*FPC_PITCH/2+1), 2, 1.5, 1.0, 0, ""))
|
||
pcb.append(ps("SB", (n_fpc*FPC_PITCH/2+1), 2, 1.5, 1.0, 0, ""))
|
||
pcb.append(" )")
|
||
|
||
# ---- Mounting holes ----
|
||
for mx,my in [(ox+2, oy+2), (ox+CARRIER_W-2, oy+2)]:
|
||
pcb.append(fp("MH",mx,my))
|
||
pcb.append(f' (pad "" thru_hole circle (at 0 0) (size 3.5 3.5) (drill 2.2) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
|
||
pcb.append(" )")
|
||
|
||
# ================================================================
|
||
# ADAPTER: SOP-8
|
||
# ================================================================
|
||
|
||
def make_adapter(prefix, center_x, top_y, height, pins_per_side, row_span, n_fpc_pins):
|
||
total_pins = pins_per_side * 2
|
||
row_len = (pins_per_side - 1) * SOP_PITCH
|
||
cy = top_y + height / 2
|
||
|
||
pcb.append(gt(prefix, center_x, cy, 0.5))
|
||
|
||
# Castellation pads on long (left/right) edges
|
||
# Bottom row: pins 1..N at cy + row_span/2
|
||
# Top row: pins 2N..N+1 at cy - row_span/2
|
||
pcb.append(fp(f"{prefix}_cast", center_x, cy, "B.Cu"))
|
||
for i in range(pins_per_side):
|
||
pin = i + 1
|
||
x = -row_len/2 + i * SOP_PITCH
|
||
nn = f"{prefix}_P{pin}"
|
||
pcb.append(pc(str(pin), x, row_span/2, net(nn), nn))
|
||
top_pin = total_pins - i
|
||
nn2 = f"{prefix}_P{top_pin}"
|
||
pcb.append(pc(str(top_pin), x, -row_span/2, net(nn2), nn2))
|
||
pcb.append(" )")
|
||
|
||
# Pin labels
|
||
for i in range(pins_per_side):
|
||
x = center_x - row_len/2 + i * SOP_PITCH
|
||
pcb.append(gt(str(i+1), x, cy + row_span/2 + 0.7, 0.3))
|
||
pcb.append(gt(str(total_pins-i), x, cy - row_span/2 - 0.7, 0.3))
|
||
pcb.append(gc(center_x - row_len/2 - 0.5, cy + row_span/2 + 0.5, 0.2))
|
||
|
||
# FPC connector centered
|
||
fpc_y = cy # centered vertically
|
||
pcb.append(fp(f"{prefix}_FPC", center_x, fpc_y))
|
||
for j in range(n_fpc_pins):
|
||
x = -((n_fpc_pins-1)*FPC_PITCH/2) + j*FPC_PITCH
|
||
if j < total_pins:
|
||
sop_pin = j + 1
|
||
nn = f"FPC_{prefix}_{j+1}"
|
||
elif j == n_fpc_pins - 2:
|
||
nn = f"{prefix}_GND"
|
||
elif j == n_fpc_pins - 1:
|
||
nn = f"{prefix}_VBAT"
|
||
else:
|
||
nn = f"FPC_{prefix}_{j+1}"
|
||
nid = net(nn)
|
||
pcb.append(ps(f"F{j+1}", x, 0, 0.3, 1.0, nid, nn))
|
||
pcb.append(ps("SA", -(n_fpc_pins*FPC_PITCH/2+1), 1.5, 1.2, 0.8, 0, ""))
|
||
pcb.append(ps("SB", (n_fpc_pins*FPC_PITCH/2+1), 1.5, 1.2, 0.8, 0, ""))
|
||
pcb.append(" )")
|
||
pcb.append(gt("FPC", center_x, fpc_y - 1.5, 0.3))
|
||
pcb.append(gc(center_x - (n_fpc_pins-1)*FPC_PITCH/2 - 0.5, fpc_y, 0.15))
|
||
|
||
stem_cx = (stem_l + stem_r) / 2
|
||
|
||
make_adapter("A8", stem_cx, stem_top, a8_h, 4, 3.9, 10)
|
||
make_adapter("A16N", stem_cx, vs1_y, a16n_h, 8, 6.0, 20)
|
||
make_adapter("A16W", stem_cx, vs2_y, a16w_h, 8, 10.3, 20)
|
||
|
||
# Back silk instructions
|
||
pcb.append(gt("Baby Mobile v5 — github.com/you/baby-mobile", ox+CARRIER_W/2, oy+CARRIER_H/2, 0.4, "B.SilkS"))
|
||
|
||
# ---- Insert net declarations at the top ----
|
||
# We need to do this after all nets are registered
|
||
net_block = net_decl()
|
||
pcb.insert(1, "") # placeholder, we'll join later
|
||
|
||
pcb.append(")")
|
||
|
||
# Splice net declarations in after the setup block
|
||
result = "\n".join(pcb)
|
||
result = result.replace("(setup (pad_to_mask_clearance 0.05)",
|
||
f"{net_block}\n\n (setup (pad_to_mask_clearance 0.05)")
|
||
return result
|
||
|
||
|
||
# ============================================================
|
||
# SCHEMATIC GENERATOR
|
||
# ============================================================
|
||
|
||
def generate_schematic():
|
||
"""Generate a matching .kicad_sch with all components and nets."""
|
||
|
||
sch = []
|
||
sch.append(f"""(kicad_sch (version 20230121) (generator "bm_v5_sch")
|
||
(uuid "{uid()}")
|
||
(paper "A3")
|
||
(title_block
|
||
(title "Baby Mobile v5 — Schematic")
|
||
(date "2026-03-11") (rev "5.0")
|
||
(comment 1 "XIAO nRF52840 + IS25LP128F + TPS61220")
|
||
(comment 2 "Key-shaped panel with breakaway SOP adapters")
|
||
)
|
||
""")
|
||
|
||
# Lib symbols (using the v2 fix: no library prefix on sub-symbols)
|
||
sch.append(" (lib_symbols")
|
||
|
||
# Generic 2-pin (for R, C, L)
|
||
for name, ref in [("R","R"), ("C","C"), ("L","L")]:
|
||
sch.append(f"""
|
||
(symbol "{LIB}:{name}" (in_bom yes) (on_board yes)
|
||
(property "Reference" "{ref}" (at 2 0 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "{name}" (at 2 -2 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(symbol "{name}_0_1"
|
||
(rectangle (start -1 2.5) (end 1 -2.5) (stroke (width 0.254) (type default)) (fill (type background)))
|
||
)
|
||
(symbol "{name}_1_1"
|
||
(pin passive line (at 0 5 270) (length 2.5) (name "1" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
|
||
(pin passive line (at 0 -5 90) (length 2.5) (name "2" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
|
||
)
|
||
)""")
|
||
|
||
# TPS61220
|
||
sch.append(f"""
|
||
(symbol "{LIB}:TPS61220" (in_bom yes) (on_board yes)
|
||
(property "Reference" "U" (at 0 6 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "TPS61220" (at 0 -6 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "Package_TO_SOT_SMD:SOT-353_SC-70-5" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(symbol "TPS61220_0_1"
|
||
(rectangle (start -7 5) (end 7 -5) (stroke (width 0.254) (type default)) (fill (type background)))
|
||
)
|
||
(symbol "TPS61220_1_1"
|
||
(pin power_in line (at -10 3 0) (length 3) (name "VIN" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
|
||
(pin power_in line (at -10 -3 0) (length 3) (name "GND" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
|
||
(pin input line (at 10 -3 180) (length 3) (name "EN" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
|
||
(pin power_out line (at 10 3 180) (length 3) (name "VOUT" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
|
||
(pin passive line (at 0 8 270) (length 3) (name "L" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
|
||
)
|
||
)""")
|
||
|
||
# IS25LP128F
|
||
sch.append(f"""
|
||
(symbol "{LIB}:IS25LP128F" (in_bom yes) (on_board yes)
|
||
(property "Reference" "U" (at 0 9 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "IS25LP128F" (at 0 7 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(symbol "IS25LP128F_0_1"
|
||
(rectangle (start -7 6) (end 7 -6) (stroke (width 0.254) (type default)) (fill (type background)))
|
||
)
|
||
(symbol "IS25LP128F_1_1"
|
||
(pin input line (at -10 4 0) (length 3) (name "~{{CS}}" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
|
||
(pin output line (at -10 1.5 0) (length 3) (name "SO" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
|
||
(pin input line (at -10 -1.5 0) (length 3) (name "~{{WP}}" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
|
||
(pin power_in line (at -10 -4 0) (length 3) (name "GND" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
|
||
(pin input line (at 10 -4 180) (length 3) (name "SI" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
|
||
(pin input line (at 10 -1.5 180) (length 3) (name "SCLK" (effects (font (size 1 1)))) (number "6" (effects (font (size 1 1)))))
|
||
(pin input line (at 10 1.5 180) (length 3) (name "~{{HOLD}}" (effects (font (size 1 1)))) (number "7" (effects (font (size 1 1)))))
|
||
(pin power_in line (at 10 4 180) (length 3) (name "VCC" (effects (font (size 1 1)))) (number "8" (effects (font (size 1 1)))))
|
||
)
|
||
)""")
|
||
|
||
# FPC connector (generic N-pin)
|
||
for npins in [10, 20]:
|
||
sch.append(f"""
|
||
(symbol "{LIB}:FPC_{npins}" (in_bom yes) (on_board yes)
|
||
(property "Reference" "J" (at 0 {npins+2} 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "FPC-{npins}" (at 0 {-npins-2} 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(symbol "FPC_{npins}_0_1"
|
||
(rectangle (start -3 {npins}) (end 3 {-npins}) (stroke (width 0.254) (type default)) (fill (type background)))
|
||
)
|
||
(symbol "FPC_{npins}_1_1"
|
||
""")
|
||
for i in range(npins):
|
||
y = npins - 1 - i * 2
|
||
sch.append(f' (pin passive line (at -6 {y} 0) (length 3) (name "P{i+1}" (effects (font (size 1 1)))) (number "{i+1}" (effects (font (size 1 1)))))')
|
||
sch.append(" )\n )")
|
||
|
||
# XIAO module symbol
|
||
sch.append(f"""
|
||
(symbol "{LIB}:XIAO_nRF52840" (in_bom yes) (on_board yes)
|
||
(property "Reference" "U" (at 0 22 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "XIAO_nRF52840" (at 0 20 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) hide))
|
||
(symbol "XIAO_nRF52840_0_1"
|
||
(rectangle (start -12 19) (end 12 -19) (stroke (width 0.254) (type default)) (fill (type background)))
|
||
)
|
||
(symbol "XIAO_nRF52840_1_1"
|
||
(pin bidirectional line (at -15 15 0) (length 3) (name "D0" (effects (font (size 1 1)))) (number "1" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 12.5 0) (length 3) (name "D1" (effects (font (size 1 1)))) (number "2" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 10 0) (length 3) (name "D2" (effects (font (size 1 1)))) (number "3" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 7.5 0) (length 3) (name "D3" (effects (font (size 1 1)))) (number "4" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 5 0) (length 3) (name "D4" (effects (font (size 1 1)))) (number "5" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 2.5 0) (length 3) (name "D5" (effects (font (size 1 1)))) (number "6" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at -15 0 0) (length 3) (name "D6" (effects (font (size 1 1)))) (number "7" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at 15 15 180) (length 3) (name "D10" (effects (font (size 1 1)))) (number "8" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at 15 12.5 180) (length 3) (name "D9/SCK" (effects (font (size 1 1)))) (number "9" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at 15 10 180) (length 3) (name "D8/MOSI" (effects (font (size 1 1)))) (number "10" (effects (font (size 1 1)))))
|
||
(pin bidirectional line (at 15 7.5 180) (length 3) (name "D7/MISO" (effects (font (size 1 1)))) (number "11" (effects (font (size 1 1)))))
|
||
(pin power_in line (at 15 2.5 180) (length 3) (name "3V3" (effects (font (size 1 1)))) (number "12" (effects (font (size 1 1)))))
|
||
(pin power_in line (at 15 0 180) (length 3) (name "GND" (effects (font (size 1 1)))) (number "13" (effects (font (size 1 1)))))
|
||
(pin power_in line (at 15 -2.5 180) (length 3) (name "5V" (effects (font (size 1 1)))) (number "14" (effects (font (size 1 1)))))
|
||
)
|
||
)""")
|
||
|
||
sch.append(" )") # end lib_symbols
|
||
|
||
# Component instances (placed with net labels for connectivity)
|
||
def inst(lib_id, ref, value, footprint, x, y, angle=0):
|
||
return f""" (symbol (lib_id "{LIB}:{lib_id}") (at {x} {y} {angle}) (unit 1)
|
||
(in_bom yes) (on_board yes) (dnp no)
|
||
(uuid "{uid()}")
|
||
(property "Reference" "{ref}" (at {x+3} {y-3} 0) (effects (font (size 1.27 1.27))))
|
||
(property "Value" "{value}" (at {x+3} {y+3} 0) (effects (font (size 1.27 1.27))))
|
||
(property "Footprint" "{footprint}" (at {x} {y} 0) (effects (font (size 1.27 1.27)) hide))
|
||
(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) hide))
|
||
)"""
|
||
|
||
def label(name, x, y):
|
||
return f' (label "{name}" (at {x} {y} 0) (effects (font (size 1.27 1.27))) (uuid "{uid()}"))'
|
||
|
||
# Place components
|
||
sch.append(inst("XIAO_nRF52840", "U3", "XIAO_nRF52840", "", 80, 80))
|
||
sch.append(inst("IS25LP128F", "U2", "IS25LP128F", "Package_SO:SOIC-8_3.9x4.9mm_P1.27mm", 180, 60))
|
||
sch.append(inst("TPS61220", "U1", "TPS61220", "Package_TO_SOT_SMD:SOT-353_SC-70-5", 180, 130))
|
||
sch.append(inst("FPC_20", "J1", "FPC-20 Carrier", "", 80, 160))
|
||
sch.append(inst("R", "R1", "4.7K", "Resistor_SMD:R_0805_2012Metric", 140, 80))
|
||
sch.append(inst("C", "C5", "10nF", "Capacitor_SMD:C_0805_2012Metric", 150, 90))
|
||
sch.append(inst("R", "R2", "10K", "Resistor_SMD:R_0805_2012Metric", 210, 50))
|
||
sch.append(inst("C", "C3", "100nF", "Capacitor_SMD:C_0805_2012Metric", 210, 70))
|
||
sch.append(inst("L", "L1", "4.7uH", "Inductor_SMD:L_0805_2012Metric", 180, 115))
|
||
sch.append(inst("C", "C1", "10uF", "Capacitor_SMD:C_0805_2012Metric", 160, 140))
|
||
sch.append(inst("C", "C2", "10uF", "Capacitor_SMD:C_0805_2012Metric", 200, 140))
|
||
|
||
# Adapter schematics (separate sections for clarity)
|
||
sch.append(inst("FPC_10", "J2", "FPC-10 SOP-8", "", 280, 80))
|
||
sch.append(inst("FPC_20", "J3", "FPC-20 SOP-16N", "", 280, 140))
|
||
sch.append(inst("FPC_20", "J4", "FPC-20 SOP-16W", "", 280, 200))
|
||
|
||
# Net labels (connecting everything logically)
|
||
# XIAO to SPI flash
|
||
labels = [
|
||
("SPI_SCK", 95, 67.5), ("SPI_SCK", 190, 58.5),
|
||
("SPI_MOSI", 95, 70), ("SPI_MOSI", 190, 56),
|
||
("SPI_MISO", 95, 72.5), ("SPI_MISO", 170, 61.5),
|
||
("FLASH_CS", 170, 64), ("FLASH_CS", 210, 45),
|
||
("C_3V3", 95, 77.5), ("C_3V3", 190, 64), ("C_3V3", 190, 133),
|
||
("GND", 95, 80), ("GND", 170, 56), ("GND", 170, 127),
|
||
# XIAO signals to carrier FPC
|
||
("C_S1", 65, 65), ("C_S2", 65, 67.5), ("C_S3", 65, 70),
|
||
("C_S4", 65, 72.5), ("C_S5", 65, 75), ("C_S6", 65, 77.5),
|
||
("C_S7", 65, 80), ("C_S8", 95, 65),
|
||
("C_S8", 130, 80), # to R1
|
||
("AUDIO_FILT", 150, 80),
|
||
# Boost
|
||
("C_VBAT", 170, 133), ("C_VBAT", 180, 110),
|
||
("BOOST_SW", 180, 122), ("BOOST_SW", 180, 120),
|
||
]
|
||
for name, x, y in labels:
|
||
sch.append(label(name, x, y))
|
||
|
||
# Text notes
|
||
notes = [
|
||
(40, 30, "CARRIER: XIAO nRF52840 + IS25LP128F (16MB) + TPS61220 (3.3V boost)"),
|
||
(40, 35, "FPC-20 cable connects carrier to one of the SOP adapter boards"),
|
||
(250, 30, "ADAPTERS: Snap-off castellated SOP footprints"),
|
||
(250, 35, "Each has its own FPC connector + unique net names"),
|
||
]
|
||
for x, y, text in notes:
|
||
sch.append(f' (text "{text}" (at {x} {y} 0) (effects (font (size 1.5 1.5)) (justify left)) (uuid "{uid()}"))')
|
||
|
||
sch.append(")")
|
||
return "\n".join(sch)
|
||
|
||
|
||
# ============================================================
|
||
# MAIN
|
||
# ============================================================
|
||
|
||
if __name__ == "__main__":
|
||
outdir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
# Generate PCB
|
||
pcb_text = generate_pcb()
|
||
pcb_path = os.path.join(outdir, "baby-mobile-v5.kicad_pcb")
|
||
with open(pcb_path, "w") as f:
|
||
f.write(pcb_text)
|
||
print(f"PCB: {pcb_path} ({len(pcb_text)} bytes)")
|
||
|
||
# Generate Schematic
|
||
sch_text = generate_schematic()
|
||
sch_path = os.path.join(outdir, "baby-mobile-v5.kicad_sch")
|
||
with open(sch_path, "w") as f:
|
||
f.write(sch_text)
|
||
print(f"SCH: {sch_path} ({len(sch_text)} bytes)")
|
||
|
||
# Generate Project
|
||
proj = json.dumps({
|
||
"meta": {"filename": "baby-mobile-v5.kicad_pro", "version": 1},
|
||
"board": {"design_settings": {"rules": {"min_track_width": 0.15, "min_via_diameter": 0.4}}},
|
||
"net_settings": {"classes": [
|
||
{"name": "Default", "clearance": 0.15, "track_width": 0.25},
|
||
{"name": "Power", "clearance": 0.2, "track_width": 0.5},
|
||
]},
|
||
}, indent=2)
|
||
with open(os.path.join(outdir, "baby-mobile-v5.kicad_pro"), "w") as f:
|
||
f.write(proj)
|
||
|
||
print(f"\n{len(NETS)} unique nets registered:")
|
||
print(f" Carrier: C_S1-8, C_VBAT, C_3V3, SPI_*, FLASH_CS, BOOST_SW, AUDIO_FILT")
|
||
print(f" SOP-8: A8_P1-8, A8_VBAT, A8_GND, FPC_A8_1-10")
|
||
print(f" SOP-16N: A16N_P1-16, A16N_VBAT, A16N_GND, FPC_A16N_1-20")
|
||
print(f" SOP-16W: A16W_P1-16, A16W_VBAT, A16W_GND, FPC_A16W_1-20")
|
||
print(f" No shared nets between carrier and adapters (FPC bridges them)")
|
||
print(f"\nKey-shaped board:")
|
||
print(f" Head (carrier): {CARRIER_W}×{CARRIER_H}mm")
|
||
print(f" Stem (adapters): {STEM_W}mm wide, stacked SOP-8 + SOP-16N + SOP-16W")
|
||
print(f" XIAO: USB-C on top, rows {XIAO_ROW_SPAN}mm apart, pads extended to {CARRIER_PAD_SPAN}mm")
|