Add routed PSU carrier board layout (rev 0.1)

- Placement script (layout_pcb.py): LV left, HV right, panel connector on the
  boundary, terminal blocks on the edges, heatsink space for the TO-220s
- Freerouting-based routing script (route_pcb.py) that routes with oversized
  tracks/vias and shrinks them, giving 3 mm HV-to-LV and 1.3 mm HV-to-HV
- Custom DRC rules (plasma_dmd_psu.kicad_dru) with documented exceptions
- Wide TO-92 footprints; schematic generator now keeps symbol UUIDs
- DRC: 0 violations, 0 unconnected pads, schematic parity OK

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-24 02:58:35 +02:00
1 parent dbc1f0041f
commit 28c666506e
11 files changed
+30816 -422

No files matched your search

+225
View File
@@ -0,0 +1,225 @@
"""Place footprints, draw the outline and silkscreen for the PSU carrier board.
Run with KiCad's bundled Python (KiCad must not have the board open):
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/layout_pcb.py
Coordinates below are board-local mm (origin = top-left corner of the board, y down).
Positions are the footprint origin (pad 1). Axial parts run left to right, pad 1 on the left.
"""
from pathlib import Path
import pcbnew
HERE = Path(__file__).parent
BOARD = HERE / "plasma_dmd_psu.kicad_pcb"
OX, OY = 100.0, 50.0 # board origin on the page
W, H = 170.0, 125.0 # board size
HV_X = 61.0 # LV | HV boundary (silkscreen)
# ref: (x, y, rotation_deg)
PLACE = {
# Phoenix MKDS terminals: wire entry is on the footprint's +y side, so top-edge blocks are
# rotated 180 (entry up) and bottom-edge blocks stay at 0 (entry down).
# ---- low-voltage side (left) -------------------------------------------------
"J1": (16.08, 6.5, 180), # 12 V in
"J3": (39.24, 6.5, 180), # Mini560
"J8": (57.16, 6.5, 180), # ESP32
"F1": (8.0, 22.0, 0),
"D1": (8.0, 31.0, 0),
"C1": (28.0, 35.0, 0),
"C2": (40.0, 22.0, 0),
"C3": (37.0, 34.0, 0),
"D3": (37.0, 41.0, 0),
"D2": (37.0, 47.0, 0),
"C4": (50.0, 41.0, 0),
"R1": (37.0, 53.0, 0),
"Q1": (44.0, 60.0, 0),
"JP1": (50.0, 72.0, 0),
"F2": (8.0, 76.0, 0),
"F3": (8.0, 90.0, 0),
"J4": (11.0, 118.5, 0), # relay module
"J6": (40.0, 118.5, 0), # isolated DC-DC input
# ---- positive HV (top right) -------------------------------------------------
"J5": (76.16, 6.5, 180), # NCH6100HV #1
"F4": (66.0, 20.0, 0),
"C5": (96.0, 20.0, 0),
"R2": (110.0, 20.0, 0),
"R3": (127.0, 20.0, 0),
"LED1": (148.0, 20.0, 0),
"D4": (68.0, 32.0, 0),
"R5": (80.0, 32.0, 0),
"R4": (102.0, 32.0, 0),
"Q2": (117.0, 33.0, 0),
"D5": (127.0, 32.0, 0),
"Q3": (81.0, 52.0, 0), # heatsink space above (tab side)
"C6": (98.0, 50.0, 0),
"R6": (119.0, 50.0, 0),
# ---- panel connector: on the LV/HV boundary, LV pins 4-7 face left, HV pins 1,2,8 right
"J2": (61.0, 78.0, 90),
# ---- negative HV (bottom right) ----------------------------------------------
"Q5": (81.0, 70.0, 0), # heatsink space above (tab side)
"Q6": (99.0, 70.0, 0),
"R13": (114.0, 62.0, 0), # 5 W, runs warm: keep clear of caps
"D7": (74.0, 80.5, 0),
"D8": (90.0, 79.0, 0),
"R12": (106.0, 79.0, 0),
"C8": (122.0, 79.0, 0),
"D6": (68.0, 89.0, 0),
"R10": (82.0, 89.0, 0),
"R9": (105.0, 89.0, 0),
"Q4": (120.0, 90.0, 0),
"R11": (130.0, 89.0, 0),
"C9": (68.0, 99.0, 0),
"C7": (92.0, 101.0, 0),
"R7": (106.0, 99.0, 0),
"R8": (122.0, 99.0, 0),
"LED2": (144.0, 99.0, 0),
"F5": (120.0, 111.0, 0),
"J7": (70.0, 118.5, 0), # NCH6100HV #2 output
# ---- mounting holes ------------------------------------------------------------
"H1": (4.0, 4.0, 0),
"H2": (W - 4.0, 4.0, 0),
"H3": (4.0, H - 4.0, 0),
"H4": (W - 4.0, H - 4.0, 0),
}
SILK = [
# (text, x, y, size, layer)
("PLASMA DMD PSU rev 0.1", 24.0, 102.0, 1.5, "F.SilkS"),
("J1 12V IN", 13.5, 13.6, 1.0, "F.SilkS"),
("J3 MINI560", 31.6, 13.6, 1.0, "F.SilkS"),
("J8 ESP32", 52.1, 13.6, 1.0, "F.SilkS"),
("J5 HV MOD 1", 71.1, 13.6, 1.0, "F.SilkS"),
("J4 RELAY MODULE", 21.2, 111.2, 1.0, "F.SilkS"),
("J6 ISO DC-DC IN", 41.5, 111.2, 1.0, "F.SilkS"),
("J7 HV MOD 2 OUT", 72.5, 111.2, 1.0, "F.SilkS"),
("J2 PANEL", 61.0, 44.0, 1.0, "F.SilkS"),
("DANGER", 158.0, 38.0, 2.0, "F.SilkS"),
("HIGH VOLTAGE", 158.0, 42.0, 1.6, "F.SilkS"),
("up to 220V", 158.0, 45.5, 1.2, "F.SilkS"),
("DANGER HIGH VOLTAGE", 152.0, 70.0, 1.6, "B.SilkS"),
]
def mm(v):
return pcbnew.FromMM(v)
def pt(x, y):
return pcbnew.VECTOR2I(mm(OX + x), mm(OY + y))
FP_DIR = Path(pcbnew.__file__).parents[3] / "share/kicad/footprints" # <KiCad>/10.0/share/...
def schematic_footprints():
"""ref -> footprint lib id, read from the schematic."""
import re
text = (HERE / "plasma_dmd_psu.kicad_sch").read_text(encoding="utf-8")
out = {}
for m in re.finditer(r'\(property "Reference" "([^"]+)".*?\(property "Footprint" "([^"]*)"', text, re.S):
out[m.group(1)] = m.group(2)
return out
def sync_footprints(board):
"""Swap footprints whose library id no longer matches the schematic (keeps ref, value, nets, link)."""
wanted = schematic_footprints()
for old in list(board.GetFootprints()):
ref = old.GetReference()
fpid = wanted.get(ref)
if not fpid or fpid == old.GetFPIDAsString():
continue
lib, name = fpid.split(":")
new = pcbnew.FootprintLoad(str(FP_DIR / f"{lib}.pretty"), name)
new.SetFPID(pcbnew.LIB_ID(lib, name))
new.SetReference(ref)
new.SetValue(old.GetValue())
new.SetPath(old.GetPath())
new.SetSheetname(old.GetSheetname())
new.SetSheetfile(old.GetSheetfile())
nets = {p.GetNumber(): p.GetNet() for p in old.Pads()}
board.Add(new)
for p in new.Pads():
if p.GetNumber() in nets:
p.SetNet(nets[p.GetNumber()])
new.SetPosition(old.GetPosition())
board.Delete(old)
print("swapped", ref, "->", fpid)
def main():
board = pcbnew.LoadBoard(str(BOARD))
sync_footprints(board)
# clean previous layout output (outline, our silkscreen, tracks, vias, zones)
for d in list(board.GetDrawings()):
if d.GetLayer() in (pcbnew.Edge_Cuts, pcbnew.F_SilkS, pcbnew.B_SilkS):
board.Delete(d)
for getter in (board.GetTracks, board.Zones):
try:
items = list(getter())
except TypeError: # SWIG returns a bare object when the list is empty
items = []
for it in items:
board.Delete(it)
fps = {f.GetReference(): f for f in board.GetFootprints()}
missing = set(fps) - set(PLACE)
extra = set(PLACE) - set(fps)
if missing or extra:
raise SystemExit(f"placement table mismatch: missing {missing}, unknown {extra}")
for ref, (x, y, rot) in PLACE.items():
f = fps[ref]
if f.IsFlipped():
f.Flip(f.GetPosition(), False)
f.SetOrientationDegrees(rot)
f.SetPosition(pt(x, y))
f.SetLocked(ref.startswith(("J", "H")))
if ref.startswith(("J", "H")):
f.Reference().SetVisible(False) # connectors are labelled by the SILK texts
# board outline with 2 mm corner radius
r = 2.0
def seg(x1, y1, x2, y2):
s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT)
s.SetStart(pt(x1, y1)); s.SetEnd(pt(x2, y2))
s.SetLayer(pcbnew.Edge_Cuts); s.SetWidth(mm(0.1)); board.Add(s)
def arc(cx, cy, sx, sy, ang):
a = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_ARC)
a.SetCenter(pt(cx, cy)); a.SetStart(pt(sx, sy)); a.SetArcAngleAndEnd(pcbnew.EDA_ANGLE(ang, pcbnew.DEGREES_T))
a.SetLayer(pcbnew.Edge_Cuts); a.SetWidth(mm(0.1)); board.Add(a)
seg(r, 0, W - r, 0); seg(W, r, W, H - r); seg(W - r, H, r, H); seg(0, H - r, 0, r)
arc(r, r, 0, r, 90); arc(W - r, r, W - r, 0, 90); arc(W - r, H - r, W, H - r, 90); arc(r, H - r, r, H, 90)
# HV boundary: dashed line on silkscreen (gap where the panel connector J2 straddles it)
y = 15.0
while y < H - 3:
if 42.0 < y < 84.0:
y += 3.5
continue
s = pcbnew.PCB_SHAPE(board, pcbnew.SHAPE_T_SEGMENT)
s.SetStart(pt(HV_X, y)); s.SetEnd(pt(HV_X, min(y + 2.0, H - 3)))
s.SetLayer(pcbnew.F_SilkS); s.SetWidth(mm(0.3)); board.Add(s)
y += 3.5
for text, x, ty, size, layer in SILK:
t = pcbnew.PCB_TEXT(board)
t.SetText(text); t.SetPosition(pt(x, ty))
t.SetTextSize(pcbnew.VECTOR2I(mm(size), mm(size))); t.SetTextThickness(mm(size * 0.15))
t.SetLayer(board.GetLayerID(layer))
if layer.startswith("B."):
t.SetMirrored(True)
board.Add(t)
board.Save(str(BOARD))
# report pad positions of rotated connectors so orientation can be checked
for ref in ("J2", "J4", "J6", "J7"):
f = fps[ref]
print(ref, [(p.GetNumber(), round(pcbnew.ToMM(p.GetPosition().x) - OX, 2),
round(pcbnew.ToMM(p.GetPosition().y) - OY, 2)) for p in f.Pads()])
print("placed", len(PLACE), "footprints")
if __name__ == "__main__":
main()