Initial commit: plasma DMD driver project

- Design docs: 12 V DC power supply, safety and bring-up, ESP32 interface, BOM
- PSU schematic (schemdraw) and KiCad 10 carrier-board project with HV net classes
- ESP32 PlatformIO firmware skeleton with HV watchdog toggle
- Reference schematic of the Stern 520-5138-00 display PSU

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mangavalkandClaude Opus 5.5 committed 2026-10-24 02:31:50 +02:00
commit dbc1f0041f
21 files changed
+13853

No files matched your search

+19
View File
@@ -0,0 +1,19 @@
# PlatformIO
firmware/.pio/
firmware/.vscode/
# KiCad backups, caches and local state
*-backups/
*.kicad_prl
fp-info-cache
*.lck
\#auto_saved_files#
_autosave-*
*.bak
# Python
__pycache__/
# KiCad generated reports
hardware/kicad/erc.rpt
*.net
+41
View File
@@ -0,0 +1,41 @@
# Plasma DMD
Drive a second-hand 128×32 pinball **plasma** dot-matrix display (originally on a Williams WPC
DMD controller) standalone, from a **Wemos D1 mini ESP32**, powered from a single **12 V DC** brick.
The display power supply is built from cheap modules plus a copy of the regulator section of the
Stern/Sega display PSU (520-5138-00). The original transformer and AC front end are not used.
> ⚠️ **High voltage.** The panel runs on about +65 V, −100 V and −112 V, which is about 175 V
> between rails. That can injure you. Read [docs/safety.md](docs/safety.md) before powering anything.
## The panel
- Bought second-hand on 18 September 2021 for €37.
- Originally driven by a Williams WPC DMD controller board. No original controller or PSU came with it.
## Layout
| Path | Contents |
|---|---|
| [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) |
| [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 |
| [reference/](reference/) | Original Stern 520-5138-00 display PSU schematic |
| [firmware/](firmware/) | PlatformIO project for the ESP32 |
## Status
- [ ] 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))
- [ ] 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
- [ ] Firmware: row scanning with test pattern
- [ ] First power-up with panel (series resistors, current measurement)
- [ ] Firmware: greyscale, content source (Wi-Fi / serial)
+55
View File
@@ -0,0 +1,55 @@
# Bill of materials
Designators match the KiCad project ([../hardware/kicad/](../hardware/kicad/README.md)).
Footprint choices are in [gen_kicad.py](../hardware/kicad/gen_kicad.py).
Prices are rough estimates.
## Modules (AliExpress unless noted)
| Qty | Part | Notes | About |
|---|---|---|---|
| 1 | 12 V 3 A power brick | **Certified brand, from a local retailer** (e.g. Mean Well GST36A12) | €15 |
| 1 | Mini560 5 V fixed buck module | Fixed output, no trim pot | €2 |
| 2 | NCH6100HV nixie HV boost module | Input 9–16 V, output 85–235 V adjustable | €16 |
| 1 | Isolated DC-DC 12 V → 12 V, 6–10 W | e.g. Mornsun URB1212YMD-6WR3 / -10WR3 | €6 |
| 1 | 5 V 1-channel relay module, with optocoupler | Switches the 12 V feed to the HV modules | €2 |
| 1 | Wemos D1 mini ESP32 | | €5 |
## Regulator section (from Mouser / Farnell / Reichelt)
| Qty | Part | Ref (KiCad) | Ref (original Stern board) |
|---|---|---|---|
| 2 | MJE15030 (NPN, TO-220) | Q3, Q6 | Q3, Q5 |
| 1 | MJE15031 (PNP, TO-220) | Q5 | Q4 |
| 1 | MPSA92 (PNP, TO-92) | Q2 | Q1 |
| 1 | MPSA42 (NPN, TO-92) | Q4 | Q2 |
| 2 | 1N5228 (3.9 V zener) | D4, D6 | D3, D4 |
| 1 | 1N4760A (68 V zener) | D5 | D5 |
| 1 | 1N4764A (100 V zener) | D8 | D6 |
| 1 | 1N4743 (13 V zener) | D7 | D7 |
| 2 | 47 kΩ 1 W | R5, R10 | R2, R3 |
| 3 | 1.5 kΩ ½ W | R4, R9, R12 | R4, R5, R10 |
| 2 | 330 kΩ ½ W | R6, R11 | R6, R7 |
| 1 | 2 kΩ 5 W | R13 | R8 |
| 3 | 0.1 µF 500 V film | C6, C8, C9 | C3, C4, C7 |
| 3 | TO-220 heatsink | for Q3, Q5, Q6 | |
## Additions
| Qty | Part | Purpose |
|---|---|---|
| 1 | P6KE15A TVS diode (DO-15) | 12 V input surge protection (D1) |
| 1 | 470 µF 25 V electrolytic | 12 V input bulk (C1) |
| 1 | 100 µF 10 V electrolytic | +5 V bulk (C2) |
| 2 | 22 µF 250 V electrolytic (12.5 mm) | Raw HV rail buffer (C5, C7) |
| 2 | 330 kΩ ½ W | Raw HV rail bleeders (R2, R7) |
| 2 | LED + 100 kΩ 1 W | HV-present indicators (LED1/R3, LED2/R8) |
| 1 | 74HCT245 + DIP socket | 3.3 V → 5 V level shifter |
| 1 | 2N7000, 2 × 1N4148, 100 nF, 10 µF, 100 kΩ | HV watchdog charge pump (Q1, D2, D3, C3, C4, R1) |
| 2 | 1 kΩ 1 W | Series resistors for first power-up |
| 5 | 5×20 mm PCB fuse holders + fuses: 3 A T (F1), 2 × 1 A T (F2, F3), 2 × 100 mA F (F4, F5) | |
| — | Phoenix MKDS 5.08 mm screw terminals: 3 × 2-pin (J1, J6, J7), 2 × 3-pin (J5, J8), 1 × 4-pin (J3), 1 × 5-pin (J4) | Module connections |
| 1 | Molex KK-396 8-pin header (J2) + mating housing | Panel power |
| 1 | 2-pin header + jumper (JP1) | +12 V to panel |
| — | M3 standoffs (H1–H4), 300 V-rated wire, plastic enclosure | |
+35
View File
@@ -0,0 +1,35 @@
# ESP32 interface
Board: **Wemos D1 mini ESP32**, powered from the 5 V rail.
## Level shifting
The panel inputs are 5 V TTL and the ESP32 is 3.3 V. Use a **74HCT245** (HCT only, because it accepts 3.3 V
inputs) powered from 5 V, with DIR tied high (A→B) and OE tied low.
## Pin plan
| Signal (to panel, through 74HCT245) | GPIO | Notes |
|---|---|---|
| Dot clock | 18 | VSPI SCK |
| Serial dot data | 23 | VSPI MOSI |
| Column latch | 17 | |
| Row data | 16 | |
| Row clock | 26 | |
| Display enable | 22 | Pull to *off* state at the 245 input |
| HV watchdog toggle | 21 | Charge pump → relay (see safety.md) |
| +65 V monitor (optional) | 34 | ADC, 2 × 1 MΩ + 33 kΩ divider |
The strapping pins (0, 2, 5, 12, 15) are avoided so nothing twitches during boot.
## Panel data connector
- [ ] Map the panel's 14-pin data connector (signal ↔ pin) for this specific panel before wiring.
## Scanning
- 128 columns × 32 rows, about 120 Hz frame rate (as on WPC).
- Per row: shift 128 bits of dot data (SPI), latch the columns, advance the row (row data is high
for the first row only, then clock the row once per row).
- The panel must never sit with one row lit. Scanning runs continuously, and the HV watchdog is
toggled once per scanned row (about 2 kHz, so the charge pump stays charged).
+82
View File
@@ -0,0 +1,82 @@
# Power supply design
## Goal
Generate every panel rail from one **12 V DC** input, using cheap modules. Panel voltages are set by
**zener references** (copied from the original Stern board), not by module trim pots. A
mis-set or failed module can't push the panel above its rated voltage.
## The original board (Stern/Sega 520-5138-00)
See [../reference/stern-520-5138-00-display-psu-schematic.jpg](../reference/stern-520-5138-00-display-psu-schematic.jpg).
| CN2 pin | Label | How it is produced | Real value |
|---|---|---|---|
| 8 | +60 V | 88 VAC → D1 → C1 (about +120 V raw) → Q3 (MJE15030) emitter follower, base on D5 68 V zener | about +62–67 V |
| 1 | −120 V | 110 VAC → D2 → C2 (about −150 V raw) → Q4 (MJE15031) follower, base on D6 100 V + D7 13 V zeners | about −112 V |
| 2 | −100 V | Q5 (MJE15030) follower from the D6/D7 junction. Collector to GND via R8 2 kΩ 5 W. | about −100 V |
| 7 | +12 V | Passed through from CN1 | |
| 6 | +5 V | Passed through from CN1 | |
| 4, 5 | GND | | |
- Q1/D3/R2/R4 and Q2/D4/R3/R5: constant-current feeds (about 2 mA) for the zener references.
- R6/R7 (330 kΩ): bleeder resistors. C3/C4/C7: 0.1 µF 500 V decoupling.
## New design
Full schematic: [../hardware/psu-schematic.svg](../hardware/psu-schematic.svg) (PNG alongside).
It is generated from [../hardware/psu_schematic.py](../hardware/psu_schematic.py):
```bash
python -m pip install schemdraw matplotlib
python hardware/psu_schematic.py
```
The schematic's reference designators are new; the notes on the sheet map them to the original board.
Replace the transformer and AC front end (D1, D2, C1, C2, F1, R1) with DC boost modules. Keep the
regulator section as it is.
```
Mains ─► [Certified 12V 3A brick] ─► F 3A ─┬──────────────► +12V (only if the panel uses it)
│
├─► [Mini560 5V buck] ─► +5V ─► panel logic, ESP32, 74HCT245
│
└─► [Relay, NO contact] ◄── HV watchdog (see safety.md)
│
┌──────────────┴──────────────┐
▼ F 1A slow ▼ F 1A slow
[NCH6100HV #1 set 90V] [Isolated 12→12V DC-DC, 6–10W]
│ │
│ [NCH6100HV #2 set 130V]
│ out+ ─► GND
▼ F 100mA, +90V raw ▼ F 100mA, −130V raw
┌───────────────────────────────────────────────────┐
│ Copy of original regulator section: │
│ Q1/Q3/D3/D5/R2/R4 → +62–67V │
│ Q2/Q4/D4/D6/D7/R3/R5 → −112V │
│ Q5/R8/R10 → −100V │
│ R6/R7 bleeders, C3/C4/C7 500V caps │
└───────────────────────────────────────────────────┘
```
### Why the isolated converter
A boost module's output negative is shared with its input ground. Powering module #2 through an
isolated DC-DC lets its output float, so its **+ output is tied to GND** and its − output becomes
−130 V. A second certified 12 V wall adapter would also work.
### Changes compared with the original
- **Removed:** D1, D2, C1, C2, F1, R1 (the boost modules replace the AC front end; the module current
limit plus a 100 mA fast fuse replaces R1).
- **Added:** 10–22 µF / 250 V cap and a 330 kΩ bleeder across each raw HV rail.
- **Added:** HV-present LED (LED + 100 kΩ 1 W) on each raw HV rail.
- **Headroom:** +90 V raw feeding the 68 V-referenced stage, and −130 V raw feeding the
113 V-referenced stage, give the followers enough headroom with modest dissipation. Q3/Q4/Q5 get small heatsinks.
## Open questions
- [ ] Real HV current draw of the panel, so you can size the isolated DC-DC (6 W vs 10 W)
- [ ] Panel power connector pinout and whether +12 V is used
- [ ] Check that NCH6100HV has a usable enable/shutdown pin (otherwise, switch with the relay only, as drawn)
+57
View File
@@ -0,0 +1,57 @@
# Safety
About 175 V sits between the +65 V and −112 V rails. Treat every HV node as live until you have
measured it.
## Design measures
1. **Only certified mains equipment.** The 12 V brick is the only mains-connected part. Buy a
certified brand (e.g. Mean Well GST36A12) from a normal retailer, not AliExpress.
2. **Hardware HV watchdog.** The relay on the HV modules' 12 V feed is driven through a charge pump:
```
GPIO21 ──┤├──── A ──►|── B ───┬───────┬──── 2N7000 gate 2N7000 drain ─► relay IN (active-low)
100nF │ 1N4148 │ │ 2N7000 source ─► GND
┴ 1N4148 ═ 10µF ▯ 100k
▲ (anode GND) │ │
GND GND GND
```
Node A gets a diode to GND (cathode at A), and a diode from A to B (anode at A). This rectifies
the toggling signal into a DC voltage on B. A constant high or low level gives no charge.
The firmware toggles GPIO21 once per scanned row (about 2 kHz). If it crashes, or the pin sticks high or low, or
the ESP32 resets, the cap discharges and the relay drops within about 1 s. A stuck row can't burn the panel.
(Check the polarity of your relay module's input. Some are active-low.)
3. **Fail-safe defaults.** The relay is normally open. Put a 10 kΩ pull-down on the watchdog
path. Pull the panel ENABLE line to its *off* state at the 74HCT245 input, so a booting ESP32
can't light anything.
4. **Sequencing.** 5 V comes up, then the ESP32 boots and starts scanning, and only then does the watchdog close the relay.
5. **Fusing.**
| Where | Fuse |
|---|---|
| 12 V input | 3 A |
| Each HV module input | 1 A slow-blow |
| Each raw HV rail | 100 mA fast |
6. **Discharge.** 330 kΩ bleeders on raw and regulated rails. HV-present LEDs are a hint only;
always measure before you touch anything.
7. **Layout.**
- Put all HV parts on one side, with ≥ 3 mm clearance between HV tracks.
- Use wire and caps rated for ≥ 300 V/500 V.
- Use a keyed connector to the panel.
- Close it in a plastic enclosure with no exposed HV.
- Tie all grounds at one point.
8. **Semiconductors from real distributors** (Mouser, Farnell, Reichelt). Fake HV transistors are
common on AliExpress.
## Bring-up procedure
1. Build the 5 V and 12 V side only. Check the voltages.
2. Connect one HV module at a time, with **no load**. Set NCH6100HV #1 to 90 V and #2 to 130 V. Lock the pots with nail varnish.
3. Connect the regulator section, still without the panel. Check for about +65 V, −112 V and −100 V and that the bleeders discharge them.
4. Test the watchdog: stop toggling GPIO21, and the HV must drop within about 1 s.
5. Map the panel's power connector against the measured rails. Get a second check (photo/label).
6. First power-up with the panel: 1 kΩ in series with each HV rail and a running test pattern.
Measure the current per rail, then remove the series resistors.
+5
View File
@@ -0,0 +1,5 @@
[env:wemos_d1_mini32]
platform = espressif32
board = wemos_d1_mini32
framework = arduino
monitor_speed = 115200
+77
View File
@@ -0,0 +1,77 @@
// Plasma DMD driver skeleton for Wemos D1 mini ESP32.
// Scans a 128x32 plasma panel continuously and toggles the HV watchdog once per row.
// Signal polarities (enable, latch, row data) are NOT yet verified against the panel.
#include <Arduino.h>
#include <SPI.h>
// Pins, see docs/esp32-interface.md
constexpr int PIN_DOT_CLK = 18; // VSPI SCK
constexpr int PIN_DOT_DATA = 23; // VSPI MOSI
constexpr int PIN_COL_LATCH = 17;
constexpr int PIN_ROW_DATA = 16;
constexpr int PIN_ROW_CLK = 26;
constexpr int PIN_ENABLE = 22;
constexpr int PIN_HV_WDOG = 21; // charge pump -> HV relay
constexpr int WIDTH = 128;
constexpr int HEIGHT = 32;
constexpr int ROW_BYTES = WIDTH / 8;
// TODO: verify polarities on the real panel
constexpr int ENABLE_ON = HIGH;
constexpr int ENABLE_OFF = !ENABLE_ON;
uint8_t frame[HEIGHT][ROW_BYTES];
static void makeTestPattern() {
for (int y = 0; y < HEIGHT; y++)
for (int b = 0; b < ROW_BYTES; b++)
frame[y][b] = ((y / 4 + b) & 1) ? 0xFF : 0x00; // checkerboard
}
static void pulse(int pin) {
digitalWrite(pin, HIGH);
delayMicroseconds(1);
digitalWrite(pin, LOW);
}
static void scanFrame() {
for (int y = 0; y < HEIGHT; y++) {
SPI.writeBytes(frame[y], ROW_BYTES);
digitalWrite(PIN_ENABLE, ENABLE_OFF); // blank while switching rows
pulse(PIN_COL_LATCH);
digitalWrite(PIN_ROW_DATA, y == 0 ? HIGH : LOW);
pulse(PIN_ROW_CLK);
digitalWrite(PIN_ENABLE, ENABLE_ON);
delayMicroseconds(200); // row on-time, ~120 Hz frame rate incl. overhead
// Toggle per row (~2 kHz) to keep the charge pump charged. If scanning stops, HV drops.
digitalWrite(PIN_HV_WDOG, !digitalRead(PIN_HV_WDOG));
}
digitalWrite(PIN_ENABLE, ENABLE_OFF);
}
void setup() {
// Safe state first: display disabled, watchdog idle (HV stays off).
pinMode(PIN_ENABLE, OUTPUT);
digitalWrite(PIN_ENABLE, ENABLE_OFF);
pinMode(PIN_HV_WDOG, OUTPUT);
digitalWrite(PIN_HV_WDOG, LOW);
pinMode(PIN_COL_LATCH, OUTPUT);
pinMode(PIN_ROW_DATA, OUTPUT);
pinMode(PIN_ROW_CLK, OUTPUT);
Serial.begin(115200);
SPI.begin(PIN_DOT_CLK, -1, PIN_DOT_DATA, -1);
SPI.setFrequency(4000000);
SPI.setDataMode(SPI_MODE0);
makeTestPattern();
}
void loop() {
scanFrame();
}
+66
View File
@@ -0,0 +1,66 @@
# KiCad project: PSU carrier board
Open `plasma_dmd_psu.kicad_pro` in KiCad 10.
The board carries the fuses, the HV watchdog, both regulators (copied from Stern 520-5138-00) and
the connectors. The AliExpress modules sit **off-board** and connect through Phoenix 5.08 mm screw terminals:
| Connector | Goes to | Pins |
|---|---|---|
| J1 | 12 V DC brick | +12V, GND |
| J3 | Mini560 5 V buck | IN+, IN−, OUT+, OUT− |
| J4 | Relay module | VCC, GND, IN, COM, NO |
| J5 | NCH6100HV #1 (set 90 V) | VIN, GND, HV+ |
| J6 | Isolated DC-DC input | VIN, GND |
| J7 | NCH6100HV #2 (set 130 V) output | HV+ (→ GND), GND (→ −130 V raw) |
| J8 | ESP32 | 5V, GND, GPIO21 (watchdog) |
| J2 | Panel power, Molex KK-396 8-pin | Stern CN2 order: 1 −112V, 2 −100V, 3 key, 4–5 GND, 6 +5V, 7 +12V (via JP1), 8 +65V |
Wire the isolated DC-DC output straight to NCH6100HV #2's input, off-board. Both float at −130 V.
> ⚠️ **J2 pinout is a placeholder.** It follows the Stern supply, not necessarily your WPC panel.
> Map it to your panel before you plug anything in.
## Files
| File | What |
|---|---|
| `gen_kicad.py` | Generates the schematic and the project's net classes from the `PARTS` table. **The source of truth.** |
| `check_netlist.py` | Exports the netlist with kicad-cli and checks every pin against `PARTS` |
| `plasma_dmd_psu.kicad_sch` | Generated schematic (labels-per-pin style: connectivity is by net label) |
| `plasma_dmd_psu.pdf` | Plot of the schematic |
```bash
python hardware/kicad/gen_kicad.py
python hardware/kicad/check_netlist.py
```
Re-running `gen_kicad.py` **overwrites the schematic**. Either keep editing `PARTS` and regenerate, or
stop using the generator once you start editing the schematic in KiCad.
## Net classes (already in the project)
| 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 |
| Power | GND, +12V*, +5V, +12V_PANEL | 0.3 mm | 1.0 mm |
| Default | Watchdog signals | 0.25 mm | 0.4 mm |
## Making the PCB
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.
## 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).
- 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.
**Not verified:** the circuit itself on real hardware, and the module current ratings.
+53
View File
@@ -0,0 +1,53 @@
"""Export the netlist with kicad-cli and compare it with PARTS in gen_kicad.py.
python hardware/kicad/check_netlist.py [path/to/kicad-cli]
"""
import re
import subprocess
import sys
from pathlib import Path
from gen_kicad import PARTS, PROJECT, pin_cache_for, read_sexpr_block
HERE = Path(__file__).parent
CLI = sys.argv[1] if len(sys.argv) > 1 else "C:/Users/patri/AppData/Local/Programs/KiCad/10.0/bin/kicad-cli.exe"
net_file = HERE / f"{PROJECT}.net"
subprocess.run([CLI, "sch", "export", "netlist", "-o", str(net_file), str(HERE / f"{PROJECT}.kicad_sch")],
check=True, capture_output=True)
text = net_file.read_text(encoding="utf-8")
net_file.unlink()
# actual: (ref, pin number) -> net name
actual = {}
for m in re.finditer(r"\(net\s+\(code", text):
blk = read_sexpr_block(text, m.start())
name = re.search(r'\(name "([^"]*)"\)', blk).group(1).lstrip("/")
for ref, pin in re.findall(r'\(node\s+\(ref "([^"]+)"\)\s+\(pin "([^"]+)"\)', blk):
actual[(ref, pin)] = name
errors = 0
expected = {}
for ref, lib_id, value, fp, conns, g in PARTS:
for num, name, *_ in pin_cache_for(lib_id):
key = num if num in conns else (name if name in conns else None)
if key is None or conns[key] is None:
continue
expected[(ref, num)] = conns[key]
for k, net in expected.items():
got = actual.get(k)
if got != net:
errors += 1
print(f"MISMATCH {k}: expected {net}, got {got}")
# every net should join at least two pins
by_net = {}
for k, net in actual.items():
by_net.setdefault(net, []).append(k)
for net, nodes in sorted(by_net.items()):
if len(nodes) < 2 and not net.startswith("unconnected"):
errors += 1
print(f"SINGLE-PIN NET {net}: {nodes}")
print(f"{len(expected)} pins checked, {len(by_net)} nets, {errors} problem(s)")
sys.exit(1 if errors else 0)
+333
View File
@@ -0,0 +1,333 @@
"""Generate the KiCad schematic for the Plasma DMD PSU carrier board.
The AliExpress modules (Mini560, 2x NCH6100HV, isolated DC-DC, relay) sit off-board and connect
through screw terminals. Symbols are copied from the installed KiCad standard libraries.
Every pin gets a short wire stub and a net label; connectivity is defined in PARTS below.
python hardware/kicad/gen_kicad.py [path/to/kicad/share]
"""
import re
import sys
import uuid
from pathlib import Path
HERE = Path(__file__).parent
PROJECT = "plasma_dmd_psu"
SHARE = Path(sys.argv[1] if len(sys.argv) > 1 else "C:/Users/patri/AppData/Local/Programs/KiCad/10.0/share/kicad")
SYMDIR = SHARE / "symbols"
FP_R = "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P10.16mm_Horizontal"
FP_R1W = "Resistor_THT:R_Axial_DIN0414_L11.9mm_D4.5mm_P15.24mm_Horizontal"
FP_R5W = "Resistor_THT:R_Axial_Power_L25.0mm_W6.4mm_P30.48mm"
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_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"
FP_CDISC = "Capacitor_THT:C_Disc_D5.0mm_W2.5mm_P5.00mm"
FP_CP5 = "Capacitor_THT:CP_Radial_D5.0mm_P2.00mm"
FP_CP63 = "Capacitor_THT:CP_Radial_D6.3mm_P2.50mm"
FP_CP10 = "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm"
FP_CP125 = "Capacitor_THT:CP_Radial_D12.5mm_P5.00mm"
FP_HDR2 = "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical"
FP_KK8 = "Connector_Molex:Molex_KK-396_A-41791-0008_1x08_P3.96mm_Vertical"
FP_HOLE = "MountingHole:MountingHole_3.2mm_M3"
def tb(n):
return f"TerminalBlock_Phoenix:TerminalBlock_Phoenix_MKDS-1,5-{n}-5.08_1x{n:02d}_P5.08mm_Horizontal"
# (ref, lib_id, value, footprint, {pin number or name: net}, group)
PARTS = [
# 1. Input and +5 V
("J1", "Connector:Screw_Terminal_01x02", "12V DC in", tb(2), {"1": "+12V_IN", "2": "GND"}, 0),
("F1", "Device:Fuse", "3A T", FP_FUSE, {"1": "+12V_IN", "2": "+12V"}, 0),
("D1", "Device:D_TVS", "P6KE15A", FP_D15, {"1": "+12V", "2": "GND"}, 0),
("C1", "Device:C_Polarized", "470uF 25V", FP_CP10, {"1": "+12V", "2": "GND"}, 0),
("J3", "Connector:Screw_Terminal_01x04", "Mini560 5V: IN+ IN- OUT+ OUT-", tb(4),
{"1": "+12V", "2": "GND", "3": "+5V", "4": "GND"}, 0),
("C2", "Device:C_Polarized", "100uF 10V", FP_CP63, {"1": "+5V", "2": "GND"}, 0),
# 2. HV enable + watchdog
("J8", "Connector:Screw_Terminal_01x03", "ESP32: 5V GND GPIO21", tb(3),
{"1": "+5V", "2": "GND", "3": "WDOG_IN"}, 1),
("C3", "Device:C", "100nF", FP_CDISC, {"1": "WDOG_IN", "2": "WDOG_A"}, 1),
("D3", "Device:D", "1N4148", FP_D35, {"K": "WDOG_A", "A": "GND"}, 1),
("D2", "Device:D", "1N4148", FP_D35, {"A": "WDOG_A", "K": "WDOG_B"}, 1),
("C4", "Device:C_Polarized", "10uF 16V", FP_CP5, {"1": "WDOG_B", "2": "GND"}, 1),
("R1", "Device:R", "100k", FP_R, {"1": "WDOG_B", "2": "GND"}, 1),
("Q1", "Transistor_FET:2N7000", "2N7000", FP_TO92, {"G": "WDOG_B", "S": "GND", "D": "RELAY_IN"}, 1),
("J4", "Connector:Screw_Terminal_01x05", "Relay module: VCC GND IN COM NO", tb(5),
{"1": "+5V", "2": "GND", "3": "RELAY_IN", "4": "+12V", "5": "+12V_HV"}, 1),
("F2", "Device:Fuse", "1A T", FP_FUSE, {"1": "+12V_HV", "2": "+12V_HVP"}, 1),
("F3", "Device:Fuse", "1A T", FP_FUSE, {"1": "+12V_HV", "2": "+12V_HVN"}, 1),
# 3. Positive HV
("J5", "Connector:Screw_Terminal_01x03", "NCH6100HV #1 (90V): VIN GND HV+", tb(3),
{"1": "+12V_HVP", "2": "GND", "3": "HVP_MOD"}, 2),
("F4", "Device:Fuse", "100mA F", FP_FUSE, {"1": "HVP_MOD", "2": "+90V_RAW"}, 2),
("C5", "Device:C_Polarized", "22uF 250V", FP_CP125, {"1": "+90V_RAW", "2": "GND"}, 2),
("R2", "Device:R", "330k", FP_R, {"1": "+90V_RAW", "2": "GND"}, 2),
("R3", "Device:R", "100k 1W", FP_R1W, {"1": "+90V_RAW", "2": "N_LED1"}, 2),
("LED1", "Device:LED", "HV on", FP_LED, {"A": "N_LED1", "K": "GND"}, 2),
("D4", "Device:D_Zener", "1N5228 3.9V", FP_D35, {"K": "+90V_RAW", "A": "REF_P"}, 2),
("R5", "Device:R", "47k 1W", FP_R1W, {"1": "REF_P", "2": "GND"}, 2),
("R4", "Device:R", "1.5k", FP_R, {"1": "+90V_RAW", "2": "Q2_E"}, 2),
("Q2", "Transistor_BJT:Q_PNP_EBC", "MPSA92", FP_TO92, {"E": "Q2_E", "B": "REF_P", "C": "Q3_B"}, 2),
("D5", "Device:D_Zener", "1N4760A 68V", FP_D41, {"K": "Q3_B", "A": "GND"}, 2),
("Q3", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"C": "+90V_RAW", "B": "Q3_B", "E": "+65V"}, 2),
("C6", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "+65V", "2": "GND"}, 2),
("R6", "Device:R", "330k", FP_R, {"1": "+65V", "2": "GND"}, 2),
# 4. Negative HV
("J6", "Connector:Screw_Terminal_01x02", "Isolated DC-DC in: VIN GND", tb(2),
{"1": "+12V_HVN", "2": "GND"}, 3),
("J7", "Connector:Screw_Terminal_01x02", "NCH6100HV #2 (130V) out: HV+ GND", tb(2),
{"1": "GND", "2": "HVN_MOD"}, 3),
("F5", "Device:Fuse", "100mA F", FP_FUSE, {"1": "HVN_MOD", "2": "-130V_RAW"}, 3),
("C7", "Device:C_Polarized", "22uF 250V", FP_CP125, {"1": "GND", "2": "-130V_RAW"}, 3),
("R7", "Device:R", "330k", FP_R, {"1": "GND", "2": "-130V_RAW"}, 3),
("R8", "Device:R", "100k 1W", FP_R1W, {"1": "N_LED2", "2": "-130V_RAW"}, 3),
("LED2", "Device:LED", "HV on", FP_LED, {"A": "GND", "K": "N_LED2"}, 3),
("D6", "Device:D_Zener", "1N5228 3.9V", FP_D35, {"K": "REF_N", "A": "-130V_RAW"}, 3),
("R10", "Device:R", "47k 1W", FP_R1W, {"1": "GND", "2": "REF_N"}, 3),
("R9", "Device:R", "1.5k", FP_R, {"1": "Q4_E", "2": "-130V_RAW"}, 3),
("Q4", "Transistor_BJT:Q_NPN_EBC", "MPSA42", FP_TO92, {"E": "Q4_E", "B": "REF_N", "C": "Q5_B"}, 3),
("D7", "Device:D_Zener", "1N4743 13V", FP_D41, {"K": "REF_M", "A": "Q5_B"}, 3),
("D8", "Device:D_Zener", "1N4764A 100V", FP_D41, {"K": "GND", "A": "REF_M"}, 3),
("Q5", "Transistor_BJT:Q_PNP_BCE", "MJE15031", FP_TO220, {"B": "Q5_B", "C": "-130V_RAW", "E": "-112V"}, 3),
("C8", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "GND", "2": "-112V"}, 3),
("R11", "Device:R", "330k", FP_R, {"1": "GND", "2": "-112V"}, 3),
("R12", "Device:R", "1.5k", FP_R, {"1": "REF_M", "2": "Q6_B"}, 3),
("Q6", "Transistor_BJT:Q_NPN_BCE", "MJE15030", FP_TO220, {"B": "Q6_B", "C": "Q6_C", "E": "-100V"}, 3),
("R13", "Device:R", "2k 5W", FP_R5W, {"1": "GND", "2": "Q6_C"}, 3),
("C9", "Device:C", "0.1uF 500V", FP_CFILM, {"1": "GND", "2": "-100V"}, 3),
# 5. Panel connector (Stern 520-5138 CN2 order; verify against your panel!)
("J2", "Connector_Generic:Conn_01x08", "Panel power (Stern CN2 order)", FP_KK8,
{"1": "-112V", "2": "-100V", "3": None, "4": "GND", "5": "GND", "6": "+5V", "7": "+12V_PANEL", "8": "+65V"}, 4),
("JP1", "Connector_Generic:Conn_01x02", "Fit if panel uses +12V", FP_HDR2,
{"1": "+12V", "2": "+12V_PANEL"}, 4),
("H1", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4),
("H2", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4),
("H3", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4),
("H4", "Mechanical:MountingHole", "M3", FP_HOLE, {}, 4),
]
GROUP_TITLES = [
"1. 12 V input and +5 V (Mini560 off-board)",
"2. HV enable relay + hardware watchdog (relay module off-board)",
"3. Positive HV: NCH6100HV #1 off-board + original regulator -> +65 V",
"4. Negative HV: isolated DC-DC + NCH6100HV #2 off-board (FLOATING at -130 V) + regulator -> -112 / -100 V",
"5. Panel connector and mounting",
]
COLS = 7 # parts per row inside a group
DX, DY = 50.8, 27.94
GROUP_GAP = 12.7
def group_origins():
"""Stack the groups vertically, each as tall as its number of part rows."""
y, out = 33.02, []
for g in range(len(GROUP_TITLES)):
n = sum(1 for p in PARTS if p[5] == g)
out.append((25.4, y))
y += 12.7 + -(-n // COLS) * DY + GROUP_GAP
return out
GROUPS = list(zip(GROUP_TITLES, group_origins()))
NOTES = """Plasma DMD - 12 V DC PSU carrier board (rev 0.1, unverified)
Connectivity is by net labels: every pin has a stub + label. Off-board modules connect via J3-J7.
Panel voltages are set by zeners D5 (68 V -> about +65 V) and D7+D8 (113 V -> about -112 / -100 V), copied from Stern 520-5138-00.
SAFETY: keep >= 3 mm clearance (more is better) between HV nets (+90V_RAW, +65V, -130V_RAW, -112V, -100V, HVN_MOD, HVP_MOD) and low-voltage nets.
J2 pin order follows the Stern CN2 connector: map it to YOUR panel before use. Pin 3 is the key position."""
# ---------------------------------------------------------------------------
def uid():
return str(uuid.uuid4())
def read_sexpr_block(text, start):
depth, i = 0, start
in_str = False
while True:
ch = text[i]
if in_str:
if ch == "\\":
i += 1
elif ch == '"':
in_str = False
elif ch == '"':
in_str = True
elif ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
return text[start:i + 1]
i += 1
_libcache = {}
def lib_symbol(lib_id):
lib, name = lib_id.split(":")
if lib not in _libcache:
_libcache[lib] = (SYMDIR / f"{lib}.kicad_sym").read_text(encoding="utf-8")
text = _libcache[lib]
m = re.search(r'\(symbol "' + re.escape(name) + r'"', text)
if not m:
raise KeyError(lib_id)
block = read_sexpr_block(text, m.start())
ext = re.search(r'\(extends "([^"]+)"\)', block)
if ext:
raise ValueError(f"{lib_id} extends {ext.group(1)}; pick a non-derived symbol")
# top-level name gets the library prefix; sub-units keep the bare name
return block.replace(f'(symbol "{name}"', f'(symbol "{lib_id}"', 1)
def pins_of(block):
"""Return list of (number, name, x, y, angle) from a symbol block (symbol coords, y up)."""
out = []
for m in re.finditer(r'\(pin \w+ \w+\s*\(at ([-\d.]+) ([-\d.]+) ([-\d.]+)\)', block):
pb = read_sexpr_block(block, m.start())
name = re.search(r'\(name "([^"]*)"', pb).group(1)
num = re.search(r'\(number "([^"]*)"', pb).group(1)
out.append((num, name, float(m.group(1)), float(m.group(2)), float(m.group(3))))
return out
def pin_cache_for(lib_id):
return pins_of(lib_symbol(lib_id))
def q(s):
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + '"'
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"]
POWER_NETS = ["GND", "+12V", "+12V_IN", "+12V_HV", "+12V_HVP", "+12V_HVN", "+5V", "+12V_PANEL"]
def netclass(name, clearance, track, priority):
return {"bus_width": 12, "clearance": clearance, "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": name, "pcb_color": "rgba(0, 0, 0, 0.000)", "priority": priority,
"schematic_color": "rgba(0, 0, 0, 0.000)", "track_width": track, "tuning_profile": "",
"via_diameter": 1.0, "via_drill": 0.5, "wire_width": 6}
def write_project():
"""Create/update the .kicad_pro with net classes. HV nets get 3 mm clearance to everything."""
import json
pro = HERE / f"{PROJECT}.kicad_pro"
data = json.loads(pro.read_text(encoding="utf-8")) if pro.exists() else {}
data.setdefault("meta", {"filename": pro.name, "version": 3})
data["net_settings"] = {
"classes": [
netclass("Default", 0.25, 0.4, 2147483647),
netclass("Power", 0.3, 1.0, 1),
netclass("HV", 3.0, 0.6, 0),
],
"meta": {"version": 5},
"net_colors": None,
"netclass_assignments": None,
"netclass_patterns": [{"netclass": "HV", "pattern": "/" + n} for n in HV_NETS]
+ [{"netclass": "Power", "pattern": "/" + n} for n in POWER_NETS],
}
pro.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def main():
root = uid()
lib_blocks, pin_cache = {}, {}
for p in PARTS:
if p[1] not in lib_blocks:
blk = lib_symbol(p[1])
lib_blocks[p[1]] = blk
pin_cache[p[1]] = pins_of(blk)
items = []
counts = {}
for ref, lib_id, value, fp, conns, g in PARTS:
i = counts.get(g, 0)
counts[g] = i + 1
gx, gy = GROUPS[g][1]
x = snap(gx + 12.7 + (i % COLS) * DX)
y = snap(gy + 12.7 + (i // COLS) * DY)
pins = pin_cache[lib_id]
# Ref/value go beside the part: above it when a net label sticks out to the right
# (horizontal two-pin parts), otherwise to the right of the body.
if any(int(a) % 360 == 180 for *_, a in pins):
rx, ry, vx, vy, just = x, snap(y - 6.35), x, snap(y - 4.445), ""
else:
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())})
(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)))
(property "Datasheet" "" (at {x} {y} 0) (effects (font (size 1.27 1.27)) (hide yes)))
{pin_uuid}
(instances (project {q(PROJECT)} (path {q('/' + root)} (reference {q(ref)}) (unit 1))))
)""")
used = set()
for num, name, px, py, ang in pins:
key = num if num in conns else (name if name in conns else None)
if key is None:
if conns:
raise KeyError(f"{ref}: pin {num}/{name} not connected in PARTS")
continue
used.add(key)
net = conns[key]
cx, cy = round(x + px, 2), round(y - py, 2) # schematic y is down
# stub points away from the body (pin angle points from connection point toward body)
dx, dy = {0: (-1, 0), 90: (0, 1), 180: (1, 0), 270: (0, -1)}[int(ang) % 360]
ex, ey = round(cx + dx * 2.54, 2), round(cy + dy * 2.54, 2)
if net is None:
items.append(f' (no_connect (at {cx} {cy}) (uuid {q(uid())}))')
continue
items.append(f' (wire (pts (xy {cx} {cy}) (xy {ex} {ey})) (stroke (width 0) (type default)) (uuid {q(uid())}))')
lang, just = {(-1, 0): (180, "right"), (1, 0): (0, "left"),
(0, -1): (90, "left"), (0, 1): (270, "right")}[(dx, dy)]
items.append(f' (label {q(net)} (at {ex} {ey} {lang}) (fields_autoplaced yes)'
f' (effects (font (size 1.27 1.27)) (justify {just} bottom)) (uuid {q(uid())}))')
missing = set(conns) - used
if missing:
raise KeyError(f"{ref}: unknown pins {missing}")
for title, (gx, gy) in GROUPS:
items.append(f' (text {q(title)} (exclude_from_sim no) (at {gx} {gy} 0)'
f' (effects (font (size 2.54 2.54) (bold yes)) (justify left bottom)) (uuid {q(uid())}))')
items.append(f' (text {q(NOTES)} (exclude_from_sim no) (at 25.4 12.7 0)'
f' (effects (font (size 1.778 1.778)) (justify left top)) (uuid {q(uid())}))')
libs = "\n".join(" " + b.replace("\n", "\n ") for b in lib_blocks.values())
sch = f"""(kicad_sch (version 20260101) (generator "plasma_dmd_gen") (generator_version "1.0")
(uuid {q(root)})
(paper "A2")
(title_block (title "Plasma DMD 12 V DC PSU") (rev "0.1") (comment 1 "Unverified - see notes"))
(lib_symbols
{libs}
)
{chr(10).join(items)}
(sheet_instances (path "/" (page "1")))
)
"""
(HERE / f"{PROJECT}.kicad_sch").write_text(sch, encoding="utf-8")
write_project()
print(f"wrote {PROJECT}.kicad_sch with {len(PARTS)} parts")
if __name__ == "__main__":
main()
+178
View File
@@ -0,0 +1,178 @@
{
"meta": {
"filename": "plasma_dmd_psu.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.25,
"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": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.4,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 0.3,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Power",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 1,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 1.0,
"tuning_profile": "",
"via_diameter": 1.0,
"via_drill": 0.5,
"wire_width": 6
},
{
"bus_width": 12,
"clearance": 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": {
"version": 5
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": [
{
"netclass": "HV",
"pattern": "/+90V_RAW"
},
{
"netclass": "HV",
"pattern": "/+65V"
},
{
"netclass": "HV",
"pattern": "/HVP_MOD"
},
{
"netclass": "HV",
"pattern": "/Q2_E"
},
{
"netclass": "HV",
"pattern": "/REF_P"
},
{
"netclass": "HV",
"pattern": "/Q3_B"
},
{
"netclass": "HV",
"pattern": "/N_LED1"
},
{
"netclass": "HV",
"pattern": "/-130V_RAW"
},
{
"netclass": "HV",
"pattern": "/-112V"
},
{
"netclass": "HV",
"pattern": "/-100V"
},
{
"netclass": "HV",
"pattern": "/HVN_MOD"
},
{
"netclass": "HV",
"pattern": "/Q4_E"
},
{
"netclass": "HV",
"pattern": "/REF_N"
},
{
"netclass": "HV",
"pattern": "/Q5_B"
},
{
"netclass": "HV",
"pattern": "/REF_M"
},
{
"netclass": "HV",
"pattern": "/Q6_B"
},
{
"netclass": "HV",
"pattern": "/Q6_C"
},
{
"netclass": "HV",
"pattern": "/N_LED2"
},
{
"netclass": "Power",
"pattern": "/GND"
},
{
"netclass": "Power",
"pattern": "/+12V"
},
{
"netclass": "Power",
"pattern": "/+12V_IN"
},
{
"netclass": "Power",
"pattern": "/+12V_HV"
},
{
"netclass": "Power",
"pattern": "/+12V_HVP"
},
{
"netclass": "Power",
"pattern": "/+12V_HVN"
},
{
"netclass": "Power",
"pattern": "/+5V"
},
{
"netclass": "Power",
"pattern": "/+12V_PANEL"
}
]
}
}
File diff suppressed because it is too large. Load diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

File diff suppressed because it is too large. Load diff

After

Width:  |  Height:  |  Size: 304 KiB

+319
View File
@@ -0,0 +1,319 @@
"""Generate the PSU schematic (SVG + PNG) with schemdraw.
python -m pip install schemdraw matplotlib
python hardware/psu_schematic.py
"""
from pathlib import Path
import schemdraw
import schemdraw.elements as elm
OUT = Path(__file__).parent
schemdraw.use("matplotlib")
def gnd(d, at):
"""Ground symbol pointing down (for parts hanging below a node)."""
d.add(elm.Ground().theta(0).at(at))
def gnd_up(d, at):
"""Ground symbol pointing up (for parts standing on a negative rail)."""
d.add(elm.Ground().theta(180).at(at))
def vdd(d, at, name):
d.add(elm.Vdd().theta(0).at(at).label(name))
def ic(left, right, label, bottom=(), top=()):
pins = [elm.IcPin(name=n, side="left") for n in left]
pins += [elm.IcPin(name=n, side="right") for n in right]
pins += [elm.IcPin(name=n, side="bottom") for n in bottom]
pins += [elm.IcPin(name=n, side="top") for n in top]
return elm.Ic(pins=pins, pinspacing=1.2, edgepadH=0.6, edgepadW=1.2).right().label(label, "top")
def title(d, at, text):
d.add(elm.Label().at(at).label(text, fontsize=12, halign="left", valign="bottom"))
def note(d, at, text, size=9, color="black"):
d.add(elm.Label().at(at).label(text, fontsize=size, halign="left", valign="top", color=color))
# For vertical parts schemdraw's "left"/"right" label locations are the part's ends;
# these give the visual side instead. A .reverse()d part swaps its sides again.
R_DOWN, L_DOWN, R_UP, L_UP = "bottom", "top", "bottom", "top"
def shunt_down(d, at, part, label):
"""part is a factory: elements auto-add when created inside a Drawing."""
d.add(elm.Dot().at(at))
d.add(part().at(at).down().label(label, R_DOWN))
gnd(d, d.here)
def shunt_up(d, at, part, label):
d.add(elm.Dot().at(at))
d.add(part().at(at).up().label(label, R_UP))
gnd_up(d, d.here)
# ---------------------------------------------------------------------------
def block_input(d, ox, oy):
title(d, (ox - 3, oy + 3.5), "1. 12 V input and +5 V")
d.add(elm.Dot(open=True).at((ox, oy)).label("J1 +12V in\n(certified brick)", "left"))
d.add(elm.Fuse().right().label("F1 3A T"))
n1 = d.here
d.add(elm.Line().right().length(3))
n2 = d.here
d.add(elm.Line().right().length(3))
n3 = d.here
d.add(elm.Line().right().length(2))
d.add(elm.Dot())
d.add(elm.DiodeTVS().at(n1).down().reverse().label("D1\nP6KE15A", L_DOWN))
gnd(d, d.here)
d.add(elm.Dot().at(n1))
shunt_down(d, n2, lambda: elm.Capacitor(polar=True), "C1\n470µF 25V")
d.add(elm.Dot().at(n3))
d.add(elm.Line().at(n3).up().length(1.2))
vdd(d, d.here, "+12V")
d.add(elm.Dot(open=True).at((ox, oy - 2.5)).label("J1 GND", "left"))
gnd(d, (ox, oy - 2.5))
u1 = d.add(ic(["VIN"], ["VOUT"], "U1 Mini560\n5V fixed buck", bottom=["GND"])
.at((n3.x + 2, oy)).anchor("VIN"))
gnd(d, u1.GND)
d.add(elm.Line().at(u1.VOUT).right().length(2))
n4 = d.here
d.add(elm.Line().right().length(2))
vdd(d, d.here, "+5V")
shunt_down(d, n4, lambda: elm.Capacitor(polar=True), "C2\n100µF 10V")
def block_enable(d, ox, oy):
title(d, (ox - 3, oy + 3.5), "2. HV enable relay + hardware watchdog")
# charge pump
d.add(elm.Dot(open=True).at((ox, oy)).label("ESP32 GPIO21\n(toggles every\nscanned row)", "left"))
d.add(elm.Capacitor().right().label("C3 100nF"))
a = d.here
d.add(elm.Diode().right().label("D2 1N4148"))
b = d.here
d.add(elm.Line().right().length(2.5))
c = d.here
d.add(elm.Line().right().length(2.5))
q1 = d.add(elm.NFet().theta(0).reverse().anchor("gate").label("Q1\n2N7000", "right"))
d.add(elm.Dot().at(a))
d.add(elm.Diode().at(a).down().reverse().label("D3\n1N4148", L_DOWN))
gnd(d, d.here)
shunt_down(d, b, lambda: elm.Capacitor(polar=True), "C4\n10µF")
shunt_down(d, c, lambda: elm.Resistor(), "R1\n100k")
gnd(d, q1.source)
# relay module
k1 = d.add(ic(["IN", "VCC"], ["NO", "COM"], "K1 relay module\n5V, opto, active-low IN",
bottom=["GND"]).at((q1.drain.x + 5, q1.drain.y + 2.5)).anchor("IN"))
d.add(elm.Wire("|-").at(q1.drain).to(k1.IN))
d.add(elm.Line().at(k1.VCC).left().length(1.5))
vdd(d, d.here, "+5V")
gnd(d, k1.GND)
d.add(elm.Line().at(k1.COM).right().length(2))
vdd(d, d.here, "+12V")
d.add(elm.Line().at(k1.NO).right().length(3).label("+12V_HV", "right"))
note(d, (ox - 6, oy - 4.5),
"Static GPIO (high, low, reset, crash) → no charge on C4 → Q1 off → relay open → HV off.\n"
"Drop-out ≈ 1 s (C4 × R1). Check your relay module's IN polarity.")
def block_pos(d, ox, oy):
title(d, (ox - 3, oy + 4.5), "3. Positive HV: boost module + original regulator → +65 V")
d.add(elm.Dot(open=True).at((ox, oy)).label("+12V_HV", "left"))
d.add(elm.Fuse().right().label("F2 1A T"))
d.add(elm.Line().right().length(1))
u2 = d.add(ic(["VIN"], ["HV+"], "U2 NCH6100HV\nset 90V", bottom=["GND"]).anchor("VIN"))
gnd(d, u2.GND)
y0 = oy
d.add(elm.Line().at(u2.absanchors["HV+"]).right().length(1))
d.add(elm.Fuse().right().label("F4 100mA F"))
x = d.here.x
d.add(elm.Label().at((x + 0.3, y0 + 0.2)).label("+90V raw", "top", fontsize=9, halign="left"))
# raw rail: bulk cap, bleeder, HV-present LED
shunt_down(d, (x + 1.5, y0), lambda: elm.Capacitor(polar=True), "C5\n22µF\n250V")
shunt_down(d, (x + 4.5, y0), lambda: elm.Resistor(), "R2\n330k")
d.add(elm.Dot().at((x + 7.5, y0)))
d.add(elm.Resistor().at((x + 7.5, y0)).down().label("R3\n100k 1W", R_DOWN))
d.add(elm.LED().down().label("LED1", R_DOWN))
gnd(d, d.here)
# regulator (original Q1/Q3 section)
xa, xb, xd = x + 12, x + 16, x + 21
d.add(elm.Resistor().at((xb, y0)).down().label("R4\n1.5k", L_DOWN))
qa = d.add(elm.BjtPnp(circle=True).theta(0).anchor("emitter").label("Q2\nMPSA92", "right"))
by = qa.base.y
d.add(elm.Dot().at((xa, y0)))
d.add(elm.Zener().endpoints((xa, y0), (xa, by)).reverse().label("D4\n3.9V\n1N5228", R_DOWN))
d.add(elm.Dot().at((xa, by)))
d.add(elm.Line().at((xa, by)).to(qa.base))
d.add(elm.Resistor().at((xa, by)).down().label("R5\n47k 1W", L_DOWN))
gnd(d, d.here)
qby = qa.collector.y - 1.2
d.add(elm.Line().at(qa.collector).toy(qby))
nqb = d.add(elm.Dot()).center
d.add(elm.Zener().at(nqb).down().reverse().label("D5\n68V\n1N4760A", R_DOWN))
gnd(d, d.here)
q3 = d.add(elm.BjtNpn(circle=True).theta(0).anchor("base").at((xd, qby))
.label("Q3\nMJE15030\n(heatsink)", "right"))
d.add(elm.Line().at(nqb).to(q3.base))
d.add(elm.Line().at(q3.collector).toy(y0))
d.add(elm.Line().at((x, y0)).tox(q3.collector.x))
oy2 = q3.emitter.y - 0.8
d.add(elm.Line().at(q3.emitter).toy(oy2))
d.add(elm.Line().right().length(2.5))
o1 = d.here
d.add(elm.Line().right().length(3))
o2 = d.here
d.add(elm.Line().right().length(3))
d.add(elm.Dot(open=True).label("+65V to panel", "right"))
shunt_down(d, o1, lambda: elm.Capacitor(), "C6\n0.1µF\n500V")
shunt_down(d, o2, lambda: elm.Resistor(), "R6\n330k")
def block_neg(d, ox, oy):
title(d, (ox - 3, oy + 4.5),
"4. Negative HV: isolated converter + floating boost module + original regulator → −112 V / −100 V")
d.add(elm.Dot(open=True).at((ox, oy)).label("+12V_HV", "left"))
d.add(elm.Fuse().right().label("F3 1A T"))
d.add(elm.Line().right().length(1))
u3 = d.add(ic(["VIN", "GND"], ["−VO", "+VO"], "U3 isolated DC-DC\n12V→12V 6–10W").anchor("VIN"))
d.add(elm.Line().at(u3.GND).left().length(1))
gnd(d, d.here)
d.add(elm.Line().at(u3.absanchors["+VO"]).right().length(2.5))
u4 = d.add(ic(["VIN"], ["HV+"], "U4 NCH6100HV\nset 130V", bottom=["GND"]).anchor("VIN"))
jy = u4.GND.y - 1.5
d.add(elm.Line().at(u3.absanchors["−VO"]).right().length(1.2))
d.add(elm.Line().toy(jy))
d.add(elm.Line().tox(u4.GND.x))
d.add(elm.Dot())
hv = u4.absanchors["HV+"]
d.add(elm.Line().at(hv).right().length(1.5))
gnd(d, d.here)
yb = u4.GND.y - 6 # −130 V raw rail
d.add(elm.Line().at(u4.GND).toy(yb))
d.add(elm.Fuse().right().label("F5 100mA F", "bottom"))
x = d.here.x
d.add(elm.Label().at((x + 0.3, yb - 0.3)).label("−130V raw", "bottom", fontsize=9, halign="left"))
d.add(elm.EncircleBox([u4], padx=1.0, pady=1.2).linestyle("--").color("red"))
note(d, (u3.center.x - 3, yb - 0.8),
"⚠ U3's output side, U4 and this whole\nnet sit at −130 V: insulate, never touch.", 8, "red")
shunt_up(d, (x + 1.5, yb), lambda: elm.Capacitor(polar=True).reverse(), "C7\n22µF\n250V")
shunt_up(d, (x + 4.5, yb), lambda: elm.Resistor(), "R7\n330k")
d.add(elm.Dot().at((x + 7.5, yb)))
d.add(elm.Resistor().at((x + 7.5, yb)).up().label("R8\n100k 1W", R_UP))
d.add(elm.LED().up().reverse().label("LED2", L_UP))
gnd_up(d, d.here)
# regulator (original Q2/Q4/Q5 section)
xa, xb, xz, xd = x + 12, x + 16, x + 19, x + 23
d.add(elm.Resistor().at((xb, yb)).up().label("R9\n1.5k", L_UP))
qa = d.add(elm.BjtNpn(circle=True).theta(0).anchor("emitter").label("Q4\nMPSA42", "right"))
by = qa.base.y
d.add(elm.Dot().at((xa, yb)))
d.add(elm.Zener().endpoints((xa, yb), (xa, by)).label("D6\n3.9V\n1N5228", L_UP))
d.add(elm.Dot().at((xa, by)))
d.add(elm.Line().at((xa, by)).to(qa.base))
d.add(elm.Resistor().at((xa, by)).up().label("R10\n47k 1W", L_UP))
gnd_up(d, d.here)
qby = qa.collector.y + 0.6
d.add(elm.Line().at(qa.collector).toy(qby))
d.add(elm.Line().tox(xz))
nqb = d.add(elm.Dot()).center
d.add(elm.Zener().at(nqb).up().label("D7\n13V\n1N4743", R_UP))
m = d.add(elm.Dot()).center
d.add(elm.Zener().up().label("D8\n100V\n1N4764A", R_UP))
gnd_up(d, d.here)
q4 = d.add(elm.BjtPnp(circle=True).theta(0).anchor("base").at((xd, qby))
.label("Q5\nMJE15031\n(heatsink)", "right"))
d.add(elm.Line().at(nqb).to(q4.base))
d.add(elm.Line().at(q4.collector).toy(yb))
d.add(elm.Line().at((x, yb)).tox(q4.collector.x))
# −112 V output
out_x = xd + 17
y112 = q4.emitter.y + 0.6
d.add(elm.Line().at(q4.emitter).toy(y112))
d.add(elm.Line().right().length(6))
o1 = d.here
d.add(elm.Line().right().length(3))
o2 = d.here
d.add(elm.Line().tox(out_x))
d.add(elm.Dot(open=True).label("−112V to panel", "right"))
shunt_up(d, o1, lambda: elm.Capacitor(), "C8\n0.1µF\n500V")
shunt_up(d, o2, lambda: elm.Resistor(), "R11\n330k")
# −100 V: Q5 follower driven from node M (D6/D7 junction)
yq5 = m.y + 5.5
d.add(elm.Line().at(m).tox(m.x + 1.5))
d.add(elm.Line().toy(yq5))
d.add(elm.Resistor().right().tox(xd + 4.5).label("R12 1.5k", "top"))
q5 = d.add(elm.BjtNpn(circle=True).theta(0).anchor("base").label("Q6\nMJE15030\n(heatsink)", "right"))
d.add(elm.Resistor().at(q5.collector).up().label("R13\n2k 5W", R_UP))
gnd_up(d, d.here)
y100 = q5.emitter.y - 0.6
d.add(elm.Line().at(q5.emitter).toy(y100))
d.add(elm.Line().right().length(6))
o3 = d.here
d.add(elm.Line().tox(out_x))
d.add(elm.Dot(open=True).label("−100V to panel", "right"))
shunt_up(d, o3, lambda: elm.Capacitor(), "C9\n0.1µF\n500V")
def block_out(d, ox, oy):
title(d, (ox - 4, oy + 3.5), "5. Panel power connector")
names = ["+5V", "+12V", "+65V", "GND", "−100V", "−112V"]
j2 = d.add(ic(list(reversed(names)), [], "J2 to panel\n(keyed, ≥300V rated)").at((ox, oy)).anchor("+5V"))
for n in names:
if n == "+12V":
d.add(elm.Switch().at(j2.absanchors[n]).left().length(3).label("JP1", "top"))
d.add(elm.Label().at(d.here).label(n, "left"))
else:
d.add(elm.Line().at(j2.absanchors[n]).left().length(3).label(n, "left"))
note(d, (ox - 4, j2.absanchors["−112V"].y - 1.5),
"Pin order is a placeholder: map it to YOUR panel's\nconnector before wiring. Fit JP1 only if the\npanel uses +12V.")
def block_notes(d, ox, oy):
title(d, (ox, oy), "Notes")
note(d, (ox, oy - 0.4),
"• Panel voltages are set by zeners D5 (68V → about +65V out) and D7+D8 (113V → about −112V / −100V out), copied from Stern 520-5138-00.\n"
" Module trim pots only set the raw rails, so a mis-set module can't over-volt the panel.\n"
"• Original board refs → this design: Q1→Q2, Q2→Q4, Q3→Q3, Q4→Q5, Q5→Q6, D3→D4, D4→D6, D5→D5, D6→D8, D7→D7, R2→R5, R3→R10, R4→R4, R5→R9,\n"
" R6→R6, R7→R11, R8→R13, R10→R12, C3→C6, C4→C8, C7→C9. Designators match the KiCad project.\n"
"• Set U2/U4 with no load, then lock the trim pots. First power-up with the panel: 1 kΩ in series with each HV output, measure current.\n"
"• Q3/Q4/Q5 on small heatsinks. HV transistors and zeners from a real distributor. Raw caps ≥ 250 V, output caps 500 V, ≥ 3 mm HV clearance.\n"
"• Bleeders discharge all rails at power-off; LEDs are a hint only. Always measure before touching.",
size=9)
with schemdraw.Drawing(show=False) as d:
d.config(fontsize=9, unit=2.5)
title(d, (-8, 8), "Plasma DMD — 12 V DC input power supply (rev 0.1, unverified)")
block_input(d, 0, 0)
block_enable(d, 42, 0)
block_pos(d, 0, -17)
block_out(d, 58, -14)
block_neg(d, 0, -36)
block_notes(d, -8, -56)
d.save(str(OUT / "psu-schematic.svg"))
d.save(str(OUT / "psu-schematic.png"), dpi=150)
print("saved")
Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB