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:
1 parent
dbc1f0041f
commit
28c666506e
11 files changed
+30816
-422
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user