301 lines
13 KiB
Python
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()
|