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
|
# KiCad generated reports
|
||||||
hardware/kicad/erc.rpt
|
hardware/kicad/erc.rpt
|
||||||
*.net
|
*.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 |
|
| [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/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/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/esp32-interface.md](docs/esp32-interface.md) | ESP32 pin plan, 74HCT245 level shifter, panel signals |
|
||||||
| [docs/bom.md](docs/bom.md) | Bill of materials |
|
| [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
|
- [ ] 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 power connector pins (WPC panel ≠ Stern CN2 pinout) and check if +12 V is used
|
||||||
- [ ] Map the panel's 14-pin data connector
|
- [ ] 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))
|
- [ ] 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 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
|
- [ ] 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 |
|
| 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 |
|
| 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:
|
The board is 170 × 125 mm, 2 layers, with 4 × M3 holes. It is fully routed with a bottom-layer GND pour.
|
||||||
- Keep the HV area (J5/J7, F4/F5, regulator, J2) on one side and the 12 V/5 V/watchdog on the other.
|
DRC is clean: 0 violations, 0 unconnected pads, schematic parity OK.
|
||||||
- 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.
|
- **Left: low voltage.** 12 V input, Mini560, ESP32 watchdog, relay, F1–F3.
|
||||||
4. Run DRC. The HV class enforces 3 mm to everything.
|
- **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
|
## Verified so far
|
||||||
|
|
||||||
- The schematic loads in KiCad 10.0.6, and ERC reports 0 violations.
|
- 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).
|
- `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.
|
- 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.
|
- 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_D41 = "Diode_THT:D_DO-41_SOD81_P10.16mm_Horizontal"
|
||||||
FP_D15 = "Diode_THT:D_DO-15_P12.70mm_Horizontal"
|
FP_D15 = "Diode_THT:D_DO-15_P12.70mm_Horizontal"
|
||||||
FP_LED = "LED_THT:LED_D5.0mm"
|
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_TO220 = "Package_TO_SOT_THT:TO-220-3_Vertical"
|
||||||
FP_FUSE = "Fuse:Fuseholder_Cylinder-5x20mm_Schurter_0031_8201_Horizontal_Open"
|
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"
|
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)
|
return round(round(v / 1.27) * 1.27, 2)
|
||||||
|
|
||||||
|
|
||||||
HV_NETS = ["+90V_RAW", "+65V", "HVP_MOD", "Q2_E", "REF_P", "Q3_B", "N_LED1",
|
# N_LED1/N_LED2 sit a LED drop away from GND, so they stay in Default.
|
||||||
"-130V_RAW", "-112V", "-100V", "HVN_MOD", "Q4_E", "REF_N", "Q5_B", "REF_M", "Q6_B", "Q6_C", "N_LED2"]
|
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"]
|
POWER_NETS = ["GND", "+12V", "+12V_IN", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"]
|
||||||
|
|
||||||
|
|
||||||
@@ -236,7 +237,7 @@ def write_project():
|
|||||||
"classes": [
|
"classes": [
|
||||||
netclass("Default", 0.25, 0.4, 2147483647),
|
netclass("Default", 0.25, 0.4, 2147483647),
|
||||||
netclass("Power", 0.3, 1.0, 1),
|
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},
|
"meta": {"version": 5},
|
||||||
"net_colors": None,
|
"net_colors": None,
|
||||||
@@ -247,8 +248,26 @@ def write_project():
|
|||||||
pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
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():
|
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 = {}, {}
|
lib_blocks, pin_cache = {}, {}
|
||||||
for p in PARTS:
|
for p in PARTS:
|
||||||
if p[1] not in lib_blocks:
|
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)"
|
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)
|
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)
|
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 "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 "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)))
|
(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": {
|
"meta": {
|
||||||
"filename": "plasma_dmd_psu.kicad_pro",
|
"filename": "plasma_dmd_psu.kicad_pro",
|
||||||
"version": 3
|
"version": 3
|
||||||
@@ -24,6 +512,25 @@
|
|||||||
"via_drill": 0.5,
|
"via_drill": 0.5,
|
||||||
"wire_width": 6
|
"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,
|
"bus_width": 12,
|
||||||
"clearance": 0.3,
|
"clearance": 0.3,
|
||||||
@@ -42,25 +549,6 @@
|
|||||||
"via_diameter": 1.0,
|
"via_diameter": 1.0,
|
||||||
"via_drill": 0.5,
|
"via_drill": 0.5,
|
||||||
"wire_width": 6
|
"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": {
|
"meta": {
|
||||||
@@ -93,10 +581,6 @@
|
|||||||
"netclass": "HV",
|
"netclass": "HV",
|
||||||
"pattern": "/Q3_B"
|
"pattern": "/Q3_B"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"netclass": "HV",
|
|
||||||
"pattern": "/N_LED1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"netclass": "HV",
|
"netclass": "HV",
|
||||||
"pattern": "/-130V_RAW"
|
"pattern": "/-130V_RAW"
|
||||||
@@ -137,10 +621,6 @@
|
|||||||
"netclass": "HV",
|
"netclass": "HV",
|
||||||
"pattern": "/Q6_C"
|
"pattern": "/Q6_C"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"netclass": "HV",
|
|
||||||
"pattern": "/N_LED2"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"netclass": "Power",
|
"netclass": "Power",
|
||||||
"pattern": "/GND"
|
"pattern": "/GND"
|
||||||
@@ -174,5 +654,157 @@
|
|||||||
"pattern": "/+12V_PANEL"
|
"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