Files
pcb/scripts/generate_footprints.py

301 lines
13 KiB
Python

#!/usr/bin/env python3
"""Generate manufacturer-recommended custom footprints used by board.kicad_sch.
Dimensions are in millimetres and come from the package/land-pattern drawings
listed in docs/reference_manifest.md. No generic IPC generator is used for
the irregular high-current and sensor packages.
"""
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "hardware" / "board.pretty"
OUT.mkdir(parents=True, exist_ok=True)
def fmt(value: float) -> str:
return f"{value:.4f}".rstrip("0").rstrip(".") or "0"
def header(name: str, description: str, courtyard: tuple[float, float], body: tuple[float, float]) -> list[str]:
cw, ch = courtyard
bw, bh = body
return [
f'(footprint "{name}"',
' (version 20240108)',
' (generator "board-footprints")',
' (layer "F.Cu")',
f' (descr "{description}")',
' (attr smd)',
' (property "Reference" "REF**" (at 0 -5 0) (layer "F.SilkS")',
' (effects (font (size 1 1) (thickness 0.15))))',
' (property "Value" "" (at 0 5 0) (layer "F.Fab") hide',
' (effects (font (size 1 1) (thickness 0.15))))',
f' (fp_rect (start {fmt(-bw/2)} {fmt(-bh/2)}) (end {fmt(bw/2)} {fmt(bh/2)})',
' (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))',
f' (fp_rect (start {fmt(-cw/2)} {fmt(-ch/2)}) (end {fmt(cw/2)} {fmt(ch/2)})',
' (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))',
]
def silk_box(lines: list[str], width: float, height: float, mark: tuple[float, float] | None = None) -> None:
x, y = width / 2, height / 2
for x1, y1, x2, y2 in ((-x, -y, x, -y), (x, -y, x, y), (x, y, -x, y), (-x, y, -x, -y)):
lines.append(
f' (fp_line (start {fmt(x1)} {fmt(y1)}) (end {fmt(x2)} {fmt(y2)}) '
'(stroke (width 0.15) (type default)) (layer "F.SilkS"))'
)
if mark:
lines.append(
f' (fp_circle (center {fmt(mark[0])} {fmt(mark[1])}) (end {fmt(mark[0]+0.25)} {fmt(mark[1])}) '
'(stroke (width 0.15) (type default)) (fill solid) (layer "F.SilkS"))'
)
def pad(lines: list[str], number: str, x: float, y: float, sx: float, sy: float,
shape: str = "roundrect", layers: str = '"F.Cu" "F.Paste" "F.Mask"',
rotation: float = 0.0) -> None:
rot = f" {fmt(rotation)}" if rotation else ""
lines.append(
f' (pad "{number}" smd {shape} (at {fmt(x)} {fmt(y)}{rot}) '
f'(size {fmt(sx)} {fmt(sy)}) (layers {layers})'
+ (' (roundrect_rratio 0.15)' if shape == "roundrect" else '') + ')'
)
def finish(name: str, lines: list[str]) -> None:
lines.append(' (embedded_fonts no)')
lines.append(')')
(OUT / f"{name}.kicad_mod").write_text("\n".join(lines) + "\n")
def texas_rpw0010a() -> None:
name = "Texas_RPW0010A_QFN10"
l = header(name, "TI RPW0010A VQFN-HR; TI land pattern 4225183/A", (3.0, 3.0), (2.0, 2.0))
silk_box(l, 2.35, 2.35, (-1.45, -1.15))
# Corner lands are the union of a 0.25 x 0.60 vertical land and a
# 0.60 x 0.30 horizontal land, exactly matching TI's L-shaped examples.
for number, sx, sy in (("1", -1, -1), ("4", -1, 1), ("7", 1, 1), ("10", 1, -1)):
pad(l, number, sx * 0.725, sy * 0.9, 0.25, 0.60)
pad(l, number, sx * 0.9, sy * 0.75, 0.60, 0.30)
pad(l, "2", -0.9, -0.225, 0.60, 0.25)
pad(l, "3", -0.9, 0.225, 0.60, 0.25)
pad(l, "5", -0.25, 0, 0.30, 2.40)
pad(l, "6", 0.25, 0, 0.30, 2.40)
pad(l, "8", 0.9, 0.225, 0.60, 0.25)
pad(l, "9", 0.9, -0.225, 0.60, 0.25)
finish(name, l)
def texas_rux0012a() -> None:
name = "Texas_RUX0012A_VQFN12"
l = header(name, "TI RUX0012A VQFN-HR; TI land pattern 4224010/A", (3.4, 3.4), (2.5, 2.5))
silk_box(l, 2.65, 2.65, (-1.45, -1.2))
# Large current pads, left pins 1/2 and right pins 8/7.
for number, x, y in (("1", -0.675, -0.35), ("2", -0.675, 0.35),
("8", 0.675, -0.35), ("7", 0.675, 0.35)):
pad(l, number, x, y, 1.05, 0.40)
for number, x in zip(("12", "11", "10", "9"), (-0.75, -0.25, 0.25, 0.75)):
pad(l, number, x, -1.15, 0.20, 0.60)
for number, x in zip(("3", "4", "5", "6"), (-0.75, -0.25, 0.25, 0.75)):
pad(l, number, x, 1.15, 0.20, 0.60)
finish(name, l)
def texas_rqy0009a() -> None:
name = "Texas_RQY0009A_VQFN9"
l = header(name, "TI RQY0009A VQFN-HR; TI land pattern 4225639/A", (2.8, 3.2), (1.5, 2.0))
silk_box(l, 1.75, 2.45, (-1.05, -1.25))
for number, x in zip(("1", "9", "8"), (-0.5, 0, 0.5)):
pad(l, number, x, -1.175, 0.25, 0.55)
for number, x in zip(("4", "5", "6"), (-0.5, 0, 0.5)):
pad(l, number, x, 1.175, 0.25, 0.55)
pad(l, "2", -0.475, -0.40, 0.95, 0.20)
pad(l, "3", -0.475, 0.40, 0.95, 0.20)
pad(l, "7", 0.475, 0, 0.95, 0.20)
finish(name, l)
def texas_rzv0024a() -> None:
name = "Texas_RZV0024A_WQFN24"
l = header(name, "TI RZV0024A WQFN-FCRLF; TI land pattern 4228538/A", (3.5, 4.5), (3.05, 4.05))
silk_box(l, 3.15, 4.15, (-1.75, -2.1))
# Left pins 1..8 and right pins 20..13. The four corner pins use TI's
# L-shaped lands; repeated pad numbers create one electrical pad in KiCad.
for number, sx, sy in (("1", -1, -1), ("8", -1, 1), ("13", 1, 1), ("20", 1, -1)):
# Trim only the two inward tips from the TI nominal 0.60-mm L shape.
# This keeps the outer solder fillet while meeting JLC's 0.10-mm
# copper web to the two adjacent lands.
pad(l, number, sx * 1.25, sy * 1.825, 0.25, 0.45)
pad(l, number, sx * 1.325, sy * 1.75, 0.45, 0.25)
for number, y in zip(("2", "3", "4", "5", "6", "7"), (-1.25, -0.75, -0.25, 0.25, 0.75, 1.25)):
pad(l, number, -1.25, y, 0.60, 0.25)
for number, y in zip(("19", "18", "17", "16", "15", "14"), (-1.25, -0.75, -0.25, 0.25, 0.75, 1.25)):
pad(l, number, 1.25, y, 0.60, 0.25)
for number, x in zip(("24", "23", "22", "21"), (-0.75, -0.25, 0.25, 0.75)):
pad(l, number, x, -1.75, 0.25, 0.60)
for number, x in zip(("9", "10", "11", "12"), (-0.75, -0.25, 0.25, 0.75)):
pad(l, number, x, 1.75, 0.25, 0.60)
# TI calls out three separate exposed-metal zones, all connected to GND.
# Leave a JLC-process-compatible 0.10-mm minimum copper gap to the
# perimeter lands while retaining the package's three-zone topology.
zones = ((-0.425, -0.65, 0.60, 1.10), (0.425, -0.65, 0.60, 1.10), (0, 0.65, 1.45, 1.10))
for x, y, sx, sy in zones:
pad(l, "25", x, y, sx, sy, "rect", '"F.Cu" "F.Mask"')
# 70-75% paste coverage, split so there are no large paste deposits.
nx = 2 if sx > 1.0 else 1
for ix in range(nx):
px = x + (ix - (nx - 1) / 2) * (sx / nx)
pad(l, "", px, y, sx / nx - 0.12, sy - 0.18, "roundrect", '"F.Paste"')
finish(name, l)
def simple_two_pad(name: str, description: str, body: tuple[float, float],
centres: float, size: tuple[float, float], courtyard: tuple[float, float]) -> None:
l = header(name, description, courtyard, body)
silk_box(l, body[0] + 0.15, body[1] + 0.15, (-body[0] / 2 - 0.2, -body[1] / 2))
pad(l, "1", -centres, 0, *size)
pad(l, "2", centres, 0, *size)
finish(name, l)
def texas_dpy0002a() -> None:
simple_two_pad("Texas_DPY0002A_X1SON", "TI DPY0002A X1SON; ESD501 official land", (1.0, 0.6),
0.35, (0.30, 0.50), (1.4, 1.0))
def murata_dfe201612e() -> None:
simple_two_pad("Murata_DFE201612E", "Murata DFE201612E recommended land J(E)TE243A-0006", (2.0, 1.6),
0.8, (0.80, 1.80), (2.8, 2.4))
def coilcraft_xel4030() -> None:
simple_two_pad("Coilcraft_XEL4030", "Coilcraft XEL4030 recommended land, March 2026", (4.0, 4.0),
1.675, (0.98, 3.40), (5.0, 4.8))
def kyocera_600l() -> None:
simple_two_pad("Kyocera_600L_0402", "KYOCERA AVX 600L 0402 recommended land", (1.0, 0.5),
0.65, (0.90, 0.70), (2.2, 1.2))
def kyocera_sf14() -> None:
name = "Kyocera_SF14_5pad"
l = header(name, "KYOCERA SF14-1582M5UUS2 SAW; official five-pad land", (1.8, 1.5), (1.1, 0.9))
silk_box(l, 1.2, 1.0, (-0.8, 0))
for number, x, y in (("1", -0.5, 0), ("2", 0, 0.2875), ("3", 0.5, 0.2875),
("4", 0.5, -0.2875), ("5", 0, -0.2875)):
pad(l, number, x, y, 0.25, 0.325)
finish(name, l)
def epson_tg2016smn() -> None:
name = "Epson_TG2016SMN"
l = header(name, "Epson TG2016SMN official 2.0 x 1.6 mm land", (3.0, 2.6), (2.0, 1.6))
silk_box(l, 2.15, 1.75, (-1.35, -0.85))
for number, x, y in (("1", -0.825, -0.565), ("2", -0.825, 0.565),
("3", 0.825, 0.565), ("4", 0.825, -0.565)):
pad(l, number, x, y, 0.60, 0.75)
finish(name, l)
def murata_sch16t() -> None:
name = "Murata_SCH16T_K01"
l = header(name, "Murata SCH16T-K01-1 official PCB land pattern", (15.2, 12.8), (13.4, 11.8))
silk_box(l, 13.55, 11.95, (-7.15, -5.45))
ys = [-4.4 + 0.8 * i for i in range(12)]
for number, y in zip(range(1, 13), ys):
pad(l, str(number), -6.165, y, 1.87, 0.50)
for number, y in zip(range(24, 12, -1), ys):
pad(l, str(number), 6.165, y, 1.87, 0.50)
# Assembly-critical inertial orientation marking.
l += [
' (fp_text user "X+ ->" (at 0 -2) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.14))))',
' (fp_text user "Y+ ^" (at 0 0) (layer "F.SilkS") (effects (font (size 0.8 0.8) (thickness 0.14))))',
]
finish(name, l)
def ftdi_vqfn56() -> None:
name = "FTDI_VQFN56_EP5p9"
l = header(name, "FTDI FT4232H-56Q VQFN56 land from TN_166; 5.9-mm EP", (9.2, 9.2), (8.0, 8.0))
silk_box(l, 8.15, 8.15, (-4.4, -3.85))
coords = [-3.25 + 0.5 * i for i in range(14)]
for number, y in zip(range(1, 15), coords):
pad(l, str(number), -4.00, y, 0.80, 0.25)
for number, x in zip(range(15, 29), coords):
pad(l, str(number), x, 4.00, 0.25, 0.80)
for number, y in zip(range(29, 43), reversed(coords)):
pad(l, str(number), 4.00, y, 0.80, 0.25)
for number, x in zip(range(43, 57), reversed(coords)):
pad(l, str(number), x, -4.00, 0.25, 0.80)
pad(l, "57", 0, 0, 5.90, 5.90, "rect", '"F.Cu" "F.Mask"')
for x in (-2.22, -0.74, 0.74, 2.22):
for y in (-2.22, -0.74, 0.74, 2.22):
pad(l, "", x, y, 1.20, 1.20, "roundrect", '"F.Paste"')
finish(name, l)
def amphenol_jtag() -> None:
name = "Amphenol_20021121_00010C4LF"
l = header(name, "Amphenol Minitek127 20021121-00010C4LF vertical SMT 2x5", (7.4, 6.0), (7.0, 4.6))
silk_box(l, 7.0, 4.7, (-3.8, 2.3))
xs = (-2.54, -1.27, 0, 1.27, 2.54)
for col, x in enumerate(xs):
pad(l, str(1 + col * 2), x, 1.95, 0.76, 2.40)
pad(l, str(2 + col * 2), x, -1.95, 0.76, 2.40)
finish(name, l)
def digilent_pmod_2x6() -> None:
"""2.54-mm dual-row PMOD with Digilent's row-wise pin numbering.
Generic KiCad 2xN headers use IDC odd/even numbering. A PMOD instead has
pins 1..6 on one physical row and 7..12 on the other, so a dedicated
footprint prevents a silent GND/VCC and signal-position mismatch.
"""
name = "Digilent_PMOD_2x6_P2.54mm"
l = [
f'(footprint "{name}"',
' (version 20240108)',
' (generator "board-footprints")',
' (layer "F.Cu")',
' (descr "Digilent PMOD 2x6, 2.54-mm pitch, row-wise pins 1-6 and 7-12")',
' (attr through_hole)',
' (property "Reference" "REF**" (at 6.35 -2.5 0) (layer "F.SilkS")',
' (effects (font (size 1 1) (thickness 0.15))))',
' (property "Value" "" (at 6.35 5 0) (layer "F.Fab") hide',
' (effects (font (size 1 1) (thickness 0.15))))',
' (fp_rect (start -1.27 -1.27) (end 13.97 3.81)',
' (stroke (width 0.15) (type default)) (fill none) (layer "F.SilkS"))',
' (fp_rect (start -1.5 -1.5) (end 14.2 4.04)',
' (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))',
' (fp_circle (center -0.8 -0.8) (end -0.5 -0.8)',
' (stroke (width 0.15) (type default)) (fill solid) (layer "F.SilkS"))',
]
for number in range(1, 13):
row = 0 if number <= 6 else 1
column = number - 1 if number <= 6 else number - 7
shape = "rect" if number == 1 else "circle"
l.append(
f' (pad "{number}" thru_hole {shape} (at {fmt(column * 2.54)} {fmt(row * 2.54)}) '
'(size 1.7 1.7) (drill 1) (layers "*.Cu" "*.Mask"))'
)
finish(name, l)
def main() -> None:
for generator in (texas_rpw0010a, texas_rux0012a, texas_rzv0024a, texas_rqy0009a,
murata_dfe201612e, texas_dpy0002a, kyocera_600l, kyocera_sf14,
epson_tg2016smn, murata_sch16t, ftdi_vqfn56,
coilcraft_xel4030, amphenol_jtag, digilent_pmod_2x6):
generator()
print(f"Generated {len(list(OUT.glob('*.kicad_mod')))} custom footprints in {OUT}")
if __name__ == "__main__":
main()