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
@@ -17,3 +17,12 @@ __pycache__/
|
||||
# KiCad generated reports
|
||||
hardware/kicad/erc.rpt
|
||||
*.net
|
||||
|
||||
# Local tools (Freerouting jar)
|
||||
tools/
|
||||
|
||||
# KiCad local history
|
||||
.history/
|
||||
|
||||
# Freerouting work files
|
||||
hardware/kicad/route/
|
||||
@@ -20,7 +20,7 @@ Stern/Sega display PSU (520-5138-00). The original transformer and AC front end
|
||||
|---|---|
|
||||
| [docs/power-supply.md](docs/power-supply.md) | PSU design: block diagram, analysis of the original board, changes |
|
||||
| [hardware/psu-schematic.svg](hardware/psu-schematic.svg) | PSU schematic (generated by [hardware/psu_schematic.py](hardware/psu_schematic.py)) |
|
||||
| [hardware/kicad/](hardware/kicad/) | KiCad 10 project for the PSU carrier board (schematic + net classes, PCB not started) |
|
||||
| [hardware/kicad/](hardware/kicad/) | KiCad 10 project for the PSU carrier board: schematic and routed PCB (rev 0.1) |
|
||||
| [docs/safety.md](docs/safety.md) | Safety design (HV watchdog, fusing, layout, bring-up procedure) |
|
||||
| [docs/esp32-interface.md](docs/esp32-interface.md) | ESP32 pin plan, 74HCT245 level shifter, panel signals |
|
||||
| [docs/bom.md](docs/bom.md) | Bill of materials |
|
||||
@@ -32,7 +32,8 @@ Stern/Sega display PSU (520-5138-00). The original transformer and AC front end
|
||||
- [ ] Identify panel: manufacturer/part number from its label
|
||||
- [ ] Map the panel's power connector pins (WPC panel ≠ Stern CN2 pinout) and check if +12 V is used
|
||||
- [ ] Map the panel's 14-pin data connector
|
||||
- [ ] Lay out the PSU PCB in KiCad ([hardware/kicad/](hardware/kicad/README.md))
|
||||
- [x] Lay out the PSU PCB in KiCad ([hardware/kicad/](hardware/kicad/README.md))
|
||||
- [ ] Review the PCB and work through the [before ordering](hardware/kicad/README.md#before-ordering) checklist
|
||||
- [ ] Order parts ([docs/bom.md](docs/bom.md))
|
||||
- [ ] Build and bench-test the PSU **without the panel** ([bring-up](docs/safety.md#bring-up-procedure))
|
||||
- [ ] Build HV watchdog and verify that the relay drops when toggling stops
|
||||
|
||||
+57
-10
@@ -42,24 +42,71 @@ 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 | **3.0 mm** | 0.6 mm |
|
||||
| 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 |
|
||||
| Default | Watchdog signals | 0.25 mm | 0.4 mm |
|
||||
| Default | Watchdog signals, LED cathode/anode nodes | 0.25 mm | 0.4 mm |
|
||||
|
||||
## Making the PCB
|
||||
## PCB (rev 0.1)
|
||||
|
||||
1. Open the PCB editor and choose **Tools → Update PCB from Schematic** (F8). All footprints arrive with ratsnest.
|
||||
2. Draw the board outline on `Edge.Cuts`.
|
||||
3. Placement tips:
|
||||
- Keep the HV area (J5/J7, F4/F5, regulator, J2) on one side and the 12 V/5 V/watchdog on the other.
|
||||
- Leave room for TO-220 heatsinks on Q3, Q5 and Q6. R13 (5 W) runs warm.
|
||||
- Consider routed slots between the HV and low-voltage areas.
|
||||
4. Run DRC. The HV class enforces 3 mm to everything.
|
||||

|
||||
|
||||
The board is 170 × 125 mm, 2 layers, with 4 × M3 holes. It is fully routed with a bottom-layer GND pour.
|
||||
DRC is clean: 0 violations, 0 unconnected pads, schematic parity OK.
|
||||
|
||||
- **Left: low voltage.** 12 V input, Mini560, ESP32 watchdog, relay, F1–F3.
|
||||
- **Right: high voltage.** +65 V regulator at the top, −112/−100 V regulator at the bottom.
|
||||
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.
|
||||
|
||||
> ⚠️ **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.
|
||||
|
||||
### Clearance rules
|
||||
|
||||
The classes are in the project, and the custom rules are in `plasma_dmd_psu.kicad_dru`. Later rules win.
|
||||
|
||||
| Between | Clearance | Why |
|
||||
|---|---|---|
|
||||
| HV and LV (GND, 12 V, 5 V, watchdog) | **3.0 mm** | Keeps HV well away from the touchable side |
|
||||
| HV and HV | 1.3 mm | IPC-2221 B2 needs 1.25 mm up to 300 V |
|
||||
| HV and LV, next to a connector pin | 1.8 mm | Fixed pin pitch of J2/J5/J7 |
|
||||
| HV and HV, next to a transistor pin | 1.0 mm | TO-220 pin escape; same-polarity nets only |
|
||||
| Pins inside one connector / transistor / HV electrolytic | 0.75 / 0.6 / 2.5 mm | Fixed footprint pitch |
|
||||
|
||||
### Regenerating the board
|
||||
|
||||
The layout is produced by two scripts, run with KiCad's Python while KiCad is closed:
|
||||
|
||||
```bash
|
||||
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/layout_pcb.py
|
||||
"C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/python.exe" hardware/kicad/route_pcb.py
|
||||
```
|
||||
|
||||
- `layout_pcb.py` places every footprint from its `PLACE` table and draws the outline, holes and silkscreen.
|
||||
It also swaps in any footprint that changed in the schematic. **It deletes all tracks and zones.**
|
||||
- `route_pcb.py` autoroutes with Freerouting (`tools/freerouting/`, needs Java 25):
|
||||
- It routes with oversized tracks and vias, then shrinks them to their real width. This turns
|
||||
the router's smaller clearances into 3 mm for HV to LV and 1.3 mm for HV to HV.
|
||||
- It adds fixed escape stubs on J2 and the GND pour.
|
||||
- Run DRC afterwards: `kicad-cli pcb drc --schematic-parity --refill-zones hardware/kicad/plasma_dmd_psu.kicad_pcb`.
|
||||
|
||||
Once you start hand-editing the board in KiCad, stop running these scripts. They would overwrite your edits.
|
||||
|
||||
### Before ordering
|
||||
|
||||
- [ ] 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.
|
||||
- [ ] Consider conformal coating on the HV side.
|
||||
- [ ] Get a second pair of eyes on the HV layout.
|
||||
|
||||
## Verified so far
|
||||
|
||||
- The schematic loads in KiCad 10.0.6, and ERC reports 0 violations.
|
||||
- `check_netlist.py`: all 122 pins sit on the intended nets (31 nets).
|
||||
- PCB DRC with schematic parity: 0 violations, 0 unconnected pads.
|
||||
- All symbols and footprints come from the KiCad 10 standard libraries.
|
||||
- Transistor pinouts match the footprints: MJE15030/31 are B-C-E (TO-220); MPSA42/92 are E-B-C (TO-92); 2N7000 is S-G-D.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ FP_D35 = "Diode_THT:D_DO-35_SOD27_P7.62mm_Horizontal"
|
||||
FP_D41 = "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal"
|
||||
FP_D15 = "Diode_THT:D_DO-15_P12.70mm_Horizontal"
|
||||
FP_LED = "LED_THT:LED_D5.0mm"
|
||||
FP_TO92 = "Package_TO_SOT_THT:TO-92_Inline"
|
||||
FP_TO92 = "Package_TO_SOT_THT:TO-92_Inline_Wide" # 2.54 mm pitch for HV clearance
|
||||
FP_TO220 = "Package_TO_SOT_THT:TO-220-3_Vertical"
|
||||
FP_FUSE = "Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open"
|
||||
FP_CFILM = "Capacitor_THT:C_Rect_L18.0mm_W5.0mm_P15.00mm_FKS3_FKP3"
|
||||
@@ -213,8 +213,9 @@ def snap(v):
|
||||
return round(round(v / 1.27) * 1.27, 2)
|
||||
|
||||
|
||||
HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B", "N_LED1",
|
||||
"-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C", "N_LED2"]
|
||||
# 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"]
|
||||
|
||||
|
||||
@@ -236,7 +237,7 @@ def write_project():
|
||||
"classes": [
|
||||
netclass("Default", 0.25, 0.4, 2147483647),
|
||||
netclass("Power", 0.3, 1.0, 1),
|
||||
netclass("HV", 3.0, 0.6, 0),
|
||||
netclass("HV", 1.3, 0.6, 0) # HV <-> LV 3 mm comes from plasma_dmd_psu.kicad_dru,
|
||||
],
|
||||
"meta": {"version": 5},
|
||||
"net_colors": None,
|
||||
@@ -247,8 +248,26 @@ def write_project():
|
||||
pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def existing_uuids():
|
||||
path = HERE / f"{PROJECT}.kicad_sch"
|
||||
if not path.exists():
|
||||
return None, {}
|
||||
text = path.read_text(encoding="utf-8")
|
||||
root = re.search(r'^\(kicad_sch.*?\(uuid "([^"]+)"\)', text, re.S).group(1)
|
||||
syms = {}
|
||||
for m in re.finditer(r'^ \(symbol \(lib_id', text, re.M):
|
||||
blk = read_sexpr_block(text, m.start() + 2)
|
||||
u = re.search(r'\(dnp no\) \(uuid "([^"]+)"\)', blk)
|
||||
r = re.search(r'\(property "Reference" "([^"]+)"', blk)
|
||||
if u and r:
|
||||
syms[r.group(1)] = u.group(1)
|
||||
return root, syms
|
||||
|
||||
|
||||
def main():
|
||||
root = uid()
|
||||
# Reuse UUIDs from an existing schematic so the PCB keeps its links to the symbols.
|
||||
old_root, old_sym = existing_uuids()
|
||||
root = old_root or uid()
|
||||
lib_blocks, pin_cache = {}, {}
|
||||
for p in PARTS:
|
||||
if p[1] not in lib_blocks:
|
||||
@@ -273,7 +292,7 @@ def main():
|
||||
rx, ry, vx, vy, just = snap(x + 5.08), snap(y - 1.27), snap(x + 5.08), snap(y + 1.27), " (justify left)"
|
||||
pin_uuid = "\n".join(f' (pin {q(num)} (uuid {q(uid())}))' for num, *_ in pins)
|
||||
items.append(f""" (symbol (lib_id {q(lib_id)}) (at {x} {y} 0) (unit 1)
|
||||
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(uid())})
|
||||
(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {q(old_sym.get(ref) or uid())})
|
||||
(property "Reference" {q(ref)} (at {rx} {ry} 0) (effects (font (size 1.27 1.27)){just}))
|
||||
(property "Value" {q(value)} (at {vx} {vy} 0) (effects (font (size 1.27 1.27)){just}))
|
||||
(property "Footprint" {q(fp)} (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
||||
|
||||
@@ -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()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 941 KiB |
@@ -0,0 +1,35 @@
|
||||
(version 1)
|
||||
|
||||
# Clearance policy for the PSU carrier board (later rules take priority).
|
||||
# IPC-2221 B2 (external, uncoated) needs 1.25 mm up to 300 V; we add margin:
|
||||
# - HV <-> anything else: 3.0 mm (keeps HV well away from the 12 V / 5 V / ESP32 side)
|
||||
# - HV <-> HV: 1.3 mm (netclass default; up to about 220 V between +90 V and -130 V)
|
||||
# - HV <-> LV next to a connector pin: 1.8 mm (tracks leaving J2/J5/J7 pins that sit next to other-class pins)
|
||||
# - inside connectors J*: 0.75 mm (fixed pin pitch; adjacent HV/LV pins differ by <= 53 V at J2, J5/J7 have 2.4 mm)
|
||||
# - inside the HV electrolytics: 2.5 mm (5 mm pin pitch, 90/130 V)
|
||||
# - HV <-> HV next to a transistor pin: 1.0 mm (TO-220 pin escape; each transistor only has same-polarity nets around it)
|
||||
# - inside transistors Q*: 0.6 mm (TO-92 / TO-220 pitch; pins of one transistor differ by < 100 V)
|
||||
|
||||
(rule "HV to low voltage"
|
||||
(condition "A.hasNetclass('HV') && !B.hasNetclass('HV')")
|
||||
(constraint clearance (min 3.0mm)))
|
||||
|
||||
(rule "HV next to connector pins"
|
||||
(condition "A.hasNetclass('HV') && !B.hasNetclass('HV') && (A.memberOfFootprint('J*') || B.memberOfFootprint('J*'))")
|
||||
(constraint clearance (min 1.8mm)))
|
||||
|
||||
(rule "HV next to transistor pins"
|
||||
(condition "A.hasNetclass('HV') && B.hasNetclass('HV') && (A.memberOfFootprint('Q*') || B.memberOfFootprint('Q*'))")
|
||||
(constraint clearance (min 1.0mm)))
|
||||
|
||||
(rule "HV inside connectors"
|
||||
(condition "A.hasNetclass('HV') && A.memberOfFootprint('J*') && B.memberOfFootprint('J*')")
|
||||
(constraint clearance (min 0.75mm)))
|
||||
|
||||
(rule "HV inside transistors"
|
||||
(condition "A.hasNetclass('HV') && B.hasNetclass('HV') && A.memberOfFootprint('Q*') && B.memberOfFootprint('Q*')")
|
||||
(constraint clearance (min 0.6mm)))
|
||||
|
||||
(rule "HV electrolytic pins"
|
||||
(condition "(A.memberOfFootprint('C5') && B.memberOfFootprint('C5')) || (A.memberOfFootprint('C7') && B.memberOfFootprint('C7'))")
|
||||
(constraint clearance (min 2.5mm)))
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,4 +1,492 @@
|
||||
{
|
||||
"board": {
|
||||
"3dviewports": [],
|
||||
"design_settings": {
|
||||
"defaults": {
|
||||
"apply_defaults_to_fp_barcodes": false,
|
||||
"apply_defaults_to_fp_dimensions": false,
|
||||
"apply_defaults_to_fp_fields": false,
|
||||
"apply_defaults_to_fp_shapes": false,
|
||||
"apply_defaults_to_fp_text": false,
|
||||
"board_outline_line_width": 0.05,
|
||||
"copper_line_width": 0.2,
|
||||
"copper_text_italic": false,
|
||||
"copper_text_size_h": 1.5,
|
||||
"copper_text_size_v": 1.5,
|
||||
"copper_text_thickness": 0.3,
|
||||
"copper_text_upright": false,
|
||||
"courtyard_line_width": 0.05,
|
||||
"dimension_precision": 4,
|
||||
"dimension_units": 3,
|
||||
"dimensions": {
|
||||
"arrow_length": 1270000,
|
||||
"extension_offset": 500000,
|
||||
"keep_text_aligned": true,
|
||||
"suppress_zeroes": true,
|
||||
"text_position": 0,
|
||||
"units_format": 0
|
||||
},
|
||||
"fab_line_width": 0.1,
|
||||
"fab_text_italic": false,
|
||||
"fab_text_size_h": 1.0,
|
||||
"fab_text_size_v": 1.0,
|
||||
"fab_text_thickness": 0.15,
|
||||
"fab_text_upright": false,
|
||||
"other_line_width": 0.1,
|
||||
"other_text_italic": false,
|
||||
"other_text_size_h": 1.0,
|
||||
"other_text_size_v": 1.0,
|
||||
"other_text_thickness": 0.15,
|
||||
"other_text_upright": false,
|
||||
"pads": {
|
||||
"drill": 0.8,
|
||||
"height": 1.27,
|
||||
"width": 1.27
|
||||
},
|
||||
"silk_line_width": 0.1,
|
||||
"silk_text_italic": false,
|
||||
"silk_text_size_h": 1.0,
|
||||
"silk_text_size_v": 1.0,
|
||||
"silk_text_thickness": 0.1,
|
||||
"silk_text_upright": false,
|
||||
"zones": {
|
||||
"border_display_style": 2,
|
||||
"border_hatch_pitch": 0.5,
|
||||
"corner_radius": 0.0,
|
||||
"corner_smoothing": 0,
|
||||
"fill_mode": 0,
|
||||
"hatch_gap": 1.5,
|
||||
"hatch_orientation": 0.0,
|
||||
"hatch_smoothing_level": 0,
|
||||
"hatch_smoothing_value": 0.1,
|
||||
"hatch_thickness": 1.0,
|
||||
"min_clearance": 0.5,
|
||||
"min_island_area": 10.0,
|
||||
"min_thickness": 0.25,
|
||||
"pad_connection": 1,
|
||||
"remove_islands": 0,
|
||||
"thermal_relief_gap": 0.5,
|
||||
"thermal_relief_spoke_width": 0.5
|
||||
}
|
||||
},
|
||||
"diff_pair_dimensions": [],
|
||||
"drc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 2
|
||||
},
|
||||
"rule_severities": {
|
||||
"annular_width": "error",
|
||||
"clearance": "error",
|
||||
"connection_width": "warning",
|
||||
"copper_edge_clearance": "error",
|
||||
"copper_sliver": "warning",
|
||||
"courtyards_overlap": "error",
|
||||
"creepage": "error",
|
||||
"diff_pair_gap_out_of_range": "error",
|
||||
"diff_pair_uncoupled_length_too_long": "error",
|
||||
"drill_out_of_range": "error",
|
||||
"duplicate_footprints": "warning",
|
||||
"extra_footprint": "warning",
|
||||
"footprint": "error",
|
||||
"footprint_filters_mismatch": "ignore",
|
||||
"footprint_symbol_field_mismatch": "warning",
|
||||
"footprint_symbol_mismatch": "warning",
|
||||
"footprint_type_mismatch": "ignore",
|
||||
"hole_clearance": "error",
|
||||
"hole_to_hole": "warning",
|
||||
"holes_co_located": "warning",
|
||||
"invalid_outline": "error",
|
||||
"isolated_copper": "warning",
|
||||
"item_on_disabled_layer": "error",
|
||||
"items_not_allowed": "error",
|
||||
"length_out_of_range": "error",
|
||||
"lib_footprint_issues": "warning",
|
||||
"lib_footprint_mismatch": "warning",
|
||||
"malformed_courtyard": "error",
|
||||
"microvia_drill_out_of_range": "error",
|
||||
"mirrored_text_on_front_layer": "warning",
|
||||
"missing_courtyard": "ignore",
|
||||
"missing_footprint": "warning",
|
||||
"missing_tuning_profile": "warning",
|
||||
"net_conflict": "warning",
|
||||
"nonmirrored_text_on_back_layer": "warning",
|
||||
"npth_inside_courtyard": "error",
|
||||
"padstack": "warning",
|
||||
"pth_inside_courtyard": "error",
|
||||
"shorting_items": "error",
|
||||
"silk_edge_clearance": "warning",
|
||||
"silk_over_copper": "warning",
|
||||
"silk_overlap": "warning",
|
||||
"skew_out_of_range": "error",
|
||||
"solder_mask_bridge": "error",
|
||||
"starved_thermal": "error",
|
||||
"text_height": "warning",
|
||||
"text_on_edge_cuts": "error",
|
||||
"text_thickness": "warning",
|
||||
"through_hole_pad_without_hole": "error",
|
||||
"too_many_vias": "error",
|
||||
"track_angle": "error",
|
||||
"track_dangling": "warning",
|
||||
"track_not_centered_on_via": "ignore",
|
||||
"track_on_post_machined_layer": "error",
|
||||
"track_segment_length": "error",
|
||||
"track_width": "error",
|
||||
"tracks_crossing": "error",
|
||||
"tuning_profile_track_geometries": "ignore",
|
||||
"unconnected_items": "error",
|
||||
"unresolved_variable": "error",
|
||||
"via_dangling": "warning",
|
||||
"zones_intersect": "error"
|
||||
},
|
||||
"rules": {
|
||||
"max_error": 0.005,
|
||||
"min_clearance": 0.0,
|
||||
"min_connection": 0.0,
|
||||
"min_copper_edge_clearance": 0.5,
|
||||
"min_groove_width": 0.0,
|
||||
"min_hole_clearance": 0.25,
|
||||
"min_hole_to_hole": 0.25,
|
||||
"min_microvia_diameter": 0.2,
|
||||
"min_microvia_drill": 0.1,
|
||||
"min_resolved_spokes": 2,
|
||||
"min_silk_clearance": 0.0,
|
||||
"min_text_height": 0.8,
|
||||
"min_text_thickness": 0.08,
|
||||
"min_through_hole_diameter": 0.3,
|
||||
"min_track_width": 0.2,
|
||||
"min_via_annular_width": 0.1,
|
||||
"min_via_diameter": 0.5,
|
||||
"solder_mask_to_copper_clearance": 0.0,
|
||||
"use_height_for_length_calcs": true
|
||||
},
|
||||
"teardrop_options": [
|
||||
{
|
||||
"td_onpthpad": true,
|
||||
"td_onroundshapesonly": false,
|
||||
"td_onsmdpad": true,
|
||||
"td_ontrackend": false,
|
||||
"td_onvia": true
|
||||
}
|
||||
],
|
||||
"teardrop_parameters": [
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_round_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_rect_shape",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
},
|
||||
{
|
||||
"td_allow_use_two_tracks": true,
|
||||
"td_curve_segcount": 0,
|
||||
"td_height_ratio": 1.0,
|
||||
"td_length_ratio": 0.5,
|
||||
"td_maxheight": 2.0,
|
||||
"td_maxlen": 1.0,
|
||||
"td_on_pad_in_zone": false,
|
||||
"td_target_name": "td_track_end",
|
||||
"td_width_to_size_filter_ratio": 0.9
|
||||
}
|
||||
],
|
||||
"track_widths": [],
|
||||
"tuning_pattern_settings": {
|
||||
"diff_pair_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 1.0
|
||||
},
|
||||
"diff_pair_skew_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
},
|
||||
"single_track_defaults": {
|
||||
"corner_radius_percentage": 80,
|
||||
"corner_style": 1,
|
||||
"max_amplitude": 1.0,
|
||||
"min_amplitude": 0.2,
|
||||
"single_sided": false,
|
||||
"spacing": 0.6
|
||||
}
|
||||
},
|
||||
"via_dimensions": [],
|
||||
"zones_allow_external_fillets": false
|
||||
},
|
||||
"ipc2581": {
|
||||
"bom_rev": "",
|
||||
"dist": "",
|
||||
"distpn": "",
|
||||
"internal_id": "",
|
||||
"mfg": "",
|
||||
"mpn": "",
|
||||
"sch_revision": ""
|
||||
},
|
||||
"layer_pairs": [],
|
||||
"layer_presets": [],
|
||||
"viewports": []
|
||||
},
|
||||
"boards": [],
|
||||
"component_class_settings": {
|
||||
"assignments": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"sheet_component_classes": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"cvpcb": {
|
||||
"equivalence_files": []
|
||||
},
|
||||
"erc": {
|
||||
"erc_exclusions": [],
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"pin_map": [
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
0,
|
||||
2,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
0,
|
||||
0,
|
||||
2
|
||||
],
|
||||
[
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2,
|
||||
2
|
||||
]
|
||||
],
|
||||
"rule_severities": {
|
||||
"bus_definition_conflict": "error",
|
||||
"bus_entry_needed": "error",
|
||||
"bus_to_bus_conflict": "error",
|
||||
"bus_to_net_conflict": "error",
|
||||
"different_unit_footprint": "error",
|
||||
"different_unit_net": "error",
|
||||
"duplicate_reference": "error",
|
||||
"duplicate_sheet_names": "error",
|
||||
"endpoint_off_grid": "warning",
|
||||
"extra_units": "error",
|
||||
"field_name_whitespace": "warning",
|
||||
"footprint_filter": "ignore",
|
||||
"footprint_link_issues": "warning",
|
||||
"four_way_junction": "ignore",
|
||||
"ground_pin_not_ground": "warning",
|
||||
"hier_label_mismatch": "error",
|
||||
"isolated_pin_label": "warning",
|
||||
"label_dangling": "error",
|
||||
"label_multiple_wires": "warning",
|
||||
"lib_symbol_issues": "warning",
|
||||
"lib_symbol_mismatch": "warning",
|
||||
"missing_bidi_pin": "warning",
|
||||
"missing_input_pin": "warning",
|
||||
"missing_power_pin": "error",
|
||||
"missing_unit": "warning",
|
||||
"multiple_net_names": "warning",
|
||||
"net_not_bus_member": "warning",
|
||||
"no_connect_connected": "warning",
|
||||
"no_connect_dangling": "warning",
|
||||
"pin_not_connected": "error",
|
||||
"pin_not_driven": "error",
|
||||
"pin_to_pin": "warning",
|
||||
"power_pin_not_driven": "error",
|
||||
"same_local_global_label": "warning",
|
||||
"similar_label_and_power": "warning",
|
||||
"similar_labels": "warning",
|
||||
"similar_power": "warning",
|
||||
"simulation_model_issue": "ignore",
|
||||
"single_global_label": "ignore",
|
||||
"stacked_pin_name": "warning",
|
||||
"unannotated": "error",
|
||||
"unconnected_wire_endpoint": "warning",
|
||||
"undefined_netclass": "error",
|
||||
"unit_value_mismatch": "error",
|
||||
"unresolved_variable": "error",
|
||||
"wire_dangling": "error"
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"pinned_footprint_libs": [],
|
||||
"pinned_symbol_libs": []
|
||||
},
|
||||
"meta": {
|
||||
"filename": "plasma_dmd_psu.kicad_pro",
|
||||
"version": 3
|
||||
@@ -24,6 +512,25 @@
|
||||
"via_drill": 0.5,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 1.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": "HV",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.6,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 1.0,
|
||||
"via_drill": 0.5,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 0.3,
|
||||
@@ -42,25 +549,6 @@
|
||||
"via_diameter": 1.0,
|
||||
"via_drill": 0.5,
|
||||
"wire_width": 6
|
||||
},
|
||||
{
|
||||
"bus_width": 12,
|
||||
"clearance": 3.0,
|
||||
"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": "HV",
|
||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||
"priority": 0,
|
||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||
"track_width": 0.6,
|
||||
"tuning_profile": "",
|
||||
"via_diameter": 1.0,
|
||||
"via_drill": 0.5,
|
||||
"wire_width": 6
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
@@ -93,10 +581,6 @@
|
||||
"netclass": "HV",
|
||||
"pattern": "/Q3_B"
|
||||
},
|
||||
{
|
||||
"netclass": "HV",
|
||||
"pattern": "/N_LED1"
|
||||
},
|
||||
{
|
||||
"netclass": "HV",
|
||||
"pattern": "/-130V_RAW"
|
||||
@@ -137,10 +621,6 @@
|
||||
"netclass": "HV",
|
||||
"pattern": "/Q6_C"
|
||||
},
|
||||
{
|
||||
"netclass": "HV",
|
||||
"pattern": "/N_LED2"
|
||||
},
|
||||
{
|
||||
"netclass": "Power",
|
||||
"pattern": "/GND"
|
||||
@@ -174,5 +654,157 @@
|
||||
"pattern": "/+12V_PANEL"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pcbnew": {
|
||||
"last_paths": {
|
||||
"idf": "",
|
||||
"netlist": "",
|
||||
"plot": "",
|
||||
"specctra_dsn": "",
|
||||
"step": "",
|
||||
"vrml": ""
|
||||
},
|
||||
"page_layout_descr_file": ""
|
||||
},
|
||||
"schematic": {
|
||||
"annotate_start_num": 0,
|
||||
"annotation": {
|
||||
"method": 0,
|
||||
"sort_order": 0
|
||||
},
|
||||
"bom_export_filename": "${PROJECTNAME}.csv",
|
||||
"bom_fmt_presets": [],
|
||||
"bom_fmt_settings": {
|
||||
"field_delimiter": ",",
|
||||
"keep_line_breaks": false,
|
||||
"keep_tabs": false,
|
||||
"name": "CSV",
|
||||
"ref_delimiter": ",",
|
||||
"ref_range_delimiter": "",
|
||||
"string_delimiter": "\""
|
||||
},
|
||||
"bom_presets": [],
|
||||
"bom_settings": {
|
||||
"exclude_dnp": false,
|
||||
"fields_ordered": [
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Reference",
|
||||
"name": "Reference",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Qty",
|
||||
"name": "${QUANTITY}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Value",
|
||||
"name": "Value",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "DNP",
|
||||
"name": "${DNP}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from BOM",
|
||||
"name": "${EXCLUDE_FROM_BOM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Board",
|
||||
"name": "${EXCLUDE_FROM_BOARD}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Simulation",
|
||||
"name": "${EXCLUDE_FROM_SIM}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Exclude from Position Files",
|
||||
"name": "${EXCLUDE_FROM_POS_FILES}",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": true,
|
||||
"label": "Footprint",
|
||||
"name": "Footprint",
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"group_by": false,
|
||||
"label": "Datasheet",
|
||||
"name": "Datasheet",
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"filter_string": "",
|
||||
"group_symbols": true,
|
||||
"include_excluded_from_bom": true,
|
||||
"name": "Default Editing",
|
||||
"sort_asc": true,
|
||||
"sort_field": "Reference"
|
||||
},
|
||||
"bus_aliases": {},
|
||||
"connection_grid_size": 50.0,
|
||||
"drawing": {
|
||||
"dashed_lines_dash_length_ratio": 12.0,
|
||||
"dashed_lines_gap_length_ratio": 3.0,
|
||||
"default_line_thickness": 6.0,
|
||||
"default_text_size": 50.0,
|
||||
"field_names": [],
|
||||
"hop_over_size_choice": 0,
|
||||
"intersheets_ref_own_page": false,
|
||||
"intersheets_ref_prefix": "",
|
||||
"intersheets_ref_short": false,
|
||||
"intersheets_ref_show": false,
|
||||
"intersheets_ref_suffix": "",
|
||||
"junction_size_choice": 3,
|
||||
"label_size_ratio": 0.375,
|
||||
"operating_point_overlay_i_precision": 3,
|
||||
"operating_point_overlay_i_range": "~A",
|
||||
"operating_point_overlay_v_precision": 3,
|
||||
"operating_point_overlay_v_range": "~V",
|
||||
"overbar_offset_ratio": 1.23,
|
||||
"pin_symbol_size": 25.0,
|
||||
"text_offset_ratio": 0.15
|
||||
},
|
||||
"legacy_lib_dir": "",
|
||||
"legacy_lib_list": [],
|
||||
"meta": {
|
||||
"version": 1
|
||||
},
|
||||
"page_layout_descr_file": "",
|
||||
"plot_directory": "",
|
||||
"reuse_designators": true,
|
||||
"subpart_first_id": 65,
|
||||
"subpart_id_separator": 0,
|
||||
"top_level_sheets": [
|
||||
{
|
||||
"filename": "plasma_dmd_psu.kicad_sch",
|
||||
"name": "plasma_dmd_psu",
|
||||
"uuid": "00000000-0000-0000-0000-000000000000"
|
||||
}
|
||||
],
|
||||
"used_designators": "",
|
||||
"variants": []
|
||||
},
|
||||
"sheets": [],
|
||||
"text_variables": {},
|
||||
"tuning_profiles": {
|
||||
"meta": {
|
||||
"version": 0
|
||||
},
|
||||
"tuning_profiles_impedance_geometric": []
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,210 @@
|
||||
"""Autoroute the PSU carrier board with Freerouting.
|
||||
|
||||
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/route_pcb.py
|
||||
|
||||
Exports a Specctra DSN, adds class-to-class clearance (HV <-> everything else 3 mm, matching
|
||||
plasma_dmd_psu.kicad_dru, which Freerouting does not read), routes, and imports the session.
|
||||
Existing tracks are removed first, so the result always comes from the current placement.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pcbnew
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
ROOT = HERE.parents[1]
|
||||
BOARD = HERE / "plasma_dmd_psu.kicad_pcb"
|
||||
WORK = HERE / "route"
|
||||
JAVA = Path("C:/Program Files/Eclipse Adoptium/jre-25.0.4.101-hotspot/bin/java.exe")
|
||||
JAR = ROOT / "tools/freerouting/freerouting-2.5.0.jar"
|
||||
PASSES = int(sys.argv[1]) if len(sys.argv) > 1 else 100
|
||||
|
||||
# Route with oversized tracks, then shrink them to the real width: every track then keeps
|
||||
# (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)}
|
||||
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
|
||||
|
||||
|
||||
def add_gnd_plane(board):
|
||||
"""Bottom-layer GND pour over the whole board (custom rules keep it 3 mm from HV)."""
|
||||
for z in list(board.Zones()):
|
||||
board.Delete(z)
|
||||
bb = board.GetBoardEdgesBoundingBox()
|
||||
inset = pcbnew.FromMM(0.5)
|
||||
x0, y0 = bb.GetX() + inset, bb.GetY() + inset
|
||||
x1, y1 = bb.GetRight() - inset, bb.GetBottom() - inset
|
||||
z = pcbnew.ZONE(board)
|
||||
z.SetLayer(pcbnew.B_Cu)
|
||||
z.SetNet(board.FindNet("/GND"))
|
||||
z.SetLocalClearance(pcbnew.FromMM(0.5))
|
||||
z.SetMinThickness(pcbnew.FromMM(0.5))
|
||||
z.SetPadConnection(pcbnew.ZONE_CONNECTION_THERMAL)
|
||||
z.SetThermalReliefGap(pcbnew.FromMM(0.5))
|
||||
z.SetThermalReliefSpokeWidth(pcbnew.FromMM(0.8))
|
||||
ol = z.Outline()
|
||||
ol.NewOutline()
|
||||
for x, y in ((x0, y0), (x1, y0), (x1, y1), (x0, y1)):
|
||||
ol.Append(x, y)
|
||||
board.Add(z)
|
||||
|
||||
|
||||
def add_connector_stubs(board, ref="J2", length=5.0):
|
||||
"""Fixed escape stubs on the panel connector: LV pins exit left, HV pins exit right.
|
||||
|
||||
Its 3.96 mm pitch is tighter than the router's HV <-> LV clearance, so the router could not
|
||||
leave those pins by itself. GND pins connect to the bottom plane and need no stub.
|
||||
"""
|
||||
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]}
|
||||
for pad in fp.Pads():
|
||||
net = pad.GetNet()
|
||||
name = net.GetNetname() if net else ""
|
||||
if not name or name == "/GND" or name.startswith("unconnected"):
|
||||
continue
|
||||
cls = pad.GetNetClassName()
|
||||
sign = 1 if cls == "HV" else -1
|
||||
p = pad.GetPosition()
|
||||
t = pcbnew.PCB_TRACK(board)
|
||||
t.SetStart(p)
|
||||
t.SetEnd(pcbnew.VECTOR2I(p.x + sign * pcbnew.FromMM(length), p.y))
|
||||
t.SetLayer(pcbnew.F_Cu)
|
||||
t.SetWidth(pcbnew.FromMM(widths.get(cls, ROUTE["kicad_default"][2])))
|
||||
t.SetNet(net)
|
||||
t.SetLocked(True)
|
||||
board.Add(t)
|
||||
|
||||
|
||||
def unconnected(board):
|
||||
board.BuildConnectivity()
|
||||
return board.GetConnectivity().GetUnconnectedCount(False)
|
||||
|
||||
|
||||
def remove_dangling_stubs(board):
|
||||
"""Drop duplicate escape stubs (the SES brings them back), then delete each stub whose
|
||||
removal does not leave anything unconnected (the router may join a stub anywhere along it)."""
|
||||
seen = set()
|
||||
for t in [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_TRACE_T and t.IsLocked()]:
|
||||
key = (t.GetNetCode(), t.GetLayer(), t.GetStart().x, t.GetStart().y, t.GetEnd().x, t.GetEnd().y)
|
||||
if key in seen:
|
||||
board.Delete(t)
|
||||
seen.add(key)
|
||||
base = unconnected(board)
|
||||
for t in [t for t in board.GetTracks() if t.Type() == pcbnew.PCB_TRACE_T and t.IsLocked()]:
|
||||
s, e = t.GetStart(), t.GetEnd() # copy values: t is freed by Delete
|
||||
keep = (s.x, s.y, e.x, e.y, t.GetLayer(), t.GetWidth(), t.GetNetCode())
|
||||
board.Delete(t)
|
||||
if unconnected(board) > base: # it was needed: put it back
|
||||
n = pcbnew.PCB_TRACK(board)
|
||||
n.SetStart(pcbnew.VECTOR2I(keep[0], keep[1])); n.SetEnd(pcbnew.VECTOR2I(keep[2], keep[3]))
|
||||
n.SetLayer(keep[4]); n.SetWidth(keep[5])
|
||||
board.Add(n)
|
||||
n.SetNet(board.FindNet(keep[6])); n.SetLocked(True)
|
||||
|
||||
|
||||
def patch_via_padstacks(text):
|
||||
"""Enlarge the via pads in the DSN library (vias are set back to their real size after import)."""
|
||||
out = []
|
||||
in_via = False
|
||||
for line in text.splitlines(keepends=True):
|
||||
if line.lstrip().startswith('(padstack "Via'):
|
||||
in_via = True
|
||||
elif in_via and line.lstrip().startswith("(shape (circle"):
|
||||
line = re.sub(r"(\(circle \S+ )[\d.]+", lambda m: m.group(1) + str(VIA_ROUTE_UM), line)
|
||||
elif in_via and line.strip() == ")":
|
||||
in_via = False
|
||||
out.append(line)
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def patch_class_rules(text):
|
||||
"""Replace each class's width/clearance in the DSN with the routing values."""
|
||||
out, pos = [], 0
|
||||
for m in re.finditer(r"\(class (\S+) ", text):
|
||||
name = m.group(1)
|
||||
start = m.start()
|
||||
rule = text.index("(rule", start)
|
||||
end = text.index(")", text.index("(clearance", rule)) + 1
|
||||
width, clear, _ = ROUTE[name]
|
||||
block = text[rule:end]
|
||||
block = re.sub(r"\(width [\d.]+\)", f"(width {width})", block)
|
||||
block = re.sub(r"\(clearance [\d.]+\)", f"(clearance {clear})", block, count=1)
|
||||
out.append(text[pos:rule] + block)
|
||||
pos = end
|
||||
out.append(text[pos:])
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def shrink_tracks(board):
|
||||
final = {("Default" if k == "kicad_default" else k): pcbnew.FromMM(v[2]) for k, v in ROUTE.items()}
|
||||
for t in board.GetTracks():
|
||||
if t.Type() == pcbnew.PCB_VIA_T:
|
||||
t.SetWidth(pcbnew.FromMM(VIA_FINAL_MM))
|
||||
t.SetDrill(pcbnew.FromMM(VIA_DRILL_MM))
|
||||
elif t.Type() == pcbnew.PCB_TRACE_T:
|
||||
name = t.GetNetClassName()
|
||||
t.SetWidth(final.get(name, final["Default"]))
|
||||
|
||||
|
||||
def main():
|
||||
WORK.mkdir(exist_ok=True)
|
||||
dsn, ses = WORK / "board.dsn", WORK / "board.ses"
|
||||
|
||||
board = pcbnew.LoadBoard(str(BOARD))
|
||||
try:
|
||||
tracks = list(board.GetTracks())
|
||||
except TypeError:
|
||||
tracks = []
|
||||
for t in tracks:
|
||||
board.Delete(t)
|
||||
for z in list(board.Zones()):
|
||||
board.Delete(z) # GND is routed as tracks; the pour is added afterwards
|
||||
add_connector_stubs(board)
|
||||
if not pcbnew.ExportSpecctraDSN(board, str(dsn)):
|
||||
raise SystemExit("DSN export failed")
|
||||
|
||||
text = dsn.read_text(encoding="utf-8")
|
||||
classes = re.findall(r"\(class (\S+) ", text)
|
||||
text = patch_class_rules(text)
|
||||
text = patch_via_padstacks(text)
|
||||
extra = "".join(
|
||||
f" (class_class (classes HV {c})\n (rule (clearance {HV_TO_LV_UM}))\n )\n"
|
||||
for c in classes if c != "HV")
|
||||
# insert after the last (class ...) block inside (network ...)
|
||||
last = text.rfind(" (class ")
|
||||
depth, i = 0, last
|
||||
while True:
|
||||
if text[i] == "(":
|
||||
depth += 1
|
||||
elif text[i] == ")":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
i += 1
|
||||
text = text[:i + 1] + "\n" + extra + text[i + 1:]
|
||||
dsn.write_text(text, encoding="utf-8")
|
||||
|
||||
ses.unlink(missing_ok=True)
|
||||
subprocess.run([str(JAVA), "-jar", str(JAR), "-de", str(dsn), "-do", str(ses),
|
||||
"-mp", str(PASSES), "-l", "en"], check=True, cwd=WORK) # logs stay in route/
|
||||
if not ses.exists():
|
||||
raise SystemExit("Freerouting produced no session file")
|
||||
|
||||
if not pcbnew.ImportSpecctraSES(board, str(ses)):
|
||||
raise SystemExit("SES import failed")
|
||||
shrink_tracks(board)
|
||||
remove_dangling_stubs(board)
|
||||
add_gnd_plane(board)
|
||||
pcbnew.ZONE_FILLER(board).Fill(board.Zones())
|
||||
board.Save(str(BOARD))
|
||||
print("routed and saved", BOARD.name)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user