Heatsinks, wider 12 V tracks, design review; pause project

- PCB rev 0.1: Power3A class (1.5 mm) for the 12 V input path, Aavid
  clip-on heatsinks on Q3/Q5/Q6 (project footprint library), T-junction
  healing and via shrink in the router; DRC 0 errors
- Current-capacity and heatsink documentation
- CLAUDE.md with project decisions, workflow and gotchas
- docs/design-review.md: review of a module-only PSU (rev 0.2) and cost
  estimate
- Project paused for budget and safety reasons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-24 15:07:58 +02:00
1 parent 28c666506e
commit cdf24e0b4b
15 files changed
+5917 -4730

No files matched your search

+48 -5
View File
@@ -29,6 +29,9 @@ Wire the isolated DC-DC output straight to NCH6100HV #2's input, off-board. Both
| `check_netlist.py` | Exports the netlist with kicad-cli and checks every pin against `PARTS` |
| `plasma_dmd_psu.kicad_sch` | Generated schematic (labels-per-pin style: connectivity is by net label) |
| `plasma_dmd_psu.pdf` | Plot of the schematic |
| `plasma_dmd.pretty/`, `fp-lib-table` | Project footprint library (courtyard-less heatsink) |
| `layout_pcb.py`, `route_pcb.py` | Placement and autorouting scripts (see below) |
| `plasma_dmd_psu.kicad_dru` | Custom clearance rules |
```bash
python hardware/kicad/gen_kicad.py
@@ -43,7 +46,8 @@ stop using the generator once you start editing the schematic in KiCad.
| Class | Nets | Clearance | Track |
|---|---|---|---|
| HV | +90V_RAW, +65V, −130V_RAW, −112V, −100V, HVP/HVN_MOD and all regulator internals | 1.3 mm (3.0 mm to LV via custom rules) | 0.6 mm |
| Power | GND, +12V*, +5V, +12V_PANEL | 0.3 mm | 1.0 mm |
| Power3A | +12V_IN, +12V (behind the 3 A fuse F1) | 0.3 mm | 1.5 mm |
| Power | GND (plus the bottom pour), +12V_HV/HVP/HVN, +5V, +12V_PANEL | 0.3 mm | 1.0 mm |
| Default | Watchdog signals, LED cathode/anode nodes | 0.25 mm | 0.4 mm |
## PCB (rev 0.1)
@@ -58,10 +62,49 @@ DRC is clean: 0 violations, 0 unconnected pads, schematic parity OK.
A dashed silkscreen line marks the boundary, with DANGER text on both sides.
- **J2 (panel)** straddles the boundary. Its LV pins (GND, +5V, +12V) leave towards the left, its HV pins towards the right.
- **Terminal blocks** sit on the top and bottom edges, with wire entries facing outwards.
- **Heatsink space** is left above Q3, Q5 and Q6.
- **Heatsinks** HS1–HS3 (Aavid 576802B03900G clip-on, about 20 K/W) sit on Q3, Q5 and Q6.
> ⚠️ **The TO-220 tabs are live.** Q3's tab is at +90 V, Q5's at −130 V and Q6's at Q6_C.
> Give each transistor its own heatsink, keep the heatsinks apart, and don't let them touch anything.
> ⚠️ **The heatsinks are live.** Each one clips straight onto its transistor's tab: HS1 is at +90 V,
> HS2 at −130 V and HS3 at Q6_C. That's why each heatsink's solder pin is on the collector net,
> so the HV clearance rules also protect it. Never let the heatsinks touch each other or anything else.
### Heatsinks
- **Footprint:** `plasma_dmd:Heatsink_AAVID_576802B03900G_NoCourtyard` in the project library
`plasma_dmd.pretty`. It's a copy of KiCad's footprint with the courtyard and silkscreen outline
removed, because the clip wraps around the transistor and would always overlap its courtyard.
The outline is kept on F.Fab, and the footprint is marked "exempt from courtyard requirement".
- **Placement:** `layout_pcb.py` places each heatsink relative to its transistor (`heatsink_for()`),
with its tab face on the TO-220 tab.
- **Dissipation (estimate):** about 1–1.5 W per transistor. Bare TO-220 runs at about 62 K/W
(60–90 °C rise); with the heatsink, about 20 K/W (about 30 °C rise).
- **Project setting:** "PTH inside courtyard" is set to ignore, because each heatsink pin sits just
behind its tab, inside the transistor courtyard. Courtyard *overlaps* are still checked for all
other parts.
- **Remaining DRC items:** the 9 silkscreen warnings are the TO-220 outlines over the heatsink pins.
The fab clips silkscreen off pads, so they're harmless.
### Current capacity
Track widths follow IPC-2221 for 1 oz outer copper. Currents are estimated from the fuses.
| Net | Max current (fuse) | Width | Capacity at 10 °C rise |
|---|---|---|---|
| +12V_IN, +12V | 3 A (F1) | 1.5 mm | ≈ 3.2 A |
| GND return | 3 A | 1.0 mm + solid bottom pour on the LV side | well above 3 A |
| +12V_HV | 2 A (F2 + F3) | 1.0 mm | ≈ 2.4 A |
| +12V_HVP, +12V_HVN | 1 A (F2, F3) | 1.0 mm | ≈ 2.4 A |
| +5V | about 1 A (unfused, from the Mini560) | 1.0 mm | ≈ 2.4 A |
| HV nets | 100 mA (F4, F5) | 0.6 mm | ≈ 1.7 A (width set by clearance, not current) |
| Signals | mA | 0.4 mm | ≈ 1.2 A |
The 12 V input path, GND and +5V have no vias.
- **Note on F3:** if the negative rail ever drew the full 100 mA that F5 allows (about 13 W), the
isolated converter plus the boost module would pull more than 1 A at 12 V. That would blow F3.
Measure the real HV current at bring-up.
- **Note on +5V:** the 5 V side isn't fused on its own. Consider a polyfuse (about 1.5 A) if the panel
or the ESP32 wiring could short.
### Clearance rules
@@ -98,7 +141,7 @@ Once you start hand-editing the board in KiCad, stop running these scripts. They
- [ ] Check every footprint against the parts you actually buy: fuse holders, the 12.5 mm caps, the R13 5 W size, and the terminal block pitch.
- [ ] Map J2 to your panel's power connector. The current order follows the Stern CN2 connector.
- [ ] Check heatsink size and clearance for Q3/Q5/Q6.
- [ ] Check the heatsink orientation against the Aavid 5768 drawing. KiCad has no 3D model for it, so it isn't visible in the render.
- [ ] Consider conformal coating on the HV side.
- [ ] Get a second pair of eyes on the HV layout.
+4
View File
@@ -0,0 +1,4 @@
(fp_lib_table
(version 7)
(lib (name "plasma_dmd")(type "KiCad")(uri "${KIPRJMOD}/plasma_dmd.pretty")(options "")(descr "Project footprints"))
)
+16 -2
View File
@@ -35,6 +35,8 @@ FP_CP125 = "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm"
FP_HDR2 = "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical"
FP_KK8 = "Connector_Molex:Molex_KK-396_A-41791-0008_1x08_P3.96mm_Vertical"
FP_HOLE = "MountingHole:MountingHole_3.2mm_M3"
# Clip-on TO-220 heatsink, ~20 K/W, solder pin. The tab is live, so the pin sits on the collector net.
FP_HS = "plasma_dmd:Heatsink_AAVID_576802B03900G_NoCourtyard" # project lib, see plasma_dmd.pretty
def tb(n):
@@ -79,6 +81,7 @@ PARTS = [
("D5", "Device:D_Zener", "1N4760A 68V", FP_D41, {"K": "Q3_B", "A": "GND"}, 2),
("Q3", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"C": "+90V_RAW", "B": "Q3_B", "E": "+65V"}, 2),
("C6", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "+65V", "2": "GND"}, 2),
("HS1", "Mechanical:Heatsink_Pad", "Aavid 576802B03900G (Q3, live +90V)", FP_HS, {"1": "+90V_RAW"}, 2),
("R6", "Device:R", "330k", FP_R, {"1": "+65V", "2": "GND"}, 2),
# 4. Negative HV
("J6", "Connector:Screw_Terminal_01x02", "Isolated DC-DC in: VIN GND", tb(2),
@@ -103,6 +106,8 @@ PARTS = [
("Q6", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"B": "Q6_B", "C": "Q6_C", "E": "-100V"}, 3),
("R13", "Device:R", "2k 5W", FP_R5W, {"1": "GND", "2": "Q6_C"}, 3),
("C9", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "GND", "2": "-100V"}, 3),
("HS2", "Mechanical:Heatsink_Pad", "Aavid 576802B03900G (Q5, live -130V)", FP_HS, {"1": "-130V_RAW"}, 3),
("HS3", "Mechanical:Heatsink_Pad", "Aavid 576802B03900G (Q6, live Q6_C)", FP_HS, {"1": "Q6_C"}, 3),
# 5. Panel connector (Stern 520-5138 CN2 order; verify against your panel!)
("J2", "Connector_Generic:Conn_01x08", "Panel power (Stern CN2 order)", FP_KK8,
{"1": "-112V", "2": "-100V", "3": None, "4": "GND", "5": "GND", "6": "+5V", "7": "+12V_PANEL", "8": "+65V"}, 4),
@@ -216,7 +221,10 @@ def snap(v):
# N_LED1/N_LED2 sit a LED drop away from GND, so they stay in Default.
HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B",
"-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C"]
POWER_NETS = ["GND", "+12V", "+12V_IN", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"]
# Track widths from IPC-2221 (1 oz outer copper): 1.5 mm carries ~3.2 A at 10 C rise, 1.0 mm ~2.4 A.
POWER3A_NETS = ["+12V", "+12V_IN"] # behind F1 (3 A)
# GND: 1.0 mm tracks plus the bottom-layer pour, which carries the 3 A return on the LV side
POWER_NETS = ["GND", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"] # <= 2 A
def netclass(name, clearance, track, priority):
@@ -236,15 +244,21 @@ def write_project():
data["net_settings"] = {
"classes": [
netclass("Default", 0.25, 0.4, 2147483647),
netclass("Power", 0.3, 1.0, 1),
netclass("Power3A", 0.3, 1.5, 1),
netclass("Power", 0.3, 1.0, 2),
netclass("HV", 1.3, 0.6, 0) # HV <-> LV 3 mm comes from plasma_dmd_psu.kicad_dru,
],
"meta": {"version": 5},
"net_colors": None,
"netclass_assignments": None,
"netclass_patterns": [{"netclass": "HV", "pattern": "/" + n} for n in HV_NETS]
+ [{"netclass": "Power3A", "pattern": "/" + n} for n in POWER3A_NETS]
+ [{"netclass": "Power", "pattern": "/" + n} for n in POWER_NETS],
}
# The heatsink pins sit just behind the TO-220 tab, inside the transistor courtyard. The heatsinks
# are the only courtyard-less parts, so this check adds nothing beyond courtyards_overlap.
sev = data.setdefault("board", {}).setdefault("design_settings", {}).setdefault("rule_severities", {})
sev["pth_inside_courtyard"] = "ignore"
pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
+69 -19
View File
@@ -60,22 +60,23 @@ PLACE = {
"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),
"D7": (74.0, 83.5, 0),
"D8": (90.0, 83.0, 0),
"R12": (106.0, 83.0, 0),
"C8": (122.0, 83.0, 0),
"D6": (68.0, 93.0, 0),
"R10": (82.0, 93.0, 0),
"R9": (105.0, 93.0, 0),
"Q4": (120.0, 94.0, 0),
"R11": (130.0, 93.0, 0),
"C9": (68.0, 103.0, 0),
"C7": (92.0, 105.0, 0),
"R7": (106.0, 103.0, 0),
"R8": (122.0, 103.0, 0),
"LED2": (144.0, 103.0, 0),
"F5": (120.0, 113.5, 0),
"J7": (70.0, 118.5, 0), # NCH6100HV #2 output
# ---- heatsinks: see heatsink_for() ------------------------------------------------
# ---- mounting holes ------------------------------------------------------------
"H1": (4.0, 4.0, 0),
"H2": (W - 4.0, 4.0, 0),
@@ -83,13 +84,26 @@ PLACE = {
"H4": (W - 4.0, H - 4.0, 0),
}
HEATSINKS = {"HS1": "Q3", "HS2": "Q5", "HS3": "Q6"}
def heatsink_for(q):
"""Aavid 576802B03900G on a TO-220-3_Vertical at rotation 0 (tab towards -y)."""
x, y, _ = PLACE[q]
return (x + 2.54, y - 3.6, 270)
for _hs, _q in HEATSINKS.items():
PLACE[_hs] = heatsink_for(_q)
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"),
("J5 HV MOD 1", 68.0, 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"),
@@ -109,6 +123,12 @@ def pt(x, y):
return pcbnew.VECTOR2I(mm(OX + x), mm(OY + y))
def lib_dir(lib):
"""Project library (plasma_dmd.pretty next to the board) or a KiCad standard library."""
local = HERE / f"{lib}.pretty"
return local if local.exists() else FP_DIR / f"{lib}.pretty"
FP_DIR = Path(pcbnew.__file__).parents[3] / "share/kicad/footprints" # <KiCad>/10.0/share/...
@@ -131,7 +151,7 @@ def sync_footprints(board):
if not fpid or fpid == old.GetFPIDAsString():
continue
lib, name = fpid.split(":")
new = pcbnew.FootprintLoad(str(FP_DIR / f"{lib}.pretty"), name)
new = pcbnew.FootprintLoad(str(lib_dir(lib)), name)
new.SetFPID(pcbnew.LIB_ID(lib, name))
new.SetReference(ref)
new.SetValue(old.GetValue())
@@ -148,8 +168,33 @@ def sync_footprints(board):
print("swapped", ref, "->", fpid)
def add_missing_footprints(board):
"""Add footprints for schematic symbols that are not on the board yet (like Update PCB, F8)."""
import gen_kicad
_, uuids = gen_kicad.existing_uuids()
present = {f.GetReference() for f in board.GetFootprints()}
for ref, lib_id, value, fpid, conns, _g in gen_kicad.PARTS:
if ref in present:
continue
lib, name = fpid.split(":")
fp = pcbnew.FootprintLoad(str(lib_dir(lib)), name)
fp.SetFPID(pcbnew.LIB_ID(lib, name))
fp.SetReference(ref)
fp.SetValue(value)
fp.SetPath(pcbnew.KIID_PATH("/" + uuids[ref]))
fp.SetSheetname("/")
fp.SetSheetfile("plasma_dmd_psu.kicad_sch")
board.Add(fp)
for pad in fp.Pads():
net = conns.get(pad.GetNumber())
if net:
pad.SetNet(board.FindNet("/" + net))
print("added", ref, "->", fpid)
def main():
board = pcbnew.LoadBoard(str(BOARD))
add_missing_footprints(board)
sync_footprints(board)
# clean previous layout output (outline, our silkscreen, tracks, vias, zones)
@@ -175,9 +220,14 @@ def main():
f.Flip(f.GetPosition(), False)
f.SetOrientationDegrees(rot)
f.SetPosition(pt(x, y))
f.SetLocked(ref.startswith(("J", "H")))
if ref.startswith(("J", "H")):
fixed = ref.startswith("J") or (ref.startswith("H") and ref[1:].isdigit())
f.SetLocked(fixed)
if fixed:
f.Reference().SetVisible(False) # connectors are labelled by the SILK texts
if ref in HEATSINKS.values():
f.Reference().SetPosition(pt(x + 2.54, y + 3.2)) # below the pins, clear of the heatsink pin
if ref in HEATSINKS:
f.Reference().SetPosition(pt(x + 9.5, y + 2.0)) # beside the heatsink
# board outline with 2 mm corner radius
r = 2.0
Binary file not shown.

Before

Width:  |  Height:  |  Size: 941 KiB

After

Width:  |  Height:  |  Size: 950 KiB

@@ -0,0 +1,219 @@
(footprint "Heatsink_AAVID_576802B03900G_NoCourtyard"
(version 20260206)
(generator "pcbnew")
(generator_version "10.0")
(layer "F.Cu")
(descr "Courtyard and silkscreen outline removed (outline kept on F.Fab): the clip wraps around its TO-220, so its courtyard would always overlap it. Heatsink, 14.48x12.7x19.05mm, TO-220/ TO-262, https://www.boydcorp.com/aavid-datasheets/Board-Level-Cooling-Plug-In-5768.pdf")
(tags "heatsink")
(property "Reference" "REF**"
(at 6.35 -8.5 0)
(layer "F.SilkS")
(uuid "07f046b7-ee46-436d-a8d8-54e735bd514a")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Value" "Heatsink_AAVID_576802B03900G"
(at 6.35 8.5 0)
(layer "F.Fab")
(uuid "0c915f93-8a39-4587-9dc9-356a06b61836")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "715aca18-8f8d-4d48-a4e1-5c8463d0d1a0")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "3dce55db-d6b0-43d7-b0f4-0d716ceb00b9")
(effects
(font
(size 1.27 1.27)
(thickness 0.15)
)
)
)
(attr through_hole allow_missing_courtyard)
(duplicate_pad_numbers_are_jumpers no)
(fp_line
(start 0.405 -1.85)
(end 1.675 -1.85)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "e2130da5-6bfd-4cff-a6e0-ce36fa9ed320")
)
(fp_line
(start 0.405 1.85)
(end 1.675 1.85)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "fad026d3-0846-44b8-ac9b-fc5c71aeb7d2")
)
(fp_line
(start 0.45 -5)
(end 4.45 -5)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "684b317f-96b2-4563-94b7-a9f9dbce19d3")
)
(fp_line
(start 1.675 -5)
(end 1.675 5)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "e1c3e282-b6b0-4028-8ab4-94f132389afb")
)
(fp_line
(start 4.45 -5)
(end 4.45 5)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "2be7b066-c2ff-480b-85f2-398b9dc58fb3")
)
(fp_line
(start 4.45 5)
(end 0.45 5)
(stroke
(width 0.1)
(type solid)
)
(layer "Dwgs.User")
(uuid "f5f503b5-f93a-4eb0-a9b7-af9692dd025f")
)
(fp_line
(start -0.405 -6.35)
(end -0.405 6.35)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "dd94d98b-8847-4ce0-b30f-d31c6f77bc25")
)
(fp_line
(start -0.405 -6.35)
(end 12.295 -7.24)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "b22575e4-0838-4d53-a202-9782ea057b6e")
)
(fp_line
(start 0.405 -5.597)
(end 0.405 5.597)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "a9099b8e-83d3-42a9-8fc6-6ccc4acfec5a")
)
(fp_line
(start 0.405 -5.597)
(end 12.295 -6.43)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "872fc3e3-6f14-4f00-aac2-46697acceeb4")
)
(fp_line
(start 12.295 -7.24)
(end 12.295 7.24)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "8f66f8ce-b782-479c-bb13-b0f17be7fa48")
)
(fp_line
(start 12.295 7.24)
(end -0.405 6.35)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "8bb6ea79-3367-4e22-b8d7-6f275fda6f15")
)
(fp_line
(start 12.3 6.43)
(end 0.405 5.597)
(stroke
(width 0.1)
(type solid)
)
(layer "F.Fab")
(uuid "ec19e519-279b-4d08-8288-be58fe8a8d44")
)
(fp_text user "${REFERENCE}"
(at 6.35 0 0)
(layer "F.Fab")
(uuid "6ecf39c3-7bd4-4dc2-b3d8-015e1ce8efe0")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" thru_hole oval
(at 0 0)
(size 2.6 3.78)
(drill oval 1.1 2.28)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "ec39e68d-6144-43b8-abb3-6034313acdbd")
)
(embedded_fonts no)
(model "${KICAD10_3DMODEL_DIR}/Heatsink.3dshapes/Heatsink_AAVID_576802B03900G.step"
(offset
(xyz 0 0 0)
)
(scale
(xyz 1 1 1)
)
(rotate
(xyz 0 0 0)
)
)
)
File diff suppressed because it is too large. Load diff
+27 -8
View File
@@ -112,7 +112,7 @@
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "error",
"padstack": "warning",
"pth_inside_courtyard": "error",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "warning",
"silk_over_copper": "warning",
@@ -542,13 +542,32 @@
"microvia_drill": 0.1,
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 1,
"priority": 2,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 1.0,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.3,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Power3A",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 1.5,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
}
],
"meta": {
@@ -622,17 +641,17 @@
"pattern": "/Q6_C"
},
{
"netclass": "Power",
"pattern": "/GND"
},
{
"netclass": "Power",
"netclass": "Power3A",
"pattern": "/+12V"
},
{
"netclass": "Power",
"netclass": "Power3A",
"pattern": "/+12V_IN"
},
{
"netclass": "Power",
"pattern": "/GND"
},
{
"netclass": "Power",
"pattern": "/+12V_HV"
File diff suppressed because it is too large. Load diff
Binary file not shown.
+53 -2
View File
@@ -26,7 +26,8 @@ PASSES = int(sys.argv[1]) if len(sys.argv) > 1 else 100
# (routing clearance + half the width difference) from its neighbours. This gets 3 mm HV <-> LV
# and 1.3 mm HV <-> HV while tracks can still squeeze out between transistor pins.
# class: (routing width um, routing clearance um, final width mm)
ROUTE = {"kicad_default": (1800, 250, 0.4), "Power": (2400, 300, 1.0), "HV": (2100, 650, 0.6)}
ROUTE = {"kicad_default": (1800, 250, 0.4), "Power": (2400, 300, 1.0), "Power3A": (2900, 300, 1.5),
"HV": (2100, 650, 0.6)}
VIA_ROUTE_UM, VIA_FINAL_MM, VIA_DRILL_MM = 2300, 1.0, 0.5 # same trick for vias
HV_TO_LV_UM = 2450 # + (2.1 - 0.6) / 2 after shrinking = 3.2 mm
@@ -62,7 +63,7 @@ def add_connector_stubs(board, ref="J2", length=5.0):
"""
fp = board.FindFootprintByReference(ref)
# final widths: a router track must end on the stub copper itself, which still holds after shrinking
widths = {"HV": ROUTE["HV"][2], "Power": ROUTE["Power"][2]}
widths = {k: v[2] for k, v in ROUTE.items()}
for pad in fp.Pads():
net = pad.GetNet()
name = net.GetNetname() if net else ""
@@ -81,6 +82,55 @@ def add_connector_stubs(board, ref="J2", length=5.0):
board.Add(t)
def _closest_on_segment(p, a, b):
ax, ay, bx, by = a.x, a.y, b.x, b.y
dx, dy = bx - ax, by - ay
L2 = dx * dx + dy * dy
u = 0.0 if L2 == 0 else max(0.0, min(1.0, ((p.x - ax) * dx + (p.y - ay) * dy) / L2))
cx, cy = ax + u * dx, ay + u * dy
return int(round(cx)), int(round(cy)), ((p.x - cx) ** 2 + (p.y - cy) ** 2) ** 0.5
def heal_t_junctions(board, reach_mm=1.6):
"""After shrinking, a track that joined another track mid-way (a T-junction) can end beside
the narrowed copper. Snap such dangling ends onto the centre line of the same-net track."""
traces = [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_TRACE_T]
vias = [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_VIA_T]
pads = [p for f in board.GetFootprints() for p in f.Pads()]
reach = pcbnew.FromMM(reach_mm)
fixed = 0
for t in traces:
for is_start in (True, False):
p = t.GetStart() if is_start else t.GetEnd()
net, layer = t.GetNetCode(), t.GetLayer()
if any(pd.GetNetCode() == net and pd.IsOnLayer(layer) and pd.HitTest(p) for pd in pads):
continue
if any(v.GetNetCode() == net and v.GetPosition() == p for v in vias):
continue
best, touching = None, False
for o in traces:
if o is t or o.GetNetCode() != net or o.GetLayer() != layer:
continue
if p in (o.GetStart(), o.GetEnd()):
touching = True
break
cx, cy, d = _closest_on_segment(p, o.GetStart(), o.GetEnd())
if d <= o.GetWidth() / 2:
touching = True
break
if d <= reach and (best is None or d < best[2]):
best = (cx, cy, d)
if touching or best is None:
continue
q = pcbnew.VECTOR2I(best[0], best[1])
if is_start:
t.SetStart(q)
else:
t.SetEnd(q)
fixed += 1
return fixed
def unconnected(board):
board.BuildConnectivity()
return board.GetConnectivity().GetUnconnectedCount(False)
@@ -199,6 +249,7 @@ def main():
if not pcbnew.ImportSpecctraSES(board, str(ses)):
raise SystemExit("SES import failed")
shrink_tracks(board)
print("healed T-junctions:", heal_t_junctions(board))
remove_dangling_stubs(board)
add_gnd_plane(board)
pcbnew.ZONE_FILLER(board).Fill(board.Zones())