#!/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()