544 lines
23 KiB
Python
544 lines
23 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Baby Mobile — Combined Panel v4
|
||
|
||
PANEL LAYOUT:
|
||
┌───────────────────────────────────────────────┐
|
||
│ CARRIER BOARD (44×26mm) │
|
||
│ ┌───────USB-C──┐ │
|
||
│ │ █████████████ │ │
|
||
│ │ │ [U2 IS25LP128F] [C3][R2] │
|
||
│ │ XIAO BLE │ ●1 │
|
||
│ │ nRF52840 │ [R1][C5] │
|
||
│ │ │ │
|
||
│ │ D0 D10│ [U1 TPS61220] │
|
||
│ │ D1 D9│ ●1 [L1][C1][C2] │
|
||
│ │ D2 D8│ │
|
||
│ │ D3 D7│ ┌────────────────┐ │
|
||
│ │ D4 3V3│ │ J1 FPC-20 0.5mm│ │
|
||
│ │ D5 GND│ └────────────────┘ │
|
||
│ │ D6 5V│ │
|
||
│ └───────────────┘ (M2) (M2) │
|
||
├ ─ ─ ─ ─ ─ SNAP ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
|
||
│ ┌──────┐ ┃ ┌──────────┐ ┃ ┌──────────┐ │
|
||
│ │SOP-8 │ ┃ │SOP-16 N │ ┃ │SOP-16 W │ │
|
||
│ C│ │C┃C│ │C┃C│ │C │
|
||
│ A│[FPC] │A┃A│ [FPC] │A┃A│ [FPC] │A │
|
||
│ S│ │S┃S│ │S┃S│ │S │
|
||
│ T│ │T┃T│ │T┃T│ │T │
|
||
│ └──────┘ ┃ └──────────┘ ┃ └──────────┘ │
|
||
└───────────┸──────────────┸─────────────────────┘
|
||
C=castellated edges ┃=v-score snap lines
|
||
|
||
Each SOP adapter:
|
||
- Long edges (top/bottom): castellated half-holes = SOP pads
|
||
- Short edges: v-score snap lines
|
||
- Center top: FPC 0.5mm connector (20-pin for SOP-16, 10-pin for SOP-8)
|
||
- All SOP pins netted to corresponding FPC pins
|
||
"""
|
||
|
||
import uuid, os, json
|
||
|
||
def uid():
|
||
return str(uuid.uuid4())
|
||
|
||
# ============================================================
|
||
# KiCad drawing primitives
|
||
# ============================================================
|
||
|
||
def gr_line(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_rect(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 gr_text(s, x, y, sz=0.8, layer="F.SilkS", mirror=False):
|
||
th = max(0.08, sz * 0.13)
|
||
mir = " (justify mirror)" if mirror else ""
|
||
return f' (gr_text "{s}" (at {x:.3f} {y:.3f}) (layer "{layer}") (uuid "{uid()}") (effects (font (size {sz} {sz}) (thickness {th:.3f})){mir}))'
|
||
|
||
def gr_circle(x, y, r=0.3, layer="F.SilkS", fill="solid"):
|
||
return f' (gr_circle (center {x:.3f} {y:.3f}) (end {x+r:.3f} {y:.3f}) (layer "{layer}") (width 0.1) (fill {fill}) (uuid "{uid()}"))'
|
||
|
||
def fp_hdr(name, x, y, layer="F.Cu"):
|
||
return f"""
|
||
(footprint "panel:{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 pad_smd(name, x, y, w, h, net, nname, layer="F.Cu"):
|
||
layers = 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 {layers}) (net {net} "{nname}") (uuid "{uid()}"))'
|
||
|
||
def pad_thru(name, x, y, pd, dd, net, nname):
|
||
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size {pd} {pd}) (drill {dd}) (layers "*.Cu" "*.Mask") (net {net} "{nname}") (uuid "{uid()}"))'
|
||
|
||
def pad_cast(name, x, y, net, nname):
|
||
"""Castellated half-hole on board edge."""
|
||
return f' (pad "{name}" thru_hole circle (at {x:.3f} {y:.3f}) (size 1.0 1.0) (drill 0.6) (layers "*.Cu" "*.Mask") (net {net} "{nname}") (uuid "{uid()}"))'
|
||
|
||
|
||
# ============================================================
|
||
# XIAO nRF52840 Dimensions (from Seeed docs)
|
||
# ============================================================
|
||
# PCB: 21.0mm x 17.5mm
|
||
# USB-C: centered on one 17.5mm short edge, ~9mm wide, protrudes ~2.5mm
|
||
# Pin headers: 2x7, 2.54mm pitch, rows 17.78mm apart (center-center)
|
||
# Pin 1 (D0) at top-left when USB faces left
|
||
# Reset button on USB side, LED on opposite side
|
||
|
||
XIAO_W = 21.0
|
||
XIAO_H = 17.5
|
||
XIAO_ROW_SPAN = 17.78 # center to center of pin rows
|
||
XIAO_USB_W = 8.9
|
||
XIAO_USB_H = 3.2
|
||
XIAO_USB_PROTRUDE = 1.5 # how far USB sticks past PCB edge
|
||
|
||
|
||
# ============================================================
|
||
# Main generator
|
||
# ============================================================
|
||
|
||
def generate():
|
||
# Panel dimensions
|
||
CARRIER_W = 44.0
|
||
CARRIER_H = 26.0
|
||
ADAPTER_STRIP_H = 14.0
|
||
PANEL_W = 48.0
|
||
PANEL_H = CARRIER_H + ADAPTER_STRIP_H + 1.0 # 1mm v-score gap
|
||
|
||
ox, oy = 100.0, 100.0 # KiCad origin
|
||
|
||
pcb = []
|
||
|
||
# ---- File header ----
|
||
pcb.append(f"""(kicad_pcb (version 20221018) (generator "panel_v4")
|
||
(general (thickness 1.6))
|
||
(paper "A4")
|
||
(title_block
|
||
(title "Baby Mobile Combined Panel v4")
|
||
(date "2026-03-11")
|
||
(rev "4.0")
|
||
(comment 1 "Carrier: XIAO nRF52840 + IS25LP128F + TPS61220 + FPC-20")
|
||
(comment 2 "Adapters: SOP-8, SOP-16N, SOP-16W — castellated, FPC connected")
|
||
(comment 3 "Order: JLCPCB, 2-layer, castellated holes=yes, v-score=yes")
|
||
)
|
||
(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/"))
|
||
)
|
||
|
||
(net 0 "")
|
||
(net 1 "GND") (net 2 "VBAT") (net 3 "3V3")
|
||
(net 4 "SPI_MOSI") (net 5 "SPI_MISO") (net 6 "SPI_SCK") (net 7 "FLASH_CS")
|
||
(net 8 "BOOST_SW") (net 9 "AUDIO_FILT")
|
||
""")
|
||
# 16 signal nets for SOP pins / FPC
|
||
for i in range(1, 17):
|
||
pcb.append(f' (net {i+9} "S{i}")')
|
||
# Total nets: 0-25
|
||
|
||
# ================================================================
|
||
# PANEL OUTLINE
|
||
# ================================================================
|
||
|
||
# Outer rectangle
|
||
pcb.append(gr_line(ox, oy, ox+PANEL_W, oy))
|
||
pcb.append(gr_line(ox+PANEL_W, oy, ox+PANEL_W, oy+PANEL_H))
|
||
pcb.append(gr_line(ox+PANEL_W, oy+PANEL_H, ox, oy+PANEL_H))
|
||
pcb.append(gr_line(ox, oy+PANEL_H, ox, oy))
|
||
|
||
# V-score: carrier / adapter strip
|
||
vs_y = oy + CARRIER_H + 0.5
|
||
pcb.append(gr_line(ox, vs_y, ox+PANEL_W, vs_y))
|
||
pcb.append(gr_text("▼ SNAP ▼", ox + PANEL_W/2, vs_y - 0.3, 0.5))
|
||
|
||
# V-score: between adapters
|
||
adapt_base = vs_y + 0.5
|
||
# SOP-8: 10mm wide, SOP-16N: 16mm wide, SOP-16W: 16mm wide
|
||
a8_w = 12.0
|
||
a16n_w = 16.0
|
||
a16w_w = 16.0
|
||
gap = 0.0 # v-score is zero-width
|
||
|
||
vs_x1 = ox + a8_w + 1.0
|
||
vs_x2 = vs_x1 + a16n_w
|
||
pcb.append(gr_line(vs_x1, adapt_base, vs_x1, oy + PANEL_H))
|
||
pcb.append(gr_line(vs_x2, adapt_base, vs_x2, oy + PANEL_H))
|
||
|
||
# ================================================================
|
||
# CARRIER: XIAO SOCKET with accurate outline
|
||
# ================================================================
|
||
|
||
# Place XIAO with USB-C facing left edge of carrier
|
||
xiao_cx = ox + 3 + XIAO_W/2 # center of XIAO
|
||
xiao_cy = oy + CARRIER_H/2
|
||
|
||
# Board outline rectangle
|
||
xb_l = xiao_cx - XIAO_W/2 # left edge of XIAO PCB
|
||
xb_r = xiao_cx + XIAO_W/2 # right edge
|
||
xb_t = xiao_cy - XIAO_H/2 # top
|
||
xb_b = xiao_cy + XIAO_H/2 # bottom
|
||
pcb.append(gr_rect(xb_l, xb_t, xb_r, xb_b, "F.SilkS", 0.2))
|
||
|
||
# USB-C rectangle protruding from left edge
|
||
usb_l = xb_l - XIAO_USB_PROTRUDE
|
||
usb_t = xiao_cy - XIAO_USB_W/2
|
||
usb_b = xiao_cy + XIAO_USB_W/2
|
||
pcb.append(gr_rect(usb_l, usb_t, xb_l, usb_b, "F.SilkS", 0.2))
|
||
# Fill USB area
|
||
pcb.append(gr_rect(usb_l + 0.3, usb_t + 0.3, xb_l - 0.1, usb_b - 0.3, "F.SilkS", 0.15))
|
||
pcb.append(gr_text("USB-C", usb_l + 2, xiao_cy, 0.5))
|
||
pcb.append(gr_text("XIAO nRF52840", xiao_cx + 1, xiao_cy, 0.8))
|
||
|
||
# Reset button indicator (top-left corner of XIAO near USB)
|
||
pcb.append(gr_text("RST", xb_l + 2, xb_t + 1.5, 0.4))
|
||
pcb.append(gr_circle(xb_l + 1, xb_t + 1.5, 0.5, "F.SilkS", "none"))
|
||
|
||
# Pin labels
|
||
left_labels = ["D0","D1","D2","D3","D4","D5","D6"]
|
||
right_labels = ["D10","D9","D8","D7","3V3","GND","5V"]
|
||
|
||
# Left row nets: SIG_1..7 → S1..S7 (from toy via FPC)
|
||
left_nets = [(10,"S1"),(11,"S2"),(12,"S3"),(13,"S4"),(14,"S5"),(15,"S6"),(16,"S7")]
|
||
# Right row: D10→S8(audio), D9→SCK, D8→MOSI, D7→MISO, 3V3, GND, 5V(NC)
|
||
right_nets = [(17,"S8"),(6,"SPI_SCK"),(4,"SPI_MOSI"),(5,"SPI_MISO"),(3,"3V3"),(1,"GND"),(0,"")]
|
||
|
||
# Left header
|
||
pcb.append(fp_hdr("J2_XIAO_L", xiao_cx - XIAO_ROW_SPAN/2, xiao_cy))
|
||
for i in range(7):
|
||
y = -7.62 + i * 2.54
|
||
net_id, net_name = left_nets[i]
|
||
pcb.append(pad_thru(left_labels[i], 0, y, 1.7, 1.0, net_id, net_name))
|
||
pcb.append(" )")
|
||
for i, lbl in enumerate(left_labels):
|
||
pcb.append(gr_text(lbl, xiao_cx - XIAO_ROW_SPAN/2 - 2.5, xiao_cy - 7.62 + i*2.54, 0.45))
|
||
|
||
# Right header
|
||
pcb.append(fp_hdr("J3_XIAO_R", xiao_cx + XIAO_ROW_SPAN/2, xiao_cy))
|
||
for i in range(7):
|
||
y = -7.62 + i * 2.54
|
||
net_id, net_name = right_nets[i]
|
||
if net_id > 0:
|
||
pcb.append(pad_thru(right_labels[i], 0, y, 1.7, 1.0, net_id, net_name))
|
||
else:
|
||
pcb.append(f' (pad "{right_labels[i]}" thru_hole circle (at 0 {y:.2f}) (size 1.7 1.7) (drill 1.0) (layers "*.Cu" "*.Mask") (uuid "{uid()}"))')
|
||
pcb.append(" )")
|
||
for i, lbl in enumerate(right_labels):
|
||
pcb.append(gr_text(lbl, xiao_cx + XIAO_ROW_SPAN/2 + 2.8, xiao_cy - 7.62 + i*2.54, 0.45))
|
||
|
||
# ================================================================
|
||
# CARRIER: IS25LP128F (SOIC-8)
|
||
# ================================================================
|
||
|
||
fl_cx = ox + 32
|
||
fl_cy = oy + 5.5
|
||
soic_span = 5.4
|
||
|
||
# SOIC-8 body outline on silk
|
||
pcb.append(gr_rect(fl_cx - 2.5, fl_cy - soic_span/2 - 0.5,
|
||
fl_cx + 2.5, fl_cy + soic_span/2 + 0.5))
|
||
# Pin 1 dot
|
||
pcb.append(gr_circle(fl_cx - 1.5, fl_cy + soic_span/2 - 0.5, 0.25))
|
||
# Notch on pin 1 end
|
||
pcb.append(gr_circle(fl_cx, fl_cy + soic_span/2 + 0.5, 0.4, "F.SilkS", "none"))
|
||
pcb.append(gr_text("U2", fl_cx, fl_cy - soic_span/2 - 1.5, 0.5))
|
||
pcb.append(gr_text("IS25LP128F", fl_cx, fl_cy + soic_span/2 + 2, 0.4))
|
||
# Pin numbers on silk
|
||
for i, num in enumerate([1,2,3,4]):
|
||
pcb.append(gr_text(str(num), fl_cx - 1.905 + i*1.27, fl_cy + soic_span/2 + 1.2, 0.3))
|
||
for i, num in enumerate([8,7,6,5]):
|
||
pcb.append(gr_text(str(num), fl_cx - 1.905 + i*1.27, fl_cy - soic_span/2 - 1.2, 0.3))
|
||
|
||
flash_pins = [
|
||
(1, -1.905, soic_span/2, 7, "FLASH_CS"),
|
||
(2, -0.635, soic_span/2, 5, "SPI_MISO"),
|
||
(3, 0.635, soic_span/2, 3, "3V3"), # ~WP tie high
|
||
(4, 1.905, soic_span/2, 1, "GND"),
|
||
(5, 1.905, -soic_span/2, 4, "SPI_MOSI"),
|
||
(6, 0.635, -soic_span/2, 6, "SPI_SCK"),
|
||
(7, -0.635, -soic_span/2, 3, "3V3"), # ~HOLD tie high
|
||
(8, -1.905, -soic_span/2, 3, "3V3"), # VCC
|
||
]
|
||
pcb.append(fp_hdr("U2_Flash", fl_cx, fl_cy))
|
||
for pin, x, y, net, nn in flash_pins:
|
||
pcb.append(pad_smd(str(pin), x, y, 0.6, 1.5, net, nn))
|
||
pcb.append(" )")
|
||
|
||
# Flash decoupling C3
|
||
pcb.append(fp_hdr("C3", fl_cx + 5.5, fl_cy - 1))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 3, "3V3"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("C3", fl_cx + 5.5, fl_cy - 2.3, 0.4))
|
||
pcb.append(gr_text("100n", fl_cx + 5.5, fl_cy + 0.3, 0.35))
|
||
|
||
# Flash CS pullup R2
|
||
pcb.append(fp_hdr("R2", fl_cx + 5.5, fl_cy + 2))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 7, "FLASH_CS"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 3, "3V3"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("R2", fl_cx + 5.5, fl_cy + 0.8, 0.4))
|
||
pcb.append(gr_text("10K", fl_cx + 5.5, fl_cy + 3.3, 0.35))
|
||
|
||
# ================================================================
|
||
# CARRIER: Audio RC filter (R1 + C5)
|
||
# ================================================================
|
||
|
||
rc_x = ox + 32
|
||
rc_y = oy + 14
|
||
|
||
pcb.append(fp_hdr("R1", rc_x, rc_y))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 17, "S8")) # from XIAO D10
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 9, "AUDIO_FILT"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("R1 4.7K", rc_x, rc_y - 1.3, 0.4))
|
||
|
||
pcb.append(fp_hdr("C5", rc_x, rc_y + 3))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 9, "AUDIO_FILT"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("C5 10n", rc_x, rc_y + 1.7, 0.4))
|
||
|
||
# ================================================================
|
||
# CARRIER: TPS61220 Boost Converter
|
||
# ================================================================
|
||
|
||
bx = ox + 32
|
||
by = oy + 21
|
||
|
||
# SC-70-5 outline
|
||
pcb.append(gr_rect(bx - 1.2, by - 1.1, bx + 1.2, by + 1.1))
|
||
pcb.append(gr_circle(bx - 0.8, by + 0.6, 0.2)) # pin 1 dot
|
||
pcb.append(gr_text("U1", bx, by - 2, 0.5))
|
||
pcb.append(gr_text("TPS61220", bx, by + 2, 0.4))
|
||
pcb.append(gr_text("●1", bx - 1.5, by + 0.7, 0.35))
|
||
|
||
pcb.append(fp_hdr("U1_Boost", bx, by))
|
||
pcb.append(pad_smd("1", -0.65, 0.7, 0.4, 0.55, 2, "VBAT")) # VIN
|
||
pcb.append(pad_smd("2", 0, 0.7, 0.4, 0.55, 1, "GND")) # GND
|
||
pcb.append(pad_smd("3", 0.65, 0.7, 0.4, 0.55, 2, "VBAT")) # EN=VIN
|
||
pcb.append(pad_smd("4", 0.65, -0.7, 0.4, 0.55, 3, "3V3")) # VOUT
|
||
pcb.append(pad_smd("5", -0.65, -0.7, 0.4, 0.55, 8, "BOOST_SW")) # L
|
||
pcb.append(" )")
|
||
|
||
# L1 (0805)
|
||
pcb.append(fp_hdr("L1", bx - 4, by))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 8, "BOOST_SW"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 2, "VBAT"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("L1 4.7u", bx - 4, by - 1.3, 0.4))
|
||
|
||
# C1 input (0805)
|
||
pcb.append(fp_hdr("C1", bx - 4, by + 3))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 2, "VBAT"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("C1 10u", bx - 4, by + 1.7, 0.4))
|
||
|
||
# C2 output (0805)
|
||
pcb.append(fp_hdr("C2", bx + 4, by))
|
||
pcb.append(pad_smd("1", -1.0, 0, 1.0, 1.2, 3, "3V3"))
|
||
pcb.append(pad_smd("2", 1.0, 0, 1.0, 1.2, 1, "GND"))
|
||
pcb.append(" )")
|
||
pcb.append(gr_text("C2 10u", bx + 4, by - 1.3, 0.4))
|
||
|
||
# ================================================================
|
||
# CARRIER: FPC-20 Connector (0.5mm pitch, bottom-contact)
|
||
# ================================================================
|
||
|
||
fpc_cx = ox + 38
|
||
fpc_cy = oy + 14
|
||
fpc_pitch = 0.5
|
||
n_fpc = 20
|
||
|
||
# FPC connector outline
|
||
fpc_w = (n_fpc - 1) * fpc_pitch + 2.0
|
||
pcb.append(gr_rect(fpc_cx - fpc_w/2, fpc_cy - 2, fpc_cx + fpc_w/2, fpc_cy + 2))
|
||
pcb.append(gr_text("J1 FPC-20", fpc_cx, fpc_cy - 3, 0.5))
|
||
pcb.append(gr_text("0.5mm TO TOY", fpc_cx, fpc_cy + 3, 0.35))
|
||
pcb.append(gr_circle(fpc_cx - (n_fpc-1)*fpc_pitch/2 - 0.8, fpc_cy, 0.2)) # pin 1
|
||
|
||
# FPC pin assignment:
|
||
# 1=VBAT 2=GND 3=S1 4=S2 ... 10=S8 11-16=S9-S14(spare) 17-20=spare/GND
|
||
fpc_nets = [
|
||
(1, 2, "VBAT"), (2, 1, "GND"),
|
||
(3, 10, "S1"), (4, 11, "S2"), (5, 12, "S3"), (6, 13, "S4"),
|
||
(7, 14, "S5"), (8, 15, "S6"), (9, 16, "S7"), (10, 17, "S8"),
|
||
(11, 18, "S9"), (12, 19, "S10"), (13, 20, "S11"), (14, 21, "S12"),
|
||
(15, 22, "S13"), (16, 23, "S14"), (17, 24, "S15"), (18, 25, "S16"),
|
||
(19, 1, "GND"), (20, 1, "GND"),
|
||
]
|
||
|
||
pcb.append(fp_hdr("J1_FPC20", fpc_cx, fpc_cy))
|
||
for pin, net, nn in fpc_nets:
|
||
x = -((n_fpc - 1) * fpc_pitch / 2) + (pin - 1) * fpc_pitch
|
||
pcb.append(pad_smd(str(pin), x, 0, 0.3, 1.2, net, nn))
|
||
# Anchor/lock pads
|
||
pcb.append(pad_smd("S1", -(fpc_w/2 - 0.5), 2.5, 1.5, 1.0, 0, ""))
|
||
pcb.append(pad_smd("S2", (fpc_w/2 - 0.5), 2.5, 1.5, 1.0, 0, ""))
|
||
pcb.append(" )")
|
||
|
||
# ================================================================
|
||
# CARRIER: Mounting holes
|
||
# ================================================================
|
||
|
||
for mx, my in [(ox + 2, oy + CARRIER_H - 2), (ox + CARRIER_W - 3, oy + CARRIER_H - 2)]:
|
||
pcb.append(fp_hdr("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(" )")
|
||
|
||
# Carrier back silk
|
||
pcb.append(gr_text("XIAO: D0-D6→SIG1-7 D7=MISO D8=MOSI D9=SCK D10=AUDIO(S8)", ox + CARRIER_W/2, oy + CARRIER_H/2, 0.4, "B.SilkS"))
|
||
pcb.append(gr_text("FPC: 1=VBAT 2=GND 3-10=S1-S8 19-20=GND", ox + CARRIER_W/2, oy + CARRIER_H/2 + 2, 0.4, "B.SilkS"))
|
||
|
||
# ================================================================
|
||
# ADAPTERS: Castellated SOP breakouts with FPC connectors
|
||
# ================================================================
|
||
|
||
SOP_PITCH = 1.27
|
||
|
||
def make_adapter(name, board_cx, board_cy, pins_per_side, row_span, n_fpc_pins):
|
||
"""
|
||
Adapter board with:
|
||
- Castellated half-holes on TOP and BOTTOM long edges (SOP pads)
|
||
- FPC connector centered on F.Cu
|
||
- Board edges through SOP pad centers
|
||
"""
|
||
total_pins = pins_per_side * 2
|
||
row_len = (pins_per_side - 1) * SOP_PITCH
|
||
|
||
bw = row_len + 3.0 # board width (along pin rows)
|
||
bh = row_span # board height = exactly SOP row spacing
|
||
|
||
# Board outline — edges at castellation centers
|
||
bl = board_cx - bw/2
|
||
br = board_cx + bw/2
|
||
bt = board_cy - bh/2 # top edge (pins N+1..2N)
|
||
bb = board_cy + bh/2 # bottom edge (pins 1..N)
|
||
|
||
# Don't draw left/right edges if they coincide with panel v-score
|
||
# (they'll be part of the panel outline or v-score already)
|
||
# Draw top and bottom edges
|
||
pcb.append(gr_line(bl, bt, br, bt))
|
||
pcb.append(gr_line(br, bt, br, bb))
|
||
pcb.append(gr_line(br, bb, bl, bb))
|
||
pcb.append(gr_line(bl, bb, bl, bt))
|
||
|
||
# Label
|
||
pcb.append(gr_text(name, board_cx, board_cy, 0.6))
|
||
|
||
# Castellated pads footprint
|
||
pcb.append(fp_hdr(f"{name}_cast", board_cx, board_cy, "B.Cu"))
|
||
|
||
for i in range(pins_per_side):
|
||
pin = i + 1
|
||
x = -row_len/2 + i * SOP_PITCH
|
||
net_id = pin + 9 # S1=net10, S2=net11, etc.
|
||
# Bottom edge: pins 1..N
|
||
pcb.append(pad_cast(str(pin), x, bh/2, net_id, f"S{pin}"))
|
||
# Top edge: pins 2N..N+1 (reversed, SOP convention)
|
||
top_pin = total_pins - i
|
||
top_net = top_pin + 9
|
||
pcb.append(pad_cast(str(top_pin), x, -bh/2, top_net, f"S{top_pin}"))
|
||
|
||
pcb.append(" )")
|
||
|
||
# Pin labels on front silk
|
||
for i in range(pins_per_side):
|
||
x = board_cx - row_len/2 + i * SOP_PITCH
|
||
pcb.append(gr_text(str(i+1), x, bb + 0.7, 0.3))
|
||
pcb.append(gr_text(str(total_pins - i), x, bt - 0.7, 0.3))
|
||
|
||
# Pin 1 dot
|
||
pcb.append(gr_circle(board_cx - row_len/2 - 0.5, bb + 0.5, 0.2))
|
||
pcb.append(gr_text("●1", board_cx - row_len/2 - 0.5, bb + 1.3, 0.3))
|
||
|
||
# Pin labels on back silk (mirror, for soldering reference)
|
||
pcb.append(gr_text(f"{name} ●1→", board_cx, board_cy, 0.5, "B.SilkS", mirror=True))
|
||
|
||
# FPC connector centered
|
||
fpc_pitch_a = 0.5
|
||
pcb.append(fp_hdr(f"{name}_FPC", board_cx, board_cy))
|
||
for j in range(n_fpc_pins):
|
||
x = -((n_fpc_pins - 1) * fpc_pitch_a / 2) + j * fpc_pitch_a
|
||
# Map FPC pins to SOP pin nets
|
||
if j < total_pins:
|
||
sop_pin = j + 1
|
||
net_id = sop_pin + 9
|
||
nn = f"S{sop_pin}"
|
||
elif j == n_fpc_pins - 2:
|
||
net_id = 1; nn = "GND" # second-to-last = GND
|
||
elif j == n_fpc_pins - 1:
|
||
net_id = 2; nn = "VBAT" # last = VBAT
|
||
else:
|
||
net_id = 0; nn = ""
|
||
pcb.append(pad_smd(f"F{j+1}", x, 0, 0.3, 1.0, net_id, nn))
|
||
# Anchor pads
|
||
aw = (n_fpc_pins - 1) * fpc_pitch_a / 2 + 1.5
|
||
pcb.append(pad_smd("SA", -aw, 1.5, 1.2, 0.8, 0, ""))
|
||
pcb.append(pad_smd("SB", aw, 1.5, 1.2, 0.8, 0, ""))
|
||
pcb.append(" )")
|
||
|
||
pcb.append(gr_text("FPC", board_cx, board_cy - 1.8, 0.35))
|
||
pcb.append(gr_circle(board_cx - (n_fpc_pins-1)*fpc_pitch_a/2 - 0.6, board_cy, 0.15))
|
||
|
||
# Place adapters in the strip
|
||
strip_cy = vs_y + 0.5 + ADAPTER_STRIP_H / 2
|
||
|
||
# SOP-8: 4+4 pins, 3.9mm span, 10-pin FPC (8 signals + GND + VBAT)
|
||
make_adapter("SOP-8", ox + a8_w/2 + 0.5, strip_cy, 4, 3.9, 10)
|
||
|
||
# SOP-16 narrow: 8+8 pins, 6.0mm span, 20-pin FPC (16 signals + GND + VBAT + 2 spare)
|
||
make_adapter("SOP-16N", vs_x1 + a16n_w/2, strip_cy, 8, 6.0, 20)
|
||
|
||
# SOP-16 wide: 8+8 pins, 10.3mm span, 20-pin FPC
|
||
make_adapter("SOP-16W", vs_x2 + a16w_w/2, strip_cy, 8, 10.3, 20)
|
||
|
||
# ================================================================
|
||
# Panel title
|
||
# ================================================================
|
||
pcb.append(gr_text("Baby Mobile v4 — JLCPCB Panel", ox + PANEL_W/2, oy - 2, 1.0))
|
||
pcb.append(gr_text("Castellated Holes=YES V-Score=YES 2-Layer 1.6mm", ox + PANEL_W/2, oy + PANEL_H + 2, 0.5))
|
||
|
||
pcb.append(")")
|
||
return "\n".join(pcb)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
outdir = os.path.dirname(os.path.abspath(__file__))
|
||
|
||
pcb_text = generate()
|
||
pcb_path = os.path.join(outdir, "baby-mobile-panel.kicad_pcb")
|
||
with open(pcb_path, "w") as f:
|
||
f.write(pcb_text)
|
||
print(f"Written: {pcb_path} ({len(pcb_text)} bytes)")
|
||
|
||
proj = json.dumps({
|
||
"meta": {"filename": "baby-mobile-panel.kicad_pro", "version": 1},
|
||
"board": {"design_settings": {"rules": {"min_track_width": 0.15}}},
|
||
"net_settings": {"classes": [{"name":"Default","clearance":0.15,"track_width":0.25}]},
|
||
}, indent=2)
|
||
with open(os.path.join(outdir, "baby-mobile-panel.kicad_pro"), "w") as f:
|
||
f.write(proj)
|
||
|
||
print("\nPanel:")
|
||
print(" Carrier (44×26mm): XIAO(accurate outline+USB-C), IS25LP128F(pin1 dot+notch), TPS61220(pin1 dot), FPC-20")
|
||
print(" SOP-8 adapter: castellation on long edges, 10-pin FPC center")
|
||
print(" SOP-16N adapter: castellation on long edges, 20-pin FPC center")
|
||
print(" SOP-16W adapter: castellation on long edges, 20-pin FPC center")
|
||
print(" V-score between carrier and adapter strip, and between each adapter")
|
||
print(" All polarized parts have ●1 markers")
|