From 804e2a2cb80a2dd0562c78438024a2583b91920a Mon Sep 17 00:00:00 2001 From: Michele Bigi Date: Tue, 8 Sep 2026 00:07:33 +0200 Subject: [PATCH] Add generator for single-row castellated edge footprints New scripts/generate_castellated_edge.py produces a reusable footprint (round PTH pads + an embedded Edge.Cuts line through their centers) for mounting a module flush onto another board's edge, ESP32-WROOM style -- distinct from the QFP/SSOP breakout-adapter footprints already in footprints/other/castellated_breakouts.pretty/. Default pad size (1.0mm dia / 0.5mm drill) matches the vias used in reference/kicad-castellated-breakouts. Generated CASTELLATED_EDGE_1x20_P1.27MM (20 pins, 1.27mm pitch) into footprints/other/castellated_edge.pretty/, registered as MIKILAB_castellated_edge. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01T5pjBJQ7tf9pwsSs8rG2sm --- MANIFEST.csv | 1 + .../CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod | 32 +++++ fp-lib-table | 1 + fp-lib-table.global | 1 + scripts/generate_castellated_edge.py | 114 ++++++++++++++++++ 5 files changed, 149 insertions(+) create mode 100644 footprints/other/castellated_edge.pretty/CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod create mode 100644 scripts/generate_castellated_edge.py diff --git a/MANIFEST.csv b/MANIFEST.csv index f12b6c8..5ca429f 100644 --- a/MANIFEST.csv +++ b/MANIFEST.csv @@ -659,3 +659,4 @@ footprint,/var/folders/cv/lwgyj_k14gx2rc3t2rlt29s80000gn/T/snapeda_gdyhq56d/BGA5 footprint,/tmp/kcb_extract/kicad-castellated-breakouts-master/kicad-castellated-breakouts.pretty/QFP48-1.27MM-CASTELLATED.kicad_mod,footprints/other/castellated_breakouts.pretty/QFP48-1.27MM-CASTELLATED.kicad_mod,NEW,3337c229df872fa541e5c96f2c5525fc53600e77eec580ab93f020d41ab44a14,category=other footprint,/tmp/kcb_extract/kicad-castellated-breakouts-master/kicad-castellated-breakouts.pretty/QFP64-1.27MM-CASTELLATED.kicad_mod,footprints/other/castellated_breakouts.pretty/QFP64-1.27MM-CASTELLATED.kicad_mod,NEW,de3f682f79645ee434d0cbe850bc687bbd65e3cecde3a0c88fe91b84432d8021,category=other footprint,/tmp/kcb_extract/kicad-castellated-breakouts-master/kicad-castellated-breakouts.pretty/SSOP8-1.27MM-CASTELLATED.kicad_mod,footprints/other/castellated_breakouts.pretty/SSOP8-1.27MM-CASTELLATED.kicad_mod,NEW,3d3beb23324b47f2d46783f537b873f02658c5bd190096ec715119bfe3eb6a76,category=other +footprint,,footprints/other/castellated_edge.pretty/CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod,NEW,5197e9b46b7a8814700067537cb67f65245ce42d7576c7c5a37269877808acc1,category=other;generated by scripts/generate_castellated_edge.py --pins 20 --pitch 1.27 diff --git a/footprints/other/castellated_edge.pretty/CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod b/footprints/other/castellated_edge.pretty/CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod new file mode 100644 index 0000000..5268b4b --- /dev/null +++ b/footprints/other/castellated_edge.pretty/CASTELLATED_EDGE_1x20_P1.27MM.kicad_mod @@ -0,0 +1,32 @@ +(footprint CASTELLATED_EDGE_1x20_P1.27MM (layer F.Cu) (tedit 00000000) + (descr "Single row of castellated edge pads, 20 pins, 1.27mm pitch. Align this row with the board outline (do not tent) so the mill/router cuts each pad in half, exposing plated copper on the board edge.") + (tags "castellated edge module") + (attr through_hole) + (fp_text reference REF** (at 0 -14.065) (layer F.SilkS) + (effects (font (size 1 1) (thickness 0.15))) + ) + (fp_text value CASTELLATED_EDGE_1x20_P1.27MM (at 0 14.065) (layer F.Fab) + (effects (font (size 1 1) (thickness 0.15))) + ) + (pad 1 thru_hole circle (at -12.065 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 2 thru_hole circle (at -10.795 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 3 thru_hole circle (at -9.525 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 4 thru_hole circle (at -8.255 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 5 thru_hole circle (at -6.985 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 6 thru_hole circle (at -5.715 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 7 thru_hole circle (at -4.445 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 8 thru_hole circle (at -3.175 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 9 thru_hole circle (at -1.905 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 10 thru_hole circle (at -0.635 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 11 thru_hole circle (at 0.635 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 12 thru_hole circle (at 1.905 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 13 thru_hole circle (at 3.175 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 14 thru_hole circle (at 4.445 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 15 thru_hole circle (at 5.715 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 16 thru_hole circle (at 6.985 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 17 thru_hole circle (at 8.255 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 18 thru_hole circle (at 9.525 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 19 thru_hole circle (at 10.795 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (pad 20 thru_hole circle (at 12.065 0) (size 1 1) (drill 0.5) (layers *.Cu *.Mask)) + (fp_line (start -12.565 0) (end 12.565 0) (layer Edge.Cuts) (width 0.1)) +) diff --git a/fp-lib-table b/fp-lib-table index a1ccf01..b7ebe5c 100644 --- a/fp-lib-table +++ b/fp-lib-table @@ -65,6 +65,7 @@ (lib (name "MIKILAB_bel_fuse")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/bel_fuse.pretty")(options "")(descr "bel_fuse.pretty")) (lib (name "MIKILAB_bourns")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/bourns.pretty")(options "")(descr "bourns.pretty")) (lib (name "MIKILAB_castellated_breakouts")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/castellated_breakouts.pretty")(options "")(descr "castellated_breakouts.pretty")) + (lib (name "MIKILAB_castellated_edge")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/castellated_edge.pretty")(options "")(descr "castellated_edge.pretty")) (lib (name "MIKILAB_ck")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/ck.pretty")(options "")(descr "ck.pretty")) (lib (name "MIKILAB_coilcraft")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/coilcraft.pretty")(options "")(descr "coilcraft.pretty")) (lib (name "MIKILAB_cts")(type "KiCad")(uri "${KIPRJMOD}/footprints/other/cts.pretty")(options "")(descr "cts.pretty")) diff --git a/fp-lib-table.global b/fp-lib-table.global index 3c3d17b..e1e272c 100644 --- a/fp-lib-table.global +++ b/fp-lib-table.global @@ -65,6 +65,7 @@ (lib (name "MIKILAB_bel_fuse")(type "KiCad")(uri "${MIKILAB}/footprints/other/bel_fuse.pretty")(options "")(descr "bel_fuse.pretty")) (lib (name "MIKILAB_bourns")(type "KiCad")(uri "${MIKILAB}/footprints/other/bourns.pretty")(options "")(descr "bourns.pretty")) (lib (name "MIKILAB_castellated_breakouts")(type "KiCad")(uri "${MIKILAB}/footprints/other/castellated_breakouts.pretty")(options "")(descr "castellated_breakouts.pretty")) + (lib (name "MIKILAB_castellated_edge")(type "KiCad")(uri "${MIKILAB}/footprints/other/castellated_edge.pretty")(options "")(descr "castellated_edge.pretty")) (lib (name "MIKILAB_ck")(type "KiCad")(uri "${MIKILAB}/footprints/other/ck.pretty")(options "")(descr "ck.pretty")) (lib (name "MIKILAB_coilcraft")(type "KiCad")(uri "${MIKILAB}/footprints/other/coilcraft.pretty")(options "")(descr "coilcraft.pretty")) (lib (name "MIKILAB_cts")(type "KiCad")(uri "${MIKILAB}/footprints/other/cts.pretty")(options "")(descr "cts.pretty")) diff --git a/scripts/generate_castellated_edge.py b/scripts/generate_castellated_edge.py new file mode 100644 index 0000000..4718cf6 --- /dev/null +++ b/scripts/generate_castellated_edge.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +generate_castellated_edge.py +============================= + +Generates a generic, reusable KiCad footprint for a single row of +castellated pads to place on a board's edge (e.g. for a module meant to +be soldered flush onto a motherboard, the same way ESP32-WROOM-style +modules are built) -- as opposed to the QFP/SSOP breakout adapters in +footprints/other/castellated_breakouts.pretty/, which are a different +thing (a fine-pitch-IC-to-hand-solderable-pad adapter). + +Technique (same one documented in reference/kicad-castellated-breakouts/ +readme.md): a round plated through-hole pad is placed with its center +exactly on the board edge; an Edge.Cuts line embedded in the footprint +runs straight through every pad center. KiCad merges Edge.Cuts graphics +from every footprint into the final board outline, so once this +footprint's row lines up with your board's edge, the mill/router cuts +each hole in half, exposing plated copper on the board edge. Default pad +size (1.0mm dia / 0.5mm drill) matches the vias used throughout that +reference project. Remember to NOT tent these pads when plotting. + +Usage: + python3 scripts/generate_castellated_edge.py --pins 20 --pitch 1.27 +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +import lib_common as lc + +ROOT = lc.LIBRARY_ROOT + + +def build_footprint(pins: int, pitch: float, pad_dia: float, drill: float) -> str: + name = f"CASTELLATED_EDGE_1x{pins}_P{pitch:g}MM" + half_width = (pins - 1) * pitch / 2 + margin = pad_dia / 2 + + lines = [ + f"(footprint {name} (layer F.Cu) (tedit 00000000)", + ' (descr "Single row of castellated edge pads, ' + f'{pins} pins, {pitch:g}mm pitch. Align this row with the board ' + 'outline (do not tent) so the mill/router cuts each pad in half, ' + 'exposing plated copper on the board edge.")', + ' (tags "castellated edge module")', + " (attr through_hole)", + f" (fp_text reference REF** (at 0 {-half_width - 2}) (layer F.SilkS)", + " (effects (font (size 1 1) (thickness 0.15)))", + " )", + f" (fp_text value {name} (at 0 {half_width + 2}) (layer F.Fab)", + " (effects (font (size 1 1) (thickness 0.15)))", + " )", + ] + + for i in range(pins): + x = round(-half_width + i * pitch, 4) + lines.append( + f" (pad {i + 1} thru_hole circle (at {x} 0) " + f"(size {pad_dia:g} {pad_dia:g}) (drill {drill:g}) (layers *.Cu *.Mask))" + ) + + x_start = round(-half_width - margin, 4) + x_end = round(half_width + margin, 4) + lines.append(f" (fp_line (start {x_start} 0) (end {x_end} 0) (layer Edge.Cuts) (width 0.1))") + lines.append(")") + return "\n".join(lines) + "\n" + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--pins", type=int, required=True, help="Number of pads in the row") + parser.add_argument("--pitch", type=float, required=True, help="Pad pitch in mm") + parser.add_argument("--pad-dia", type=float, default=1.0, help="Pad diameter in mm (default 1.0, matches reference project)") + parser.add_argument("--drill", type=float, default=0.5, help="Drill diameter in mm (default 0.5, matches reference project)") + parser.add_argument("--category", default="other", choices=lc.CATEGORIES, help="MIKILAB category (default: other)") + args = parser.parse_args() + + if args.pins < 2: + parser.error("--pins must be at least 2") + + name = f"CASTELLATED_EDGE_1x{args.pins}_P{args.pitch:g}MM" + pretty_dir = ROOT / "footprints" / args.category / "castellated_edge.pretty" + pretty_dir.mkdir(parents=True, exist_ok=True) + dest = pretty_dir / f"{name}.kicad_mod" + + if dest.exists(): + print(f"error: {dest} already exists", file=sys.stderr) + return 1 + + dest.write_text(build_footprint(args.pins, args.pitch, args.pad_dia, args.drill), encoding="utf-8") + + lc.write_fp_lib_table(ROOT) + lc.write_global_tables(ROOT) + + digest = lc.sha256_file(dest) + lc.append_manifest_rows(ROOT, [[ + "footprint", "", str(dest.relative_to(ROOT)), "NEW", digest, + f"category={args.category};generated by scripts/generate_castellated_edge.py " + f"--pins {args.pins} --pitch {args.pitch}", + ]]) + + print(f"Wrote {dest.relative_to(ROOT)}") + print(f"Registered as MIKILAB_castellated_edge:{name}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())