Compare commits

..

11 commits

Author SHA1 Message Date
Me Here
3192f3debc PM_X-1 0.0.1: Pimoroni Explorer sibling firmware + Kit 0.0.23 device-id reply
Adds pico-explorer/ as a parallel CircuitPython firmware target alongside the 52Pi
Kit in pico-cp/. Same engine, same program-string grammar, same programs.json, same
live-sync protocol. Read-only on the device (no on-device beat editing); the web
editor's Live sync mirrors all edits in real time and the Explorer emits its own
play/stop/bpm/sel deltas back.

Hardware (Pimoroni Explorer PIM744):
- RP2350B + 2.8" ST7789V 320x240 LCD (8-bit parallel; CircuitPython's official
  board definition pre-builds the BusDisplay so we just use board.DISPLAY).
- 6 user buttons - A/B/C on the left of the screen, X/Y/Z on the right.
- Piezo speaker on GP12 (PWM) with amp enable on GP13.
- I2C QwSTEMMA on GP20/21 - reserved, unused by the firmware.
- No touchscreen, no joystick, no RGB LED. Run state shows on a tiny on-screen dot.

Buttons:
- A = play/stop. B = tap tempo. C = menu.
- X = prev track (hold-repeat). Z = next track (hold-repeat).
- Y = tempo -1 (hold-repeat; -5 after 1.5s).
- X+Z chord = tempo +1 (mirrors Y).
- In a menu: X/Z move the row cursor, Y decrements, A cycles/increments/selects,
  B = back, C = close.

Files added:
- pico-explorer/{boot.py, code.py, app.py, programs.json, README.md}.
  app.py = 1444 lines (~73KB source -> 29.8KB compiled .mpy).
- info-explorer.html.

Files touched:
- pico-cp/app.py: bump to 0.0.23. Version-query (SysEx 0x02 -> 0x03) reply now
  includes the device id as "K;<version>" (backward-compat: editor parses
  "contains ';'?" - old firmware sent bare version, treated as K).
- editor.html + editor-beta.html: _parseDeviceReply() splits id;version, FW_PATHS
  maps id to .py/.mpy URL pair, so Update firmware now pushes the right binary.
- build.sh + deploy.sh: precompile pico-explorer/app.py -> dist/explorer-app.mpy,
  zip pm_x1_circuitpy.zip alongside pm_k1_circuitpy.zip, ship
  pico-explorer-app.{py,mpy} next to pico-cp-app.{py,mpy}.
- docs/livesync-protocol.md: new section 7 - per-device emit/apply matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 20:43:38 -05:00
Me Here
617bb5a8b2 PM_K-1 hardware: integrate audio chain into one netlist (dedup shared parts)
circuits/audio_chain.py wires stages 1/1b/2/3/4 with shared nets and deduplicated
parts: ONE OPA1612 (U4) does both the Stage-2 filter (A) and Stage-3 summer (B);
ONE ULN2003 (U6) drives all three relays (K1 select, K2 mute, K3 ground-lift).
54 components, ERC 0 errors, netlist 0 errors. Per-stage files remain as the
documented, individually-simulated building blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:41:39 -05:00
Me Here
5a75dbbbdc PM_K-1 hardware: Stage 4 -- balanced output driver (completes the audio chain)
THAT1646 balanced driver (pinout verified, THAT doc 600078 rev07) closes the chain:
MIX_OUT -> 25-turn level-cal trim -> THAT1646 (+6dB, sense tied local) -> 47ohm
build-out -> fail-safe mute relay K2 -> balanced AOUT_HOT/COLD on the interconnect;
ground-lift relay K3 (de-energized=bonded, soft-lift 100R||10nF) -> CHASSIS.

- Phase: Stage 3 inversion corrected via HOT<-OUT-, COLD<-OUT+.
- Level cal trim ahead of the driver (its +6dB gain is fixed).
- K2 fail-safe: de-energized shorts both legs to GND after the build-out (driver
  current-limited). K3 ground-lift in series with a face panel switch.
- stage4_driver.cir: differential flat +4.76dB (1k=20k), legs antiphase (0 vs pi rad),
  build-out+cable rolloff above audio. ERC 0 errors; netlist 0 errors.

AUDIO CHAIN COMPLETE: stages 1, 1b, 2, 3, 4 all captured + simulated + ERC-clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:27:04 -05:00
Me Here
6b6a58fa56 PM_K-1 hardware: Stage 3 -- summing node (selected input + click)
Inverting summing amp (OPA1612 section) mixes STAGE1_OUT (line/instrument) and
CLICK_OUT (filtered DAC) at unity into MIX_OUT. Each source enters its own 10k into
the op-amp virtual ground, so they sum with no interaction.

stage3_sum.cir confirms: each input alone = 0 dB, both together = +6.02 dB, and each
input's gain is unchanged by the other (virtual-ground isolation). ERC/netlist 0 errors.

Note: inverting summer flips phase -> corrected at the Stage 4 balanced driver via
hot/cold assignment. At integration, this summer can use the parked 2nd half of the
Stage 2 filter OPA1612 (U4) instead of a separate package.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:15:17 -05:00
Me Here
2f44be6f63 PM_K-1 hardware: resolve TQ2SA relay pinout; Stage 2 DAC + reconstruction filter
Relay residuals resolved from the Panasonic TQ-SMD connection diagram + contact-
resistance terminal pairs: coil=1/10, pole1 COM=3/NC=4/NO=2, pole2 COM=8/NC=7/NO=9,
pins 5/6 unused. (NC/NO sense also firmware-correctable via the GPIO drive.)
Stage 1b encoding already matched; docstring updated to "resolved".

Stage 2 (click source): PCM5102A DAC + 2nd-order Sallen-Key reconstruction filter.
- PCM5102A pinout verified (TI SLAS859C, TSSOP-20). 2.1Vrms GND-centered out (no
  DC-block), charge-pump flying cap + VNEG, DEMP/FLT/FMT tied for I2S/normal/no-deemph,
  SCK<-low-jitter MCLK, BCK/DIN/LRCK<-RP2350, XSMT pulled-up soft-mute.
- OPA1612 Sallen-Key LPF on OUTL. stage2_recon.cir confirms flat to 20kHz, -3dB at
  74.8 kHz -- cleans delta-sigma HF residue without touching audio.
- ERC 0 errors; netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 20:06:26 -05:00
Me Here
e6f425ee6f PM_K-1 hardware: Stage 1b -- Hi-Z instrument DI buffer + line/inst select relay
OPA1641 non-inverting DI buffer (1Mohm in, +12dB) + TQ2SA DPDT relay that both
routes the jack tip (line receiver vs DI buffer) and selects the output. Default
de-energized = LINE (common case, fail-safe). Driven by the shared ULN2003 via
net K1_DRV from GPIO SEL_LINST.

Pinouts verified from datasheets before capture (per the no-guessing rule):
- OPA1641 (TI SBOS484D): 1=NC 2=-IN 3=+IN 4=V- 5=NC 6=OUT 7=V+ 8=NC.
- ULN2003A: GND=8, COM=9, in 1-7 / out 16-10.
- TQ2SA (Panasonic TQ-SMD): pole1 COM=3 throws 2/4, pole2 COM=8 throws 7/9
  (from contact-resistance terminal pairs). NC/NO orientation + coil pins (1/10)
  follow the standard single-side-stable diagram -- flagged in-file for a final
  connection-diagram cross-check (not over-claimed).

ngspice stage1b_di.cir confirms +12.04dB gain, flat across the audio band.
ERC 0 errors; netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:56:55 -05:00
Me Here
075c1786af PM_K-1 hardware: verify THAT1240 against datasheet; fix pinout + part numbers
Verified the receiver from THAT doc 600035 rev05 instead of guessing:
- THAT1240 = 0 dB (unity) -- correct as specced; 1243=-3dB, 1246=-6dB would be wrong.
- SO-8 pinout 1=Ref 2=In- 3=In+ 4=Vee 5=Sense 6=Vout 7=Vcc 8=NC. My initial
  SKiDL pins were mostly wrong; corrected. Netlist now matches the datasheet.
- KiCad Device:D is pin1=K/pin2=A; my clamp diodes were reversed -- fixed so they
  actually clamp (D high->cathode to +15, D low->anode to -15).
- BOM part numbers had a bogus "W16" suffix; corrected to S08-U (SO-8). Noted
  INA134/SSM2141 as pin-compatible 2nd sources for long-term availability.

ERC 0 errors, netlist 0 errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:48:41 -05:00
Me Here
3f7f4b94d9 PM_K-1 hardware: Stage 1 input receiver as SKiDL (code-defined schematic)
Capture method = SKiDL per decision. circuits/stage1_input.py defines the
balanced line receiver + per-leg protection (DC-block film cap, series R, bias R,
clamp diodes to the rails) and emits a KiCad netlist. ERC: 0 errors (2 expected
warnings -- AIN_HOT/COLD reach only one pin until the interconnect block exists).

Container: env vars point SKiDL/KiCad at the symbol/footprint libs.

VERIFY-before-layout flagged in-file: exact THAT124x gain suffix, its SO-8 pin
numbers, clamp-diode orientation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:43:07 -05:00
Me Here
d51c9f1011 PM_K-1 hardware: Stage 1 audio (input receiver) sims + container libs
Audio chain, stage 1 (balanced input receiver + protection) validated in ngspice:
- stage1_cmrr.cir: CMRR vs resistor matching -> 1% = 46dB, 0.1% = 66dB, perfect
  = amp-limited; justifies the laser-trimmed THAT1240 over discrete resistors.
- stage1_phantom.cir: +48V phantom step -> clamped to ~16V blip, steady-state
  ~0.12V; the DC-block cap + clamp + series R make a miswire a non-event.

Container: add kicad-symbols + kicad-footprints (for symbol placement) and skidl.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:27:25 -05:00
Me Here
bcfa5dd7f0 PM_K-1 hardware: reproducible EDA container (KiCad 9 + ngspice)
Pinned toolchain under hardware/eda/ so the design can be checked/simulated
identically in the future (system KiCad is 7.0, which has no CLI ERC):
- Containerfile: Ubuntu 24.04 + KiCad 9 (PPA) + ngspice + python3.
- run.sh: build-if-needed + run with the repo mounted; lands in hardware/kicad.
- sim/input_loading.cir: ngspice deck proving the line(25k) vs instrument(1M)
  input-loading decision — Hi-Z preserves a +16dB pickup resonance the 25k load
  flattens to -3dB.

Verified: KiCad 9.0.9, ngspice-42, ERC runs clean (0 violations) on
pm_k1_core.kicad_sch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:17:54 -05:00
Me Here
87caa933ea PM_K-1 hardware: core-board design-of-record + KiCad scaffold
Heirloom pro-audio modular brain/face design captured under hardware/:
- DESIGN.md: full spec (RP2350, ±15V studio rails via TPS65131+TPS7A LDOs,
  PCM5102A click, THAT1240/1646 balanced click-injector with switchable
  protected line/instrument input, fail-safe mute relay, series ground-lift,
  USB-MIDI default + DNP hardware MIDI, sig/clip detect, ESD/EMI, chassis),
  plus the two-interconnect pinouts (Pico-compatible digital ribbon + separate
  analog/MIDI).
- BOM.csv: manufacturer part numbers + rough costs.
- kicad/: valid KiCad 7 project + documented schematic canvas. PCB
  layout/routing remains the interactive step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:42:45 -05:00
56 changed files with 3987 additions and 23 deletions

View file

@ -19,6 +19,10 @@ MPYC="$PWD/tools/mpy-cross"; ROOT="$PWD"
[[ -x "$MPYC" ]] || { echo "error: $MPYC missing (Adafruit mpy-cross for CircuitPython 10.2.1)" >&2; exit 1; }
( cd pico-cp && "$MPYC" app.py -o "$ROOT/dist/app.mpy" ) # compile from pico-cp/ so tracebacks read "app.py"
echo "precompiled dist/app.mpy ($(stat -c%s dist/app.mpy) bytes <- $(stat -c%s pico-cp/app.py) source)"
# PM_X-1 Explorer firmware uses the same mpy-cross. Output as dist/explorer-app.mpy so the Kit + Explorer
# bundles each ship their own precompiled binary; the served URLs follow the same one-target-per-file rule.
( cd pico-explorer && "$MPYC" app.py -o "$ROOT/dist/explorer-app.mpy" )
echo "precompiled dist/explorer-app.mpy ($(stat -c%s dist/explorer-app.mpy) bytes <- $(stat -c%s pico-explorer/app.py) source)"
python3 - <<'PY'
import os, pathlib, re
@ -40,7 +44,7 @@ def build(name):
for name in ("index.html","editor.html","editor-beta.html","player.html","teacher.html","stage.html","micro.html","showcase.html","kit.html",
"embed.html",
"info-editor.html","info-player.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html","info-kit.html"):
"info-editor.html","info-player.html","info-teacher.html","info-stage.html","info-micro.html","info-showcase.html","info-kit.html","info-explorer.html"):
print("built %s (%dKB)" % (name, build(name) // 1024))
pathlib.Path("dist/embed.js").write_text(pathlib.Path("embed.js").read_text()) # loader, served as-is
print("copied embed.js")
@ -55,6 +59,12 @@ pathlib.Path("dist/pico-cp-app.py").write_text(_appsrc) # served for version r
# the editor pushes the PRECOMPILED .mpy (base64); serve it next to the source
pathlib.Path("dist/pico-cp-app.mpy").write_bytes(pathlib.Path("dist/app.mpy").read_bytes())
print("copied pico-cp-app.py + pico-cp-app.mpy")
_xsrc = pathlib.Path("pico-explorer/app.py").read_text() # PM_X-1 Explorer firmware (sibling to the Kit)
_xbad = [(i, c) for i, c in enumerate(_xsrc) if ord(c) > 0x7F]
assert not _xbad, "pico-explorer/app.py has non-ASCII at %r -- keep it ASCII (version regex + clean source)" % (_xbad[:5],)
pathlib.Path("dist/pico-explorer-app.py").write_text(_xsrc) # editor reads APP_VERSION from here
pathlib.Path("dist/pico-explorer-app.mpy").write_bytes(pathlib.Path("dist/explorer-app.mpy").read_bytes())
print("copied pico-explorer-app.py + pico-explorer-app.mpy")
import zipfile # PM_K-1 CircuitPython drive bundle (download → unzip onto CIRCUITPY)
with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
for f in ("code.py", "boot.py", "programs.json", "font_s.bin", "font_m.bin", "font_l.bin",
@ -63,4 +73,13 @@ with zipfile.ZipFile("dist/pm_k1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z
z.write("dist/app.mpy", "app.mpy") # the precompiled firmware (NOT app.py - too big to compile on-device)
z.write("dist/editor.html", "editor.html") # offline copy of the editor, on the drive
print("zipped pm_k1_circuitpy.zip")
# PM_X-1 Explorer drive bundle (download → unzip onto CIRCUITPY on the Pimoroni Explorer with CircuitPython for Pico 2)
with zipfile.ZipFile("dist/pm_x1_circuitpy.zip", "w", zipfile.ZIP_DEFLATED) as z:
for f in ("code.py", "boot.py", "programs.json", "README.md"):
z.write("pico-explorer/" + f, f)
for f in ("font_s.bin", "font_m.bin", "font_l.bin", "logo.bin", "midi.bin", "usb.bin"):
z.write("pico-cp/" + f, f) # fonts + icons are resolution-agnostic; reuse the Kit's baked blobs
z.write("dist/explorer-app.mpy", "app.mpy")
z.write("dist/editor.html", "editor.html")
print("zipped pm_x1_circuitpy.zip")
PY

View file

@ -42,7 +42,7 @@ fi
echo "deployed v$BUILD -> $DEST_DIR"
for f in index.html editor.html editor-beta.html player.html teacher.html stage.html micro.html showcase.html kit.html \
embed.html \
info-editor.html info-player.html info-teacher.html info-stage.html info-micro.html info-showcase.html info-kit.html; do
info-editor.html info-player.html info-teacher.html info-stage.html info-micro.html info-showcase.html info-kit.html info-explorer.html; do
sed "s|const APP_VERSION = \"[^\"]*\";|const APP_VERSION = \"$BUILD\";|" "$DIST_DIR/$f" > "$DEST_DIR/$f"
echo " $f ($(stat -c '%s' "$DEST_DIR/$f") bytes)"
done
@ -51,6 +51,9 @@ cp "$DIST_DIR/pico-main.py" "$DEST_DIR/pico-main.py"; echo " pico-main.py ($(st
cp "$DIST_DIR/pm_k1_circuitpy.zip" "$DEST_DIR/pm_k1_circuitpy.zip"; echo " pm_k1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_k1_circuitpy.zip") bytes)" # PM_K-1 CircuitPython bundle
cp "$DIST_DIR/pico-cp-app.py" "$DEST_DIR/pico-cp-app.py"; echo " pico-cp-app.py ($(stat -c '%s' "$DEST_DIR/pico-cp-app.py") bytes)" # served for version reading + reference
cp "$DIST_DIR/pico-cp-app.mpy" "$DEST_DIR/pico-cp-app.mpy"; echo " pico-cp-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-cp-app.mpy") bytes)" # precompiled firmware the editor pushes (base64)
cp "$DIST_DIR/pm_x1_circuitpy.zip" "$DEST_DIR/pm_x1_circuitpy.zip"; echo " pm_x1_circuitpy.zip ($(stat -c '%s' "$DEST_DIR/pm_x1_circuitpy.zip") bytes)" # PM_X-1 Explorer CircuitPython bundle
cp "$DIST_DIR/pico-explorer-app.py" "$DEST_DIR/pico-explorer-app.py"; echo " pico-explorer-app.py ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.py") bytes)" # served for version reading
cp "$DIST_DIR/pico-explorer-app.mpy" "$DEST_DIR/pico-explorer-app.mpy"; echo " pico-explorer-app.mpy ($(stat -c '%s' "$DEST_DIR/pico-explorer-app.mpy") bytes)" # PM_X-1 firmware (the editor pushes this when device id = X)
rm -f "$DEST_DIR/player-asbuilt.html" # renamed to teacher.html
rm -f "$DEST_DIR/concepts.html" # Concepts is now the landing (/)
# info-*.html are first-class pages again: each form factor has a lean widget page

View file

@ -193,3 +193,20 @@ target.
- Streaming the device practice log (`history.json`) up to the browser.
- Mirroring device `settings.json` (LED brightness, MIDI config, etc.).
- Multipeer / multieditor arbitration beyond lastwriterwins.
---
## 7. Perdevice emit/apply matrix
Both targets implement the **full apply path** for every verb. They differ in what
they **emit**, because ondevice editing differs:
| Device | Emits | Applies |
|-------------|----------------------------------------------------|---------------------------------------------|
| **PM_K1** Kit (touchscreen + joystick) | `play` / `stop` / `bpm` / `sel` / `beat` / `lane` (FULL on structural lane edits) | all of the above |
| **PM_X1** Explorer (6 buttons, readonly beats) | `play` / `stop` / `bpm` / `sel` only (no ondevice beat/lane editing) | all of the above |
Editors don't need to specialcase the source — both DELTA streams look identical on
the wire, and the **device id is only exposed on the version query** (SysEx `0x02`
`0x03` reply, `<id>;<version>`; pre0.0.23 firmware sends bare version → assume
`K`).

View file

@ -1211,34 +1211,42 @@ async function toggleDeviceAudio() {
function _queryDeviceVersion() { // ask the device its firmware version (SysEx 0x02 -> reply 0x03)
return new Promise((res) => { _verCb = res; _send([0xF0, 0x7D, 0x02, 0xF7]); setTimeout(() => { if (_verCb) { _verCb = null; res(null); } }, 1500); });
}
// 0.0.23+ devices reply "<id>;<version>" (e.g. "K;0.0.23", "X;0.0.1"); pre-0.0.23 send bare version -> assume K.
function _parseDeviceReply(s) {
if (!s) return { id: null, version: null };
const i = s.indexOf(";");
return i >= 0 ? { id: s.slice(0, i), version: s.slice(i + 1) } : { id: "K", version: s };
}
const FW_PATHS = { K: { py: "/pico-cp-app.py", mpy: "/pico-cp-app.mpy", label: "PM_K-1 Kit" },
X: { py: "/pico-explorer-app.py", mpy: "/pico-explorer-app.mpy", label: "PM_X-1 Explorer" } };
async function updateFirmware() { // A/B firmware update over USB-MIDI: push the precompiled .mpy
console.log("[fw] update start");
if (!(await _ensureMidi()) || !_midiOutputs().length) {
console.log("[fw] no MIDI output (outputs:", _midiOutputs().length, ")");
return alert("Connect the PM_K-1 (Chrome/Edge/Firefox), then try again.");
return alert("Connect the device (Chrome/Edge/Firefox), then try again.");
}
console.log("[fw] MIDI ok; outputs:", _midiOutputs().map((o) => o.name));
const dev = await _queryDeviceVersion();
console.log("[fw] device version reply:", dev);
const reply = await _queryDeviceVersion();
const { id: devId, version: dev } = _parseDeviceReply(reply);
const paths = FW_PATHS[devId] || FW_PATHS.K; // unknown id -> assume Kit (pre-0.0.23 firmware)
console.log("[fw] device reply:", reply, "-> id:", devId || "(none)", "version:", dev, "paths:", paths.label);
let latest = null, b64 = null;
// version comes from the (text) source; the payload is the precompiled .mpy bytecode — CircuitPython
// compiles a big .py at boot, which OOMs the RP2040, so we ship + push compiled bytecode instead.
for (const base of ["", "https://metronome.varasys.io"]) {
try { const t = await (await fetch(base + "/pico-cp-app.py", { cache: "no-store" })).text();
try { const t = await (await fetch(base + paths.py, { cache: "no-store" })).text();
const m = t.match(/APP_VERSION\s*=\s*["']([^"']+)["']/); if (m) latest = m[1]; } catch (_) {}
try { const r = await fetch(base + "/pico-cp-app.mpy", { cache: "no-store" });
try { const r = await fetch(base + paths.mpy, { cache: "no-store" });
if (r.ok) { b64 = _b64(new Uint8Array(await r.arrayBuffer())); break; } } catch (_) {}
}
if (!b64) { // offline: let the user pick app.mpy
alert("Can't reach the site.\n\nPick the firmware file (app.mpy) to flash — download it from\n" +
"metronome.varasys.io/pico-cp-app.mpy, or use the online editor at metronome.varasys.io/editor.html.");
"metronome.varasys.io" + paths.mpy + ", or use the online editor at metronome.varasys.io/editor.html.");
const u8 = await _pickBinary(); if (!u8) return;
b64 = _b64(u8); if (!latest) latest = "(picked .mpy)";
}
if (!latest) latest = "?";
console.log("[fw] latest:", latest, "| .mpy base64 length:", b64 && b64.length);
const upToDate = dev && dev === latest;
if (!confirm("Device firmware: " + (dev || "unknown") + "\nNew build: " + latest +
if (!confirm(paths.label + " firmware: " + (dev || "unknown") + "\nNew build: " + latest +
(upToDate ? "\n\nSame version. Re-install anyway?"
: "\n\nUpdate now? The device reboots, runs the new build, and auto-rolls-back if it fails to start."))) {
console.log("[fw] confirm returned false (cancelled, OR the browser is suppressing dialogs -> reload the page)");

View file

@ -1204,34 +1204,42 @@ async function toggleDeviceAudio() {
function _queryDeviceVersion() { // ask the device its firmware version (SysEx 0x02 -> reply 0x03)
return new Promise((res) => { _verCb = res; _send([0xF0, 0x7D, 0x02, 0xF7]); setTimeout(() => { if (_verCb) { _verCb = null; res(null); } }, 1500); });
}
// 0.0.23+ devices reply "<id>;<version>" (e.g. "K;0.0.23", "X;0.0.1"); pre-0.0.23 send bare version -> assume K.
function _parseDeviceReply(s) {
if (!s) return { id: null, version: null };
const i = s.indexOf(";");
return i >= 0 ? { id: s.slice(0, i), version: s.slice(i + 1) } : { id: "K", version: s };
}
const FW_PATHS = { K: { py: "/pico-cp-app.py", mpy: "/pico-cp-app.mpy", label: "PM_K-1 Kit" },
X: { py: "/pico-explorer-app.py", mpy: "/pico-explorer-app.mpy", label: "PM_X-1 Explorer" } };
async function updateFirmware() { // A/B firmware update over USB-MIDI: push the precompiled .mpy
console.log("[fw] update start");
if (!(await _ensureMidi()) || !_midiOutputs().length) {
console.log("[fw] no MIDI output (outputs:", _midiOutputs().length, ")");
return alert("Connect the PM_K-1 (Chrome/Edge/Firefox), then try again.");
return alert("Connect the device (Chrome/Edge/Firefox), then try again.");
}
console.log("[fw] MIDI ok; outputs:", _midiOutputs().map((o) => o.name));
const dev = await _queryDeviceVersion();
console.log("[fw] device version reply:", dev);
const reply = await _queryDeviceVersion();
const { id: devId, version: dev } = _parseDeviceReply(reply);
const paths = FW_PATHS[devId] || FW_PATHS.K; // unknown id -> assume Kit (pre-0.0.23 firmware)
console.log("[fw] device reply:", reply, "-> id:", devId || "(none)", "version:", dev, "paths:", paths.label);
let latest = null, b64 = null;
// version comes from the (text) source; the payload is the precompiled .mpy bytecode — CircuitPython
// compiles a big .py at boot, which OOMs the RP2040, so we ship + push compiled bytecode instead.
for (const base of ["", "https://metronome.varasys.io"]) {
try { const t = await (await fetch(base + "/pico-cp-app.py", { cache: "no-store" })).text();
try { const t = await (await fetch(base + paths.py, { cache: "no-store" })).text();
const m = t.match(/APP_VERSION\s*=\s*["']([^"']+)["']/); if (m) latest = m[1]; } catch (_) {}
try { const r = await fetch(base + "/pico-cp-app.mpy", { cache: "no-store" });
try { const r = await fetch(base + paths.mpy, { cache: "no-store" });
if (r.ok) { b64 = _b64(new Uint8Array(await r.arrayBuffer())); break; } } catch (_) {}
}
if (!b64) { // offline: let the user pick app.mpy
alert("Can't reach the site.\n\nPick the firmware file (app.mpy) to flash — download it from\n" +
"metronome.varasys.io/pico-cp-app.mpy, or use the online editor at metronome.varasys.io/editor.html.");
"metronome.varasys.io" + paths.mpy + ", or use the online editor at metronome.varasys.io/editor.html.");
const u8 = await _pickBinary(); if (!u8) return;
b64 = _b64(u8); if (!latest) latest = "(picked .mpy)";
}
if (!latest) latest = "?";
console.log("[fw] latest:", latest, "| .mpy base64 length:", b64 && b64.length);
const upToDate = dev && dev === latest;
if (!confirm("Device firmware: " + (dev || "unknown") + "\nNew build: " + latest +
if (!confirm(paths.label + " firmware: " + (dev || "unknown") + "\nNew build: " + latest +
(upToDate ? "\n\nSame version. Re-install anyway?"
: "\n\nUpdate now? The device reboots, runs the new build, and auto-rolls-back if it fails to start."))) {
console.log("[fw] confirm returned false (cancelled, OR the browser is suppressing dialogs -> reload the page)");

36
hardware/BOM.csv Normal file
View file

@ -0,0 +1,36 @@
Ref,Block,Part,Manufacturer,MPN,Qty,Approx_USD_ea,Notes
U1,MCU,RP2350A microcontroller,Raspberry Pi,RP2350A,1,1.10,QFN-60; mind E9 input-latch erratum (external pulldowns)
U2,MCU,16MB QSPI flash,Winbond,W25Q128JVSIQ,1,1.20,genuine part; firmware wear-levels history.json
Y1,MCU,12MHz crystal,Abracon,ABM8-272-12.000MHZ,1,0.30,+/-30ppm
U3,Power,3V3 IO LDO,Diodes Inc,AP2112K-3.3TRG1,1,0.25,digital domain
L1,Power,RP2350 core SMPS inductor,TDK,VLS3012,1,0.15,per RP2350 reference
U4,Power,Dual boost/inverter +/-18V,Texas Instruments,TPS65131RGER,1,2.50,raw +/-18V from 5V; guarded corner
U5,Power,Ultra-low-noise +15V LDO,Texas Instruments,TPS7A4901DGNR,1,1.80,post-regulates +18 to clean +15
U6,Power,Ultra-low-noise -15V LDO,Texas Instruments,TPS7A3001DGNR,1,1.90,post-regulates -18 to clean -15
J1,Power/USB,USB-C receptacle (TH-anchored),GCT,USB4085-GF-A,1,0.60,through-hole anchor tabs for strain
U7,Power/USB,USB ESD protection array,STMicroelectronics,USBLC6-2SC6,1,0.30,D+/D-/CC/VBUS
FL1,Power/USB,USB common-mode choke,Wurth,744232090,1,0.35,data-pair EMI
U8,Click,I2S audio DAC,Texas Instruments,PCM5102APWR,1,2.20,Burr-Brown; reliability-first
X1,Click,Low-jitter audio oscillator,Abracon,ASEM1-24.576MHZ-LR-T,1,1.40,dedicated MCLK; not PIO-jittered
U9,Audio-in,Balanced line receiver,THAT Corp,THAT1240S08-U,1,3.20,0dB unity; pinout verified doc600035 rev05; 2nd-src INA134/SSM2141
U10,Audio-in,JFET Hi-Z instrument buffer,Texas Instruments,OPA1641AID,1,1.60,>=1Mohm DI buffer + gain
U11,Audio-mix,Dual audio op-amp (sum/filter),Texas Instruments,OPA1612AIDR,1,2.40,signal-path low noise
U12,Audio-out,Balanced line driver,THAT Corp,THAT1646S08-U,1,3.30,near-zero source; 47ohm build-out per leg; pinout TBV at output stage
RV1,Audio-out,Output level cal trimmer 25-turn,Bourns,3296W-1-103LF,1,0.70,factory-set DAC FS -> +4dBu
U13,Indicator,Dual comparator (sig/clip),Texas Instruments,LM393DR,1,0.15,peak-detect -> RP2350 GPIO + LED lines
K1,Audio-in,Signal relay line/inst (DPDT gold),Panasonic,TQ2SA-5V,1,1.30,sealed gold bifurcated contacts
K2,Audio-out,Mute relay (fail-safe DPDT gold),Panasonic,TQ2SA-5V,1,1.30,de-energized=muted
K3,Audio-out,Ground-lift relay (gold),Panasonic,TQ2SA-5V,1,1.30,series with face panel switch; soft-lift 100R||10nF
U14,Control,Relay driver array,Texas Instruments,ULN2003ADR,1,0.20,drives K1-K3 with flyback
U15,RTC,Real-time clock (integrated xtal),Micro Crystal,RV-8803-C7,1,1.50,I2C on touch bus; drift irrelevant
BT1,RTC,CR2032 holder (socketed),Keystone,1066,1,0.30,user-replaceable
U16,MIDI(DNP),Opto-isolator MIDI IN,Vishay,H11L1M,1,0.55,DNP populate-option
U17,MIDI(DNP),Hex Schmitt buffer MIDI OUT/THRU,Nexperia,74LVC14APW,1,0.20,DNP populate-option
U18,Speaker(DNP),Class-D mono amp,Diodes Inc,PAM8302AASCR,1,0.35,DNP monitor option per face
J2,Interconnect,Digital ribbon header 2x13 shrouded keyed,Wurth,61303421821,1,0.45,Pico-pinout-compatible
J3,Interconnect,Analog audio header 2x5 shrouded keyed,Wurth,61301021821,1,0.35,twisted/shielded; away from digital
J4,Interconnect,MIDI header 1x6,Wurth,61300611121,1,0.20,used only if DNP MIDI populated
J5,Debug,SWD Cortex-Debug 2x5 1.27mm,Samtec,FTSH-105-01-L-DV-K,1,0.50,service header
D-arr,ESD,Interconnect ESD clamp arrays,Texas Instruments,TPD2E2U06DCKR,3,0.10,on user-touchable lines
PASS,Passives,Film caps / 0.1% thin-film R / clamp diodes / ferrites,various,various,1,4.00,WIMA film signal caps; Panasonic ERA 0.1%; no electrolytics in signal path
PCB,Fabrication,4-layer PCB ENIG finish,JLCPCB/PCBWay,custom,1,3.00,gold finish; qty-dependent
1 Ref Block Part Manufacturer MPN Qty Approx_USD_ea Notes
2 U1 MCU RP2350A microcontroller Raspberry Pi RP2350A 1 1.10 QFN-60; mind E9 input-latch erratum (external pulldowns)
3 U2 MCU 16MB QSPI flash Winbond W25Q128JVSIQ 1 1.20 genuine part; firmware wear-levels history.json
4 Y1 MCU 12MHz crystal Abracon ABM8-272-12.000MHZ 1 0.30 +/-30ppm
5 U3 Power 3V3 IO LDO Diodes Inc AP2112K-3.3TRG1 1 0.25 digital domain
6 L1 Power RP2350 core SMPS inductor TDK VLS3012 1 0.15 per RP2350 reference
7 U4 Power Dual boost/inverter +/-18V Texas Instruments TPS65131RGER 1 2.50 raw +/-18V from 5V; guarded corner
8 U5 Power Ultra-low-noise +15V LDO Texas Instruments TPS7A4901DGNR 1 1.80 post-regulates +18 to clean +15
9 U6 Power Ultra-low-noise -15V LDO Texas Instruments TPS7A3001DGNR 1 1.90 post-regulates -18 to clean -15
10 J1 Power/USB USB-C receptacle (TH-anchored) GCT USB4085-GF-A 1 0.60 through-hole anchor tabs for strain
11 U7 Power/USB USB ESD protection array STMicroelectronics USBLC6-2SC6 1 0.30 D+/D-/CC/VBUS
12 FL1 Power/USB USB common-mode choke Wurth 744232090 1 0.35 data-pair EMI
13 U8 Click I2S audio DAC Texas Instruments PCM5102APWR 1 2.20 Burr-Brown; reliability-first
14 X1 Click Low-jitter audio oscillator Abracon ASEM1-24.576MHZ-LR-T 1 1.40 dedicated MCLK; not PIO-jittered
15 U9 Audio-in Balanced line receiver THAT Corp THAT1240S08-U 1 3.20 0dB unity; pinout verified doc600035 rev05; 2nd-src INA134/SSM2141
16 U10 Audio-in JFET Hi-Z instrument buffer Texas Instruments OPA1641AID 1 1.60 >=1Mohm DI buffer + gain
17 U11 Audio-mix Dual audio op-amp (sum/filter) Texas Instruments OPA1612AIDR 1 2.40 signal-path low noise
18 U12 Audio-out Balanced line driver THAT Corp THAT1646S08-U 1 3.30 near-zero source; 47ohm build-out per leg; pinout TBV at output stage
19 RV1 Audio-out Output level cal trimmer 25-turn Bourns 3296W-1-103LF 1 0.70 factory-set DAC FS -> +4dBu
20 U13 Indicator Dual comparator (sig/clip) Texas Instruments LM393DR 1 0.15 peak-detect -> RP2350 GPIO + LED lines
21 K1 Audio-in Signal relay line/inst (DPDT gold) Panasonic TQ2SA-5V 1 1.30 sealed gold bifurcated contacts
22 K2 Audio-out Mute relay (fail-safe DPDT gold) Panasonic TQ2SA-5V 1 1.30 de-energized=muted
23 K3 Audio-out Ground-lift relay (gold) Panasonic TQ2SA-5V 1 1.30 series with face panel switch; soft-lift 100R||10nF
24 U14 Control Relay driver array Texas Instruments ULN2003ADR 1 0.20 drives K1-K3 with flyback
25 U15 RTC Real-time clock (integrated xtal) Micro Crystal RV-8803-C7 1 1.50 I2C on touch bus; drift irrelevant
26 BT1 RTC CR2032 holder (socketed) Keystone 1066 1 0.30 user-replaceable
27 U16 MIDI(DNP) Opto-isolator MIDI IN Vishay H11L1M 1 0.55 DNP populate-option
28 U17 MIDI(DNP) Hex Schmitt buffer MIDI OUT/THRU Nexperia 74LVC14APW 1 0.20 DNP populate-option
29 U18 Speaker(DNP) Class-D mono amp Diodes Inc PAM8302AASCR 1 0.35 DNP monitor option per face
30 J2 Interconnect Digital ribbon header 2x13 shrouded keyed Wurth 61303421821 1 0.45 Pico-pinout-compatible
31 J3 Interconnect Analog audio header 2x5 shrouded keyed Wurth 61301021821 1 0.35 twisted/shielded; away from digital
32 J4 Interconnect MIDI header 1x6 Wurth 61300611121 1 0.20 used only if DNP MIDI populated
33 J5 Debug SWD Cortex-Debug 2x5 1.27mm Samtec FTSH-105-01-L-DV-K 1 0.50 service header
34 D-arr ESD Interconnect ESD clamp arrays Texas Instruments TPD2E2U06DCKR 3 0.10 on user-touchable lines
35 PASS Passives Film caps / 0.1% thin-film R / clamp diodes / ferrites various various 1 4.00 WIMA film signal caps; Panasonic ERA 0.1%; no electrolytics in signal path
36 PCB Fabrication 4-layer PCB ENIG finish JLCPCB/PCBWay custom 1 3.00 gold finish; qty-dependent

211
hardware/DESIGN.md Normal file
View file

@ -0,0 +1,211 @@
# PM_K-1 Core Board ("brain") — design-of-record
VARASYS PolyMeter · heirloom pro-audio · modular brain/face architecture
Status: **design-of-record / pre-layout.** Component selection complete; PCB routing is the
remaining interactive step (see [Open items](#open-items)).
---
## 1. Philosophy
This is meant to be a device people hand down to their great-grandkids. The core board carries
**all the active electronics** and is specced to pro/audiophile tier with longevity and
serviceability as first-class goals. We do **not** value-engineer the audio path.
The user selects the face/enclosure/connector components; this document specs only the **core**.
## 2. Architecture — modular brain/face
- **Core ("brain"):** RP2350 + power + RTC + the full pro-audio analog chain + control logic.
One core design is reused across every form factor.
- **Face ("form factor"):** display, touch, joystick, buttons, LED, speaker, the physical
audio/MIDI connectors, panel switches, enclosure. **The core never decides connector type.**
- **Two interconnects, deliberately separate** (§7):
1. a **digital ribbon** whose pinout mirrors the Raspberry Pi Pico, so a stock Pico/Pico 2 on a
test adapter can drive any face board for bring-up;
2. an **analog interconnect** (+ a small MIDI interconnect) kept physically away from the fast
digital ribbon, because a balanced audio signal must never run parallel to the 24 MHz display SPI.
## 3. Block diagram
```
USB-C 5V ─┬─► 3V3 LDO ──────────────────────► RP2350 (core reg + ext L) + W25Q128 16MB + RV-8803 RTC(+CR2032)
│ │ I²S + low-jitter MCLK
└─► TPS65131 ±18V ─► TPS7A49/30 LDO ─► clean ±15V ▼
PCM5102A DAC ──► [summing: click + input]
bal IN ─[ESD/DC-block/clamp/series-R]─► [LINE/INST relay] ─┬─ THAT1240 receiver ──┘
(analog interconnect) └─ OPA1641 Hi-Z DI buffer (+gain)
THAT1646 balanced driver ─[47Ω build-out]─► [MUTE relay]─► bal OUT
shield ─[panel SW]─[GND-LIFT relay]─ gnd
sig/clip peak detect ─► LM393 ─► RP2350 GPIO (UI) + LED lines (interconnect)
RP2350 UART ─► [DNP: H11L1 opto IN + 74LVC14 buffer OUT] ─► MIDI interconnect (USB-MIDI = default)
```
## 4. Functional blocks
### 4.1 MCU + digital
- **RP2350A** (QFN-60, 30 GPIO). Chosen over RP2040 as the newest in-house part: dual M33+RISC-V,
520 KB RAM, secure boot, longest production runway. Firmware runs unchanged.
- **Erratum E9:** high-impedance inputs can latch — every input we read (face switches, buttons)
gets an **external pulldown**, never relies on the internal pad alone.
- Core supply via the RP2350 on-chip switched-mode regulator (external inductor); 3V3 IO from an
external LDO (AP2112K-3.3 / TLV75533).
- **Flash:** Winbond **W25Q128JV** (16 MB) — genuine part; the CircuitPython appliance bundles the
editor + tracks on a USB drive. Firmware does wear-leveling for `history.json`.
- **Crystal:** 12 MHz ±30 ppm.
- **Debug/service:** SWD 2×5 Cortex-Debug header + labeled test points (rails, I²S, audio nodes).
### 4.2 Click source (DAC)
- **TI/Burr-Brown PCM5102A** I²S DAC — reliability-first, widely stocked for future repair.
- Fed by a **dedicated low-jitter audio oscillator** (22.5792/24.576 MHz MEMS XO), **not** an MCLK
jittered out of the RP2350 PIO — jitter is audible as a raised noise floor.
### 4.3 Analog audio chain (the heart)
- **Input (balanced, switchable line/instrument):**
- **Line mode:** THAT1240 laser-trimmed balanced receiver (~high CMRR, no hand-matched resistors).
- **Instrument mode:** OPA1641 JFET Hi-Z buffer (≥1 MΩ) + ~+1015 dB gain (active DI).
- Selected by a **gold-contact signal relay** on the core (1 GPIO; touchscreen toggle, optional
face panel switch on a separate GPIO input).
- **Protection (non-negotiable):** series DC-blocking film cap (blocks +48 V phantom — the real
input-killer), clamp diodes/TVS to the rails, series current-limit resistor. A wrong-mode plug
then only *sounds* wrong; nothing is damaged.
- **Mix:** digital/firmware (touchscreen). Analog stage at unity; click level set via the DAC.
- **Output driver:** THAT1646 balanced line driver, **47 Ω build-out per leg** for
cable-capacitance stability and short-circuit tolerance (§5.1).
- **No electrolytics in the signal path** — film coupling caps (WIMA). 0.1 % thin-film resistors.
### 4.4 Output protection / conditioning
- **Power-up/down mute relay** — fail-safe, **de-energized = muted** (shorts hot+cold to gnd). A
hardware rail supervisor + RC turn-on delay un-mutes only after ±15 V settles; on power loss the
coil drops and mutes *faster than the rails can thump*. **Not MCU-dependent.** The MCU can *also*
assert mute (for clean line/inst flips and DAC reconfig).
- **Ground-lift** — both a **face panel switch** and a **core GPIO relay**, wired in **series** in
the shield-ground path: bonded only when both closed, either opening lifts it. **Soft lift =
100 Ω ∥ 10 nF** (not a hard open) so RF/safety keeps a path.
### 4.5 RTC
- **Micro Crystal RV-8803** (integrated 32.768 kHz crystal → no second crystal near the RP2350's
own) + **CR2032 in a socketed holder**. Shares the touch I²C bus (no extra GPIO). Drift is
irrelevant for a practice-log timestamp; reliability and zero-fuss layout win.
### 4.6 MIDI (default USB, hardware optional)
- **USB-MIDI is the default** and already in firmware — IN/OUT/THRU are software routing to a
computer/tablet host. Zero extra parts.
- **Dedicated DIN/TRS MIDI is a DNP populate-option:** the RP2350 UART lines route to the MIDI
interconnect with **H11L1 opto IN + 74LVC14 buffered OUT/THRU footprints left unpopulated**.
A "stage" face can populate them for laptop-free hardware sync. (USB-MIDI can't peer-to-peer with
standalone DIN gear — the device is a USB *peripheral*, not a host — which is why the hardware
option stays available.) Analog pulse/clock sync: **not included** (MIDI only).
### 4.7 Monitor speaker
- **DNP-optional** class-D amp (PAM8302) footprint on the core; speaker +/- routed on the analog
interconnect. Populated only for form factors that want a built-in monitor.
## 5. The five remaining pro details (decided)
### 5.1 Output impedance & level calibration
- THAT1646 source impedance is near-zero; **47 Ω build-out resistors** per leg give stable driving
into long/capacitive cables and survive a shorted output.
- **Level calibration:** a 25-turn precision trimmer (Bourns 3296W) in the driver gain network,
factory-set so DAC full-scale → **+4 dBu nominal**, leaving ~**+24 dBu** peak headroom on ±15 V.
Set-and-forget on the core; not a face control.
### 5.2 Signal / clip indication
- A peak detector (Schottky + hold cap) on the input (signal-present, ~40 dBu) and on the
driver-input/summing node (clip, within ~3 dB of rail) feeds an **LM393** dual comparator.
- Comparator outputs go to **RP2350 GPIOs** (clip/signal shown on the touchscreen) **and** are
mirrored to **SIG/CLIP LED drive lines on the digital interconnect** so a face can fit discrete LEDs.
### 5.3 ESD / EMI hardening
- **USB-C:** USBLC6-2SC6 (or TPD4E05U06) ESD array on D±/CC/VBUS; common-mode choke on the data
pair; shell bonded to chassis via RC; ferrite + TVS + bulk on VBUS.
- **Interconnects:** ~33 Ω series on fast SPI lines for edge-rate control; ESD clamp arrays on any
line reaching a user-touchable cable; interleaved ground pins; ferrite beads where 3V3/5V cross
into the analog domain.
- **Board:** full ground planes + stitching vias; the boost/inverter switcher lives in a guarded
corner away from the analog section; analog/digital grounds meet at a single **star point**.
- **Heirloom option:** conformal coating for humidity/longevity (build-time choice).
### 5.4 Chassis / strain-relief (core-side)
- 4× **M3 mounting holes** with keep-outs; a dedicated chassis-ground pad/pin.
- **Through-hole-anchored USB-C** jack (SMD-only tabs shear off with cable wiggle — unacceptable for
a 50-year device).
- **Shrouded, keyed, latching** interconnect headers (can't insert backward or vibrate loose).
- Panel strain-relief and connector mounting live on the face/enclosure.
### 5.5 Interconnect pinout — see §7.
## 6. Power tree
| Rail | Source | Part | Notes |
|---|---|---|---|
| +5 V | USB-C VBUS | — | ferrite + TVS + bulk |
| +3V3 (IO) | LDO from 5 V | AP2112K-3.3 / TLV75533 | digital domain |
| +1.1 V core | RP2350 internal SMPS | (external inductor) | per RP2350 ref |
| ±18 V (raw) | dual boost/inverter from 5 V | **TPS65131** | switcher, guarded corner |
| **±15 V (clean)** | ultra-low-noise LDO | **TPS7A4901 (+) / TPS7A3001 ()** | feeds all audio op-amps |
## 7. Interconnect pinouts
### 7.1 Digital ribbon — 2×13 (26-pin) IDC, Pico-pinout-compatible
Grounds interleaved around SPI. A Pico/Pico 2 test adapter maps these to the listed GP.
| Pin | Signal | GP | | Pin | Signal | GP |
|----|--------|----|--|----|--------|----|
| 1 | +5V | — | | 2 | GND | — |
| 3 | +3V3 | — | | 4 | GND | — |
| 5 | SPI_SCK | GP2 | | 6 | GND | — |
| 7 | SPI_MOSI | GP3 | | 8 | LCD_CS | GP5 |
| 9 | LCD_DC | GP6 | | 10 | LCD_RST | GP7 |
| 11 | GND | — | | 12 | I2C_SDA | GP8 |
| 13 | I2C_SCL | GP9 | | 14 | GND | — |
| 15 | JOY_X (ADC0) | GP26 | | 16 | JOY_Y (ADC1) | GP27 |
| 17 | BTN_A | GP15 | | 18 | BTN_B | GP14 |
| 19 | WS2812 | GP12 | | 20 | GND | — |
| 21 | GNDLIFT_SW (in) | GP21 | | 22 | LINEINST_SW (in) | GP22 |
| 23 | SIG_LED | GP19 | | 24 | CLIP_LED | GP20 |
| 25 | GND | — | | 26 | GND | — |
*I²S (BCK/LRCK/DOUT), the relays (line/inst route GP16, mute GP18, gnd-lift GP17), and MCLK stay
on-core — they are not on the ribbon. A Pico test brain drives the digital/face I/O above but
**cannot** drive the analog chain (DAC/op-amps are core-only).*
### 7.2 Analog audio interconnect — 2×5 (10-pin), twisted/shielded, away from digital
| Pin | Signal | | Pin | Signal |
|----|--------|--|----|--------|
| 1 | AOUT_HOT | | 2 | AGND |
| 3 | AOUT_COLD | | 4 | CHASSIS/SHIELD (face side of ground-lift) |
| 5 | AIN_HOT | | 6 | AGND |
| 7 | AIN_COLD | | 8 | SPK+ (DNP) |
| 9 | AGND | | 10 | SPK (DNP) |
### 7.3 MIDI interconnect — 1×6, only if DNP MIDI populated
| Pin | Signal |
|----|--------|
| 1 | MIDI_OUT_A (TRS-A tip/ring leg) |
| 2 | MIDI_OUT_B |
| 3 | MIDI_IN_A (to opto) |
| 4 | MIDI_IN_B |
| 5 | +5V (OUT drive) |
| 6 | GND / shield |
## 8. BOM
Full part list with manufacturer numbers and rough costs in **`hardware/BOM.csv`**. Headline parts:
RP2350A · W25Q128JV · PCM5102A · THAT1240 + THAT1646 · OPA1641 · OPA1612 · TPS65131 + TPS7A4901/3001 ·
RV-8803 · USBLC6-2SC6 · 3× Panasonic TQ2SA gold-contact relays · H11L1 (DNP).
## 9. Manufacturing
- **PCB:** ENIG (gold) finish — non-negotiable for decades of reliable contacts/solderability.
- **Assembly:** JLCPCB/PCBWay PCBA, ~5-board prototype minimum; ~$80200 first run. Core parts cost
~$2540/board one-off (pro op-amps + relays dominate), trending toward ~$1520 at qty 100.
- Most expensive items are the THAT audio ICs and the relays — that's where "heirloom" lives.
## 10. Open items
- **PCB layout/routing is the interactive next step** — placement, controlled-impedance USB pair,
star ground, switcher isolation, copper pours, DRC. The KiCad project under `hardware/kicad/`
is a documented schematic canvas + design-of-record; symbol placement and wiring happen in
Eeschema. `kicad-cli` is used here for ERC and PDF export verification.
- Confirm the exact 3.5" ST7796/GT911 panel connector (FPC vs header) before finalizing the face.
- Decide MIDI connector (TRS-MIDI Type-A vs DIN-5) per form factor — face decision.

View file

@ -0,0 +1,36 @@
# Reproducible EDA toolchain for the PM_K-1 core board.
#
# Why this exists: the system KiCad is 7.0 (no CLI ERC). This pins a known,
# rebuildable environment so the design can be checked/simulated identically
# years from now — fitting for a device meant to outlive its tools.
#
# KiCad 9 -> schematic capture, CLI ERC/DRC, netlist/PDF/Gerber export
# ngspice -> SPICE simulation of the analog audio circuits
# python3 -> scripting / BOM / skidl-style helpers
#
# Build/run via ../eda/run.sh (or: podman build -t pmk1-eda:9.0 .)
FROM docker.io/library/ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && apt-get install -y --no-install-recommends \
software-properties-common ca-certificates gnupg && \
add-apt-repository -y ppa:kicad/kicad-9.0-releases && \
apt-get update && apt-get install -y --no-install-recommends \
kicad \
kicad-symbols \
kicad-footprints \
ngspice \
python3 python3-pip python3-venv \
git make && \
apt-get clean && rm -rf /var/lib/apt/lists/* && \
pip3 install --no-cache-dir --break-system-packages skidl
# Point SKiDL / KiCad CLI at the installed libraries (reproducible, not ad-hoc).
ENV KICAD9_SYMBOL_DIR=/usr/share/kicad/symbols \
KICAD9_FOOTPRINT_DIR=/usr/share/kicad/footprints \
KICAD_SYMBOL_DIR=/usr/share/kicad/symbols
WORKDIR /work
CMD ["bash"]

36
hardware/eda/README.md Normal file
View file

@ -0,0 +1,36 @@
# PM_K-1 EDA environment
A reproducible container with the tools to design, check, and simulate the core board —
so the work doesn't depend on whatever happens to be installed on a given machine, now or
in 50 years.
## What's inside
- **KiCad 9** — schematic capture + PCB layout, and a CLI (`kicad-cli`) that can run
**ERC** (Electrical Rules Check) and DRC, and export netlists/PDF/Gerbers.
- **ngspice** — SPICE simulator for validating the analog audio circuits before we commit
copper (op-amp stages, filters, input loading, etc.).
- **python3** — scripting, BOM munging, optional code-defined-schematic helpers.
## Why a container?
The system KiCad here is 7.0, whose CLI can't run ERC (that arrived in KiCad 8). Rather than
fight the host, we pin a known toolchain. Anyone — including future-you — rebuilds the exact
environment with one command.
## Use it
```bash
cd hardware/eda
./run.sh # interactive shell, lands in hardware/kicad/
./run.sh kicad-cli version # confirm KiCad 9
./run.sh kicad-cli sch erc pm_k1_core.kicad_sch # run ERC on the schematic
./run.sh ngspice -b ../eda/sim/input_loading.cir # run a simulation (cwd is kicad/)
```
`run.sh` builds the image on first use, then mounts the whole repo at `/work` (so KiCad sees
`hardware/`). Use `RUNTIME=docker ./run.sh …` to use Docker instead of Podman.
## Layout
```
eda/
Containerfile # the pinned toolchain (KiCad 9 + ngspice + python)
run.sh # build-if-needed + run with the repo mounted
sim/ # ngspice decks (SPICE simulations of the analog circuits)
```

View file

@ -0,0 +1,195 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - INTEGRATED board netlist (SKiDL).
Wires stages 1, 1b, 2, 3, 4 into one netlist with shared nets and DEDUPLICATED parts:
* the Stage-2 reconstruction filter and the Stage-3 summer share ONE OPA1612 dual
(U4: section A = filter, section B = summer) -- no parked half.
* the three relays (K1 select, K2 mute, K3 ground-lift) share ONE ULN2003 (U6).
This is the source of truth for the integrated audio netlist; the per-stage circuits/
files remain as the documented, individually-simulated building blocks.
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/audio_chain.py
Outputs ERC + hardware/kicad/audio_chain.net.
All IC pinouts datasheet-verified (see the per-stage files for the citations):
THAT1240 1=Ref 2=In- 3=In+ 4=Vee 5=Sns 6=Vout 7=Vcc 8=NC
OPA1641 1=NC 2=-IN 3=+IN 4=V- 5=NC 6=OUT 7=V+ 8=NC
OPA1612 1=OUTA 2=-INA 3=+INA 4=V- 5=+INB 6=-INB 7=OUTB 8=V+
PCM5102A (TSSOP-20) per TI SLAS859C
THAT1646 1=Out- 2=Sns- 3=Gnd 4=In 5=Vee 6=Vcc 7=Sns+ 8=Out+
TQ2SA coil 1/10; pole1 COM3/NC4/NO2; pole2 COM8/NC7/NO9
ULN2003A in 1B-7B=1..7, out 1C-7C=16..10, GND=8, COM=9
"""
import os
from skidl import *
set_default_tool(KICAD9)
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
D = Part("Device", "D", dest=TEMPLATE, footprint="Diode_SMD:D_SOD-323")
# ---------------- nets ----------------
p15, n15, p3v3, p5, gnd = Net("+15V"), Net("-15V"), Net("+3V3"), Net("+5V"), Net("GND")
for n in (p15, n15, p3v3, p5, gnd):
n.drive = POWER
# audio signal flow
ain_hot, ain_cold = Net("AIN_HOT"), Net("AIN_COLD") # input jack tip/ring
rx_hot_in = Net("RX_HOT_IN") # relay-routed hot -> receiver
rx_out, di_in, di_out = Net("RX_OUT"), Net("DI_IN"), Net("DI_OUT")
stage1_out, click_out, mix_out = Net("STAGE1_OUT"), Net("CLICK_OUT"), Net("MIX_OUT")
aout_hot, aout_cold, chassis = Net("AOUT_HOT"), Net("AOUT_COLD"), Net("CHASSIS")
# DAC clocks/data + control
mclk, i2s_bck, i2s_din, i2s_lrck = Net("MCLK"), Net("I2S_BCK"), Net("I2S_DIN"), Net("I2S_LRCK")
dac_xsmt = Net("DAC_XSMT")
sel_linst, mute_en, gndlift_en = Net("SEL_LINST"), Net("MUTE_EN"), Net("GNDLIFT_EN")
k1_drv, k2_drv, k3_drv = Net("K1_DRV"), Net("K2_DRV"), Net("K3_DRV")
# ---------------- part definitions ----------------
def mk(name, pins, ref_prefix="U", fp="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm"):
return Part(name=name, tool=SKIDL, dest=TEMPLATE, ref_prefix=ref_prefix, footprint=fp, pins=pins)
P = Pin.types
THAT1240 = mk("THAT1240", [Pin(num=1,name="REF",func=P.INPUT),Pin(num=2,name="IN-",func=P.INPUT),
Pin(num=3,name="IN+",func=P.INPUT),Pin(num=4,name="V-",func=P.PWRIN),Pin(num=5,name="SENSE",func=P.PASSIVE),
Pin(num=6,name="OUT",func=P.OUTPUT),Pin(num=7,name="V+",func=P.PWRIN),Pin(num=8,name="NC",func=P.NOCONNECT)])
OPA1641 = mk("OPA1641", [Pin(num=1,name="NC1",func=P.NOCONNECT),Pin(num=2,name="-IN",func=P.INPUT),
Pin(num=3,name="+IN",func=P.INPUT),Pin(num=4,name="V-",func=P.PWRIN),Pin(num=5,name="NC5",func=P.NOCONNECT),
Pin(num=6,name="OUT",func=P.OUTPUT),Pin(num=7,name="V+",func=P.PWRIN),Pin(num=8,name="NC8",func=P.NOCONNECT)])
OPA1612 = mk("OPA1612", [Pin(num=1,name="OUTA",func=P.OUTPUT),Pin(num=2,name="-INA",func=P.INPUT),
Pin(num=3,name="+INA",func=P.INPUT),Pin(num=4,name="V-",func=P.PWRIN),Pin(num=5,name="+INB",func=P.INPUT),
Pin(num=6,name="-INB",func=P.INPUT),Pin(num=7,name="OUTB",func=P.OUTPUT),Pin(num=8,name="V+",func=P.PWRIN)])
THAT1646 = mk("THAT1646", [Pin(num=1,name="OUT-",func=P.OUTPUT),Pin(num=2,name="SNS-",func=P.INPUT),
Pin(num=3,name="GND",func=P.PWRIN),Pin(num=4,name="IN",func=P.INPUT),Pin(num=5,name="V-",func=P.PWRIN),
Pin(num=6,name="V+",func=P.PWRIN),Pin(num=7,name="SNS+",func=P.INPUT),Pin(num=8,name="OUT+",func=P.OUTPUT)])
PCM5102A = mk("PCM5102A", [Pin(num=1,name="CPVDD",func=P.PWRIN),Pin(num=2,name="CAPP",func=P.PASSIVE),
Pin(num=3,name="CPGND",func=P.PWRIN),Pin(num=4,name="CAPM",func=P.PASSIVE),Pin(num=5,name="VNEG",func=P.PASSIVE),
Pin(num=6,name="OUTL",func=P.OUTPUT),Pin(num=7,name="OUTR",func=P.OUTPUT),Pin(num=8,name="AVDD",func=P.PWRIN),
Pin(num=9,name="AGND",func=P.PWRIN),Pin(num=10,name="DEMP",func=P.INPUT),Pin(num=11,name="FLT",func=P.INPUT),
Pin(num=12,name="SCK",func=P.INPUT),Pin(num=13,name="BCK",func=P.INPUT),Pin(num=14,name="DIN",func=P.INPUT),
Pin(num=15,name="LRCK",func=P.INPUT),Pin(num=16,name="FMT",func=P.INPUT),Pin(num=17,name="XSMT",func=P.INPUT),
Pin(num=18,name="LDOO",func=P.PWROUT),Pin(num=19,name="DGND",func=P.PWRIN),Pin(num=20,name="DVDD",func=P.PWRIN)],
fp="Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm")
TQ2SA = mk("TQ2SA-5V", [Pin(num=1,name="COIL_A",func=P.PASSIVE),Pin(num=10,name="COIL_B",func=P.PASSIVE),
Pin(num=3,name="P1_COM",func=P.PASSIVE),Pin(num=4,name="P1_NC",func=P.PASSIVE),Pin(num=2,name="P1_NO",func=P.PASSIVE),
Pin(num=8,name="P2_COM",func=P.PASSIVE),Pin(num=7,name="P2_NC",func=P.PASSIVE),Pin(num=9,name="P2_NO",func=P.PASSIVE),
Pin(num=5,name="NC5",func=P.NOCONNECT),Pin(num=6,name="NC6",func=P.NOCONNECT)],
ref_prefix="K", fp="Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA")
ULN2003 = mk("ULN2003A", [Pin(num=1,name="1B",func=P.INPUT),Pin(num=2,name="2B",func=P.INPUT),
Pin(num=3,name="3B",func=P.INPUT),Pin(num=4,name="4B",func=P.INPUT),Pin(num=5,name="5B",func=P.INPUT),
Pin(num=6,name="6B",func=P.INPUT),Pin(num=7,name="7B",func=P.INPUT),Pin(num=8,name="GND",func=P.PWRIN),
Pin(num=9,name="COM",func=P.PWRIN),Pin(num=10,name="7C",func=P.OPENCOLL),Pin(num=11,name="6C",func=P.OPENCOLL),
Pin(num=12,name="5C",func=P.OPENCOLL),Pin(num=13,name="4C",func=P.OPENCOLL),Pin(num=14,name="3C",func=P.OPENCOLL),
Pin(num=15,name="2C",func=P.OPENCOLL),Pin(num=16,name="1C",func=P.OPENCOLL)],
fp="Package_SO:SOIC-16_3.9x9.9mm_P1.27mm")
# instances (deduplicated)
u1 = THAT1240(ref="U1") # line receiver
u2 = OPA1641(ref="U2") # Hi-Z DI buffer
u3 = PCM5102A(ref="U3") # DAC
u4 = OPA1612(ref="U4") # A = recon filter, B = summer (shared dual)
u5 = THAT1646(ref="U5") # balanced driver
u6 = ULN2003(ref="U6") # shared relay driver
k1, k2, k3 = TQ2SA(ref="K1"), TQ2SA(ref="K2"), TQ2SA(ref="K3")
def dcp(rail): # 100nF decoupling helper
c = C(value="100nF"); rail += c[1]; c[2] += gnd
# ---------------- Stage 1: balanced line receiver + protection ----------------
def protect_bal(src, tag):
cblk = C(value="2.2uF", footprint="Capacitor_SMD:C_1206_3216Metric")
rs, rb = R(value="1k"), R(value="1Meg")
dp, dn = D(value="1N4148WS"), D(value="1N4148WS")
node = Net(tag)
src += cblk[1]; cblk[2] += rs[1]; rs[2] += node
rb[1] += node; rb[2] += gnd
dp[1] += p15; dp[2] += node # Device:D pin1=K,pin2=A : high clamp
dn[1] += node; dn[2] += n15 # low clamp
return node
u1["IN+"] += protect_bal(rx_hot_in, "RXIN_P") # hot comes from the select relay (NC)
u1["IN-"] += protect_bal(ain_cold, "RXIN_N") # cold is the ring (direct)
u1["REF"] += gnd; u1["SENSE"] += rx_out; u1["OUT"] += rx_out
u1["V+"] += p15; u1["V-"] += n15; dcp(p15); dcp(n15)
# ---------------- Stage 1b: Hi-Z DI buffer (OPA1641) ----------------
cblk = C(value="100nF", footprint="Capacitor_SMD:C_1206_3216Metric")
rbias = R(value="1Meg"); dp, dn = D(value="1N4148WS"), D(value="1N4148WS")
di_node = Net("DI_NODE")
di_in += cblk[1]; cblk[2] += di_node
rbias[1] += di_node; rbias[2] += gnd
dp[1] += p15; dp[2] += di_node
dn[1] += di_node; dn[2] += n15
u2["+IN"] += di_node
rf2, rg2 = R(value="3k"), R(value="1k") # +12 dB
u2["OUT"] += di_out
rf2[1] += di_out; rf2[2] += u2["-IN"]
rg2[1] += u2["-IN"]; rg2[2] += gnd
u2["V+"] += p15; u2["V-"] += n15; dcp(p15); dcp(n15)
# ---------------- Stage 1b: select relay K1 ----------------
k1["P1_COM"] += ain_hot; k1["P1_NC"] += rx_hot_in; k1["P1_NO"] += di_in # route tip
k1["P2_COM"] += stage1_out; k1["P2_NC"] += rx_out; k1["P2_NO"] += di_out # select output
k1["COIL_A"] += p5; k1["COIL_B"] += k1_drv
# ---------------- Stage 2: PCM5102A DAC ----------------
for pin in ("AVDD","CPVDD","DVDD"): u3[pin] += p3v3
for pin in ("AGND","DGND","CPGND"): u3[pin] += gnd
for pin in ("AVDD","CPVDD","DVDD"):
c = C(value="100nF"); u3[pin] += c[1]; c[2] += gnd
cb = C(value="10uF", footprint="Capacitor_SMD:C_1206_3216Metric"); u3["AVDD"] += cb[1]; cb[2] += gnd
cfly = C(value="2.2uF"); u3["CAPP"] += cfly[1]; u3["CAPM"] += cfly[2]
cvneg = C(value="2.2uF"); u3["VNEG"] += cvneg[1]; cvneg[2] += gnd
cldoo = C(value="1uF"); u3["LDOO"] += cldoo[1]; cldoo[2] += gnd
u3["DEMP"] += gnd; u3["FLT"] += gnd; u3["FMT"] += gnd
u3["XSMT"] += dac_xsmt; rpu = R(value="10k"); dac_xsmt += rpu[1]; rpu[2] += p3v3
u3["SCK"] += mclk; u3["BCK"] += i2s_bck; u3["DIN"] += i2s_din; u3["LRCK"] += i2s_lrck
rload = R(value="2.2k"); u3["OUTR"] += rload[1]; rload[2] += gnd
# ---------------- Stage 2: reconstruction filter = OPA1612 section A ----------------
r1, r2 = R(value="1.5k"), R(value="1.5k"); ca = C(value="2.2nF"); cbq = C(value="1nF")
rcA = Net("RC_A")
u3["OUTL"] += r1[1]; r1[2] += rcA
r2[1] += rcA; r2[2] += u4["+INA"]
ca[1] += rcA; ca[2] += u4["OUTA"]
cbq[1] += u4["+INA"]; cbq[2] += gnd
u4["-INA"] += u4["OUTA"]; u4["OUTA"] += click_out
u4["V+"] += p15; u4["V-"] += n15; dcp(p15); dcp(n15)
# ---------------- Stage 3: summer = OPA1612 section B (shared chip) ----------------
ri_in, ri_clk, rf3 = R(value="10k"), R(value="10k"), R(value="10k")
stage1_out += ri_in[1]; ri_in[2] += u4["-INB"]
click_out += ri_clk[1]; ri_clk[2] += u4["-INB"]
rf3[1] += u4["-INB"]; rf3[2] += u4["OUTB"]
u4["+INB"] += gnd; u4["OUTB"] += mix_out
# ---------------- Stage 4: level trim + THAT1646 + build-out ----------------
RV = Part("Device","R_Potentiometer", dest=TEMPLATE,
footprint="Potentiometer_THT:Potentiometer_Bourns_3296W_Vertical")
rv1 = RV(value="10k", ref="RV1")
mix_out += rv1[1]; rv1[3] += gnd; rv1[2] += u5["IN"]
u5["SNS-"] += u5["OUT-"]; u5["SNS+"] += u5["OUT+"]; u5["GND"] += gnd
u5["V+"] += p15; u5["V-"] += n15; dcp(p15); dcp(n15)
rbo_h, rbo_c = R(value="47"), R(value="47")
u5["OUT-"] += rbo_h[1]; rbo_h[2] += aout_hot # phase-corrected: HOT<-OUT-
u5["OUT+"] += rbo_c[1]; rbo_c[2] += aout_cold # COLD<-OUT+
# ---------------- Stage 4: mute relay K2 + ground-lift relay K3 ----------------
k2["P1_COM"] += aout_hot; k2["P1_NC"] += gnd # de-energized = muted (short to gnd)
k2["P2_COM"] += aout_cold; k2["P2_NC"] += gnd
k2["COIL_A"] += p5; k2["COIL_B"] += k2_drv
k3["P1_COM"] += gnd; k3["P1_NC"] += chassis # de-energized = bonded
rlift = R(value="100"); clift = C(value="10nF")
rlift[1] += gnd; rlift[2] += chassis; clift[1] += gnd; clift[2] += chassis
k3["COIL_A"] += p5; k3["COIL_B"] += k3_drv
# ---------------- shared relay driver U6 (ULN2003): 3 channels ----------------
u6["1B"] += sel_linst; u6["1C"] += k1_drv
u6["2B"] += mute_en; u6["2C"] += k2_drv
u6["3B"] += gndlift_en; u6["3C"] += k3_drv
u6["GND"] += gnd; u6["COM"] += p5 # flyback common to coil supply
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "audio_chain.net"))
generate_netlist(file_=out)
print("Integrated audio netlist ->", out)

View file

@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - Stage 1: balanced LINE input receiver + protection (SKiDL).
Code-defined schematic. Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/stage1_input.py
Outputs ERC results + a KiCad netlist at hardware/kicad/stage1_input.net,
which imports into Pcbnew for layout.
VERIFIED against datasheets:
* THAT1240 = 0 dB (unity-gain) line receiver; SO-8 pinout per THAT doc 600035
rev 05: 1=Ref 2=In- 3=In+ 4=Vee 5=Sense 6=Vout 7=Vcc 8=NC. Supply 6-36V
(our +/-15V is in range). Pin-compatible 2nd sources: INA134 / SSM2141.
* clamp-diode orientation per KiCad Device:D (pin1=K cathode, pin2=A anode).
Open at BOM time only: exact clamp-diode part (fast low-leakage small-signal;
1N4148WS is a placeholder).
"""
import os
from skidl import *
set_default_tool(KICAD9)
# ---------- passive templates ----------
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
D = Part("Device", "D", dest=TEMPLATE, footprint="Diode_SMD:D_SOD-323")
# ---------- power rails (global nets; marked POWER-driven for ERC) ----------
# No power-flag *symbols* -- they carry no footprint and SKiDL errors on that.
# The rails arrive from the power block; here we just declare + drive them.
p15, n15, gnd = Net("+15V"), Net("-15V"), Net("GND")
for n in (p15, n15, gnd):
n.drive = POWER
# ---------- THAT1240 balanced line receiver (0 dB) : pinout VERIFIED ----------
RX = Part(name="THAT1240", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
pins=[
Pin(num=1, name="REF", func=Pin.types.INPUT),
Pin(num=2, name="IN-", func=Pin.types.INPUT),
Pin(num=3, name="IN+", func=Pin.types.INPUT),
Pin(num=4, name="V-", func=Pin.types.PWRIN),
Pin(num=5, name="SENSE", func=Pin.types.PASSIVE),
Pin(num=6, name="OUT", func=Pin.types.OUTPUT),
Pin(num=7, name="V+", func=Pin.types.PWRIN),
Pin(num=8, name="NC", func=Pin.types.NOCONNECT),
])
rx = RX(ref="U1")
# ---------- nets ----------
ain_hot, ain_cold, rxout = Net("AIN_HOT"), Net("AIN_COLD"), Net("RX_OUT")
def protected_input(src, node_name):
"""series DC-block cap (blocks +48V phantom) -> series R -> node;
bias R to gnd; clamp diodes to the +/-15V rails (orientation TODO-VERIFY)."""
cblk = C(value="2.2uF", footprint="Capacitor_SMD:C_1206_3216Metric") # film
rs, rb = R(value="1k"), R(value="1Meg")
dp, dn = D(value="1N4148WS"), D(value="1N4148WS")
node = Net(node_name)
src += cblk[1]; cblk[2] += rs[1]; rs[2] += node
rb[1] += node; rb[2] += gnd
# clamp diodes: KiCad Device:D is pin1=K (cathode), pin2=A (anode).
dp[1] += p15; dp[2] += node # high clamp: conducts when node > +15
dn[1] += node; dn[2] += n15 # low clamp: conducts when node < -15
return node
rx["IN+"] += protected_input(ain_hot, "RXIN_P")
rx["IN-"] += protected_input(ain_cold, "RXIN_N")
rx["REF"] += gnd
rx["SENSE"] += rxout # sense tied to output (local feedback)
rx["OUT"] += rxout
rx["V+"] += p15
rx["V-"] += n15
# supply decoupling at the receiver
for rail in (p15, n15):
c = C(value="100nF")
rail += c[1]; c[2] += gnd
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "stage1_input.net"))
generate_netlist(file_=out)
print("Stage 1 netlist ->", out)

View file

@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - Stage 1b: Hi-Z instrument DI buffer + line/instrument select (SKiDL).
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/stage1b_select.py
Outputs ERC + a KiCad netlist at hardware/kicad/stage1b_select.net.
WHAT THIS STAGE DOES
The same input jack feeds either the balanced LINE receiver (Stage 1) or a Hi-Z
INSTRUMENT buffer (here). One DPDT relay (K1) does two jobs at once:
pole 1 routes the jack TIP to the line receiver (default) OR the DI buffer
pole 2 selects which OUTPUT (RX_OUT or DI_OUT) feeds the summing stage
De-energized (relay OFF) = LINE = the common case (saves coil power; fail to line).
The DI buffer = OPA1641 non-inverting, gain +12 dB (1+Rf/Rg), 1 Mohm input.
PINOUTS VERIFIED FROM DATASHEETS
* OPA1641 (TI SBOS484D): 1=NC 2=-IN 3=+IN 4=V- 5=NC 6=OUT 7=V+ 8=NC. Supply +/-18V max.
* TQ2SA (Panasonic TQ-SMD): 2 Form C. pole1 COM=3 (throws 2,4); pole2 COM=8 (throws 7,9)
-- confirmed from the contact-resistance terminal pairs (2-3,3-4,7-8,8-9).
RESOLVED from the Panasonic TQ-SMD connection diagram (single-side-stable, top view)
+ the contact-resistance terminal pairs (2-3,3-4,7-8,8-9):
* coil = pins 1 & 10 (polarity irrelevant -- non-latching single coil, no diode).
* pole1 COM=3, NC=4, NO=2 ; pole2 COM=8, NC=7, NO=9 ; pins 5,6 unused.
NC/NO sense is also firmware-correctable: the relay is GPIO-driven, so if a physical
unit reads opposite, invert SEL_LINST in firmware (no board change).
Relay coil is driven by the SHARED ULN2003 (U14) -- represented here as net K1_DRV
(sinks the coil) + SEL_LINST (the RP2350 GPIO). The ULN2003 is instantiated once in
the power/control block, not per-relay.
"""
import os
from skidl import *
set_default_tool(KICAD9)
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
D = Part("Device", "D", dest=TEMPLATE, footprint="Diode_SMD:D_SOD-323")
# ---- rails ----
p15, n15, gnd, p5 = Net("+15V"), Net("-15V"), Net("GND"), Net("+5V")
for n in (p15, n15, gnd, p5):
n.drive = POWER
# ---- OPA1641 JFET Hi-Z buffer (pinout verified) ----
OPA = Part(name="OPA1641", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
pins=[
Pin(num=1, name="NC1", func=Pin.types.NOCONNECT),
Pin(num=2, name="-IN", func=Pin.types.INPUT),
Pin(num=3, name="+IN", func=Pin.types.INPUT),
Pin(num=4, name="V-", func=Pin.types.PWRIN),
Pin(num=5, name="NC5", func=Pin.types.NOCONNECT),
Pin(num=6, name="OUT", func=Pin.types.OUTPUT),
Pin(num=7, name="V+", func=Pin.types.PWRIN),
Pin(num=8, name="NC8", func=Pin.types.NOCONNECT),
])
u = OPA(ref="U2")
# ---- TQ2SA DPDT select relay (contacts verified; NC/NO + coil pins flagged) ----
RLY = Part(name="TQ2SA-5V", tool=SKIDL, dest=TEMPLATE, ref_prefix="K",
footprint="Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA", # footprint: confirm at layout
pins=[
Pin(num=1, name="COIL_A", func=Pin.types.PASSIVE),
Pin(num=10, name="COIL_B", func=Pin.types.PASSIVE),
Pin(num=3, name="P1_COM", func=Pin.types.PASSIVE),
Pin(num=4, name="P1_NC", func=Pin.types.PASSIVE), # de-energized = LINE
Pin(num=2, name="P1_NO", func=Pin.types.PASSIVE), # energized = INSTRUMENT
Pin(num=8, name="P2_COM", func=Pin.types.PASSIVE),
Pin(num=7, name="P2_NC", func=Pin.types.PASSIVE),
Pin(num=9, name="P2_NO", func=Pin.types.PASSIVE),
Pin(num=5, name="NC5", func=Pin.types.NOCONNECT),
Pin(num=6, name="NC6", func=Pin.types.NOCONNECT),
])
k1 = RLY(ref="K1")
# ---- nets (shared with Stage 1 / Stage 3 by name) ----
ain_hot = Net("AIN_HOT") # jack TIP
rx_hot_in = Net("RX_HOT_IN") # -> Stage 1 line-receiver hot protection (was AIN_HOT there)
di_in = Net("DI_IN") # -> this DI buffer input
rx_out = Net("RX_OUT") # <- Stage 1 line receiver output
di_out = Net("DI_OUT") # this DI buffer output
stage1_out = Net("STAGE1_OUT") # selected -> Stage 3 summing
sel = Net("SEL_LINST") # RP2350 GPIO: low=LINE(default), high=INSTRUMENT
k1_drv = Net("K1_DRV") # shared ULN2003 output sinks the coil
# pole 1: route jack tip
k1["P1_COM"] += ain_hot
k1["P1_NC"] += rx_hot_in # default -> line receiver
k1["P1_NO"] += di_in # energized -> DI buffer
# pole 2: select output
k1["P2_COM"] += stage1_out
k1["P2_NC"] += rx_out # default -> line receiver output
k1["P2_NO"] += di_out # energized -> DI buffer output
# coil: +5V -- coil -- K1_DRV (ULN2003 sinks to gnd when SEL_LINST high)
k1["COIL_A"] += p5
k1["COIL_B"] += k1_drv
# ---- DI buffer input: DC-block, 1M bias, rail clamps ----
cblk = C(value="100nF", footprint="Capacitor_SMD:C_1206_3216Metric") # film
rbias = R(value="1Meg")
dp, dn = D(value="1N4148WS"), D(value="1N4148WS")
node = Net("DI_NODE")
di_in += cblk[1]; cblk[2] += node
rbias[1] += node; rbias[2] += gnd
dp[1] += p15; dp[2] += node # Device:D pin1=K, pin2=A -> high clamp (>+15)
dn[1] += node; dn[2] += n15 # low clamp (< -15)
u["+IN"] += node
# ---- non-inverting gain: Av = 1 + Rf/Rg = 1 + 3k/1k = 4 (+12 dB) ----
rf, rg = R(value="3k"), R(value="1k")
u["OUT"] += di_out
rf[1] += di_out; rf[2] += u["-IN"]
rg[1] += u["-IN"]; rg[2] += gnd
# ---- supplies + decoupling ----
u["V+"] += p15; u["V-"] += n15
for rail in (p15, n15):
c = C(value="100nF"); rail += c[1]; c[2] += gnd
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "stage1b_select.net"))
generate_netlist(file_=out)
print("Stage 1b netlist ->", out)

View file

@ -0,0 +1,137 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - Stage 2: PCM5102A DAC + reconstruction filter (SKiDL).
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/stage2_dac.py
Outputs ERC + a KiCad netlist at hardware/kicad/stage2_dac.net.
WHAT THIS STAGE DOES
The RP2350 streams I2S audio (the click, and later sampled sounds) to a PCM5102A
DAC clocked by the dedicated low-jitter oscillator (SCK=MCLK). The PCM5102A puts
out 2.1 Vrms GROUND-CENTERED analog (no DC-block cap needed). A 2nd-order
Sallen-Key low-pass (~75 kHz, one OPA1612 section) removes the delta-sigma HF
residue before the click reaches the summing/output stages -> CLICK_OUT.
PINOUTS VERIFIED FROM DATASHEETS
* PCM5102A (TI SLAS859C, PW/TSSOP-20): pin map below matches the datasheet table.
Output 2.1Vrms GND-centered; 3.3V supplies; charge pump (CAPP/CAPM flying cap +
VNEG) makes the negative rail for the ground-centered swing.
* OPA1612 (dual): standard JEDEC dual-opamp SOIC-8 pinout
(1=OUTA 2=-INA 3=+INA 4=V- 5=+INB 6=-INB 7=OUTB 8=V+), confirmed against the
OPA164x dual on its datasheet (identical layout). Confirm OPA1612 sheet at layout.
"""
import os
from skidl import *
set_default_tool(KICAD9)
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
# ---- rails / signals ----
p3v3, gnd, p15, n15 = Net("+3V3"), Net("GND"), Net("+15V"), Net("-15V")
for n in (p3v3, gnd, p15, n15):
n.drive = POWER
mclk = Net("MCLK") # low-jitter audio clock -> SCK
i2s_bck, i2s_din, i2s_lrck = Net("I2S_BCK"), Net("I2S_DIN"), Net("I2S_LRCK") # from RP2350
dac_xsmt = Net("DAC_XSMT") # GPIO soft-mute (high=unmute), pulled up
click_out = Net("CLICK_OUT")# filtered click -> Stage 3 summing
# ---- PCM5102A (pinout verified, TI SLAS859C) ----
DAC = Part(name="PCM5102A", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm",
pins=[
Pin(num=1, name="CPVDD", func=Pin.types.PWRIN),
Pin(num=2, name="CAPP", func=Pin.types.PASSIVE),
Pin(num=3, name="CPGND", func=Pin.types.PWRIN),
Pin(num=4, name="CAPM", func=Pin.types.PASSIVE),
Pin(num=5, name="VNEG", func=Pin.types.PASSIVE),
Pin(num=6, name="OUTL", func=Pin.types.OUTPUT),
Pin(num=7, name="OUTR", func=Pin.types.OUTPUT),
Pin(num=8, name="AVDD", func=Pin.types.PWRIN),
Pin(num=9, name="AGND", func=Pin.types.PWRIN),
Pin(num=10, name="DEMP", func=Pin.types.INPUT),
Pin(num=11, name="FLT", func=Pin.types.INPUT),
Pin(num=12, name="SCK", func=Pin.types.INPUT),
Pin(num=13, name="BCK", func=Pin.types.INPUT),
Pin(num=14, name="DIN", func=Pin.types.INPUT),
Pin(num=15, name="LRCK", func=Pin.types.INPUT),
Pin(num=16, name="FMT", func=Pin.types.INPUT),
Pin(num=17, name="XSMT", func=Pin.types.INPUT),
Pin(num=18, name="LDOO", func=Pin.types.PWROUT),
Pin(num=19, name="DGND", func=Pin.types.PWRIN),
Pin(num=20, name="DVDD", func=Pin.types.PWRIN),
])
dac = DAC(ref="U3")
# ---- OPA1612 dual op-amp (standard dual pinout) ----
OPA2 = Part(name="OPA1612", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
pins=[
Pin(num=1, name="OUTA", func=Pin.types.OUTPUT),
Pin(num=2, name="-INA", func=Pin.types.INPUT),
Pin(num=3, name="+INA", func=Pin.types.INPUT),
Pin(num=4, name="V-", func=Pin.types.PWRIN),
Pin(num=5, name="+INB", func=Pin.types.INPUT),
Pin(num=6, name="-INB", func=Pin.types.INPUT),
Pin(num=7, name="OUTB", func=Pin.types.OUTPUT),
Pin(num=8, name="V+", func=Pin.types.PWRIN),
])
flt = OPA2(ref="U4")
# ---- PCM5102A supplies + grounds (all grounds tied; <0.2V per datasheet) ----
dac["AVDD"] += p3v3
dac["CPVDD"] += p3v3
dac["DVDD"] += p3v3
dac["AGND"] += gnd
dac["DGND"] += gnd
dac["CPGND"] += gnd
for pin in ("AVDD", "CPVDD", "DVDD"):
c = C(value="100nF"); dac[pin] += c[1]; c[2] += gnd
cbulk = C(value="10uF", footprint="Capacitor_SMD:C_1206_3216Metric")
dac["AVDD"] += cbulk[1]; cbulk[2] += gnd
# charge pump: flying cap across CAPP/CAPM, VNEG + LDOO decoupling
cfly = C(value="2.2uF", footprint="Capacitor_SMD:C_0805_2012Metric")
dac["CAPP"] += cfly[1]; dac["CAPM"] += cfly[2]
cvneg = C(value="2.2uF"); dac["VNEG"] += cvneg[1]; cvneg[2] += gnd
cldoo = C(value="1uF"); dac["LDOO"] += cldoo[1]; cldoo[2] += gnd
# control-pin tie-offs: no de-emphasis, normal latency, I2S format
dac["DEMP"] += gnd
dac["FLT"] += gnd
dac["FMT"] += gnd
# soft-mute: GPIO with 10k pull-up to keep un-muted by default
dac["XSMT"] += dac_xsmt
rpull = R(value="10k"); dac_xsmt += rpull[1]; rpull[2] += p3v3
# clocks + data
dac["SCK"] += mclk
dac["BCK"] += i2s_bck
dac["DIN"] += i2s_din
dac["LRCK"] += i2s_lrck
# OUTR unused (mono click on L): give it the datasheet-recommended load
rload = R(value="2.2k"); dac["OUTR"] += rload[1]; rload[2] += gnd
# ---- Sallen-Key reconstruction LPF on OUTL (R=1.5k, Ca=2.2n, Cb=1n ~75kHz) ----
r1, r2 = R(value="1.5k"), R(value="1.5k")
ca = C(value="2.2nF"); cb = C(value="1nF")
nodeA = Net("RC_A")
dac["OUTL"] += r1[1]; r1[2] += nodeA
r2[1] += nodeA; r2[2] += flt["+INA"]
ca[1] += nodeA; ca[2] += flt["OUTA"] # feedback cap
cb[1] += flt["+INA"]; cb[2] += gnd
flt["-INA"] += flt["OUTA"] # unity-gain follower
flt["OUTA"] += click_out
# ---- OPA1612 supplies + decoupling; park unused section B ----
flt["V+"] += p15; flt["V-"] += n15
for rail in (p15, n15):
c = C(value="100nF"); rail += c[1]; c[2] += gnd
flt["+INB"] += gnd; flt["OUTB"] += flt["-INB"] # B = grounded follower (parked)
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "stage2_dac.net"))
generate_netlist(file_=out)
print("Stage 2 netlist ->", out)

View file

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - Stage 3: summing node (selected input + click) (SKiDL).
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/stage3_sum.py
Outputs ERC + a KiCad netlist at hardware/kicad/stage3_sum.net.
WHAT THIS STAGE DOES
Inverting summing amp (OPA1612 section) mixes STAGE1_OUT (the line/instrument input,
unity) and CLICK_OUT (the filtered DAC click). Each source enters through its own
10k resistor into the op-amp's virtual-ground node, so the two never interact. The
"digital mix" lives upstream: click level is set by the DAC; the input passes at
unity. Output MIX_OUT -> Stage 4 balanced driver.
Vout = -(STAGE1_OUT + CLICK_OUT) (Rf = Ri = 10k)
POLARITY: an inverting summer flips phase. That is corrected for free at the Stage 4
balanced driver by assigning hot/cold accordingly (absolute polarity preserved).
OPA1612 dual: standard JEDEC pinout (1=OUTA 2=-INA 3=+INA 4=V- 5=+INB 6=-INB 7=OUTB
8=V+). At INTEGRATION this summer can use the PARKED 2nd half of the Stage 2 filter's
OPA1612 (U4) instead of a separate package -- noted for the merge step.
"""
import os
from skidl import *
set_default_tool(KICAD9)
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
p15, n15, gnd = Net("+15V"), Net("-15V"), Net("GND")
for n in (p15, n15, gnd):
n.drive = POWER
stage1_out = Net("STAGE1_OUT") # from Stage 1b relay (selected input)
click_out = Net("CLICK_OUT") # from Stage 2 reconstruction filter
mix_out = Net("MIX_OUT") # -> Stage 4 balanced driver
OPA2 = Part(name="OPA1612", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
pins=[
Pin(num=1, name="OUTA", func=Pin.types.OUTPUT),
Pin(num=2, name="-INA", func=Pin.types.INPUT),
Pin(num=3, name="+INA", func=Pin.types.INPUT),
Pin(num=4, name="V-", func=Pin.types.PWRIN),
Pin(num=5, name="+INB", func=Pin.types.INPUT),
Pin(num=6, name="-INB", func=Pin.types.INPUT),
Pin(num=7, name="OUTB", func=Pin.types.OUTPUT),
Pin(num=8, name="V+", func=Pin.types.PWRIN),
])
u = OPA2(ref="U5")
# inverting summer: each source -> 10k -> virtual-ground (-INA); Rf 10k; +INA -> gnd
ri_in, ri_clk, rf = R(value="10k"), R(value="10k"), R(value="10k")
stage1_out += ri_in[1]; ri_in[2] += u["-INA"]
click_out += ri_clk[1]; ri_clk[2] += u["-INA"]
rf[1] += u["-INA"]; rf[2] += u["OUTA"]
u["+INA"] += gnd
u["OUTA"] += mix_out
# supplies + decoupling; park unused section B
u["V+"] += p15; u["V-"] += n15
for rail in (p15, n15):
c = C(value="100nF"); rail += c[1]; c[2] += gnd
u["+INB"] += gnd; u["OUTB"] += u["-INB"] # parked follower
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "stage3_sum.net"))
generate_netlist(file_=out)
print("Stage 3 netlist ->", out)

View file

@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""PM_K-1 audio chain - Stage 4: balanced output driver (THAT1646) + mute + ground-lift.
Run INSIDE the EDA container:
cd hardware/eda && ./run.sh python3 ../eda/circuits/stage4_driver.py
Outputs ERC + a KiCad netlist at hardware/kicad/stage4_driver.net. This closes the
audio chain: MIX_OUT -> level trim -> THAT1646 -> build-out -> mute relay -> balanced
output on the analog interconnect; plus the ground-lift relay.
PINOUT VERIFIED (THAT doc 600078 rev 07, SO-8):
THAT1646: 1=Out- 2=Sns- 3=Gnd 4=In 5=Vee(V-) 6=Vcc(V+) 7=Sns+ 8=Out+.
Fixed +6 dB differential gain; sense pins tie to the output pins (local sense) with
the 47 ohm build-out OUTSIDE the loop for cable stability. Supply 4-18V (we use +/-15).
TQ2SA relay pinout per stage1b (coil 1/10; pole1 COM=3 NC=4 NO=2; pole2 COM=8 NC=7 NO=9).
KEY CHOICES
* Level cal: the THAT1646 gain is FIXED (+6 dB), so the calibration trim is a 25-turn
pot ahead of it (DAC full-scale -> +4 dBu, accounting for summer + the +6 dB).
* Phase: Stage 3's summer inverted the signal; corrected here by taking AOUT_HOT from
Out- and AOUT_COLD from Out+ (absolute polarity preserved).
* Mute (K2): fail-safe -- de-energized shorts both legs to GND (after the 47 ohm
build-out, so the driver is current-limited). Energized = un-muted. Driven by the
hardware rail-supervisor via K2_DRV (in the power block); MCU can also assert.
* Ground-lift (K3): de-energized = bonded (GND<->CHASSIS); energize to LIFT. Soft-lift
100 ohm || 10 nF across the contact keeps an RF/safety path. A face panel switch sits
in series downstream (CHASSIS pin on the interconnect) -> both-must-close to bond.
"""
import os
from skidl import *
set_default_tool(KICAD9)
R = Part("Device", "R", dest=TEMPLATE, footprint="Resistor_SMD:R_0805_2012Metric")
C = Part("Device", "C", dest=TEMPLATE, footprint="Capacitor_SMD:C_0805_2012Metric")
p15, n15, gnd, p5 = Net("+15V"), Net("-15V"), Net("GND"), Net("+5V")
for n in (p15, n15, gnd, p5):
n.drive = POWER
mix_out = Net("MIX_OUT") # from Stage 3 summer (inverted)
aout_hot, aout_cold = Net("AOUT_HOT"), Net("AOUT_COLD") # -> analog interconnect
chassis = Net("CHASSIS") # shield -> interconnect (face panel switch + chassis)
k2_drv, k3_drv = Net("K2_DRV"), Net("K3_DRV") # relay coil drives
# ---- THAT1646 balanced line driver (pinout verified) ----
DRV = Part(name="THAT1646", tool=SKIDL, dest=TEMPLATE, ref_prefix="U",
footprint="Package_SO:SOIC-8_3.9x4.9mm_P1.27mm",
pins=[
Pin(num=1, name="OUT-", func=Pin.types.OUTPUT),
Pin(num=2, name="SNS-", func=Pin.types.INPUT),
Pin(num=3, name="GND", func=Pin.types.PWRIN),
Pin(num=4, name="IN", func=Pin.types.INPUT),
Pin(num=5, name="V-", func=Pin.types.PWRIN),
Pin(num=6, name="V+", func=Pin.types.PWRIN),
Pin(num=7, name="SNS+", func=Pin.types.INPUT),
Pin(num=8, name="OUT+", func=Pin.types.OUTPUT),
])
drv = DRV(ref="U6")
# ---- TQ2SA relay (verified pinout, reused def) ----
def tq2sa(ref):
p = Part(name="TQ2SA-5V", tool=SKIDL, dest=TEMPLATE, ref_prefix="K",
footprint="Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA",
pins=[
Pin(num=1, name="COIL_A", func=Pin.types.PASSIVE),
Pin(num=10, name="COIL_B", func=Pin.types.PASSIVE),
Pin(num=3, name="P1_COM", func=Pin.types.PASSIVE),
Pin(num=4, name="P1_NC", func=Pin.types.PASSIVE),
Pin(num=2, name="P1_NO", func=Pin.types.PASSIVE),
Pin(num=8, name="P2_COM", func=Pin.types.PASSIVE),
Pin(num=7, name="P2_NC", func=Pin.types.PASSIVE),
Pin(num=9, name="P2_NO", func=Pin.types.PASSIVE),
Pin(num=5, name="NC5", func=Pin.types.NOCONNECT),
Pin(num=6, name="NC6", func=Pin.types.NOCONNECT),
])
return p(ref=ref)
k2 = tq2sa("K2") # mute
k3 = tq2sa("K3") # ground-lift
# ---- level-cal trim (25-turn pot): MIX_OUT (top) / GND (bottom) / wiper -> THAT1646 IN ----
RV = Part("Device", "R_Potentiometer", dest=TEMPLATE,
footprint="Potentiometer_THT:Potentiometer_Bourns_3296W_Vertical")
rv1 = RV(value="10k", ref="RV1")
mix_out += rv1[1]; rv1[3] += gnd; rv1[2] += drv["IN"]
# ---- THAT1646: sense ties (local) + supplies ----
drv["SNS-"] += drv["OUT-"]
drv["SNS+"] += drv["OUT+"]
drv["GND"] += gnd
drv["V+"] += p15; drv["V-"] += n15
for rail in (p15, n15):
c = C(value="100nF"); rail += c[1]; c[2] += gnd
# ---- 47 ohm build-out (phase-corrected: HOT<-OUT-, COLD<-OUT+) ----
rbo_h, rbo_c = R(value="47"), R(value="47")
drv["OUT-"] += rbo_h[1]; rbo_h[2] += aout_hot
drv["OUT+"] += rbo_c[1]; rbo_c[2] += aout_cold
# ---- mute relay K2: de-energized shorts both legs to GND (fail-safe) ----
k2["P1_COM"] += aout_hot; k2["P1_NC"] += gnd # de-energized: hot -> GND (muted)
k2["P2_COM"] += aout_cold; k2["P2_NC"] += gnd # de-energized: cold -> GND
k2["COIL_A"] += p5; k2["COIL_B"] += k2_drv # energize (rails OK) = un-mute
# ---- ground-lift K3: de-energized bonds GND<->CHASSIS; energize to lift ----
k3["P1_COM"] += gnd; k3["P1_NC"] += chassis # de-energized: bonded
rlift = R(value="100"); clift = C(value="10nF")
gnd += rlift[1]; rlift[2] += chassis # soft-lift: 100 ohm || 10 nF
clift[1] += gnd; clift[2] += chassis
k3["COIL_A"] += p5; k3["COIL_B"] += k3_drv
ERC()
out = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "kicad", "stage4_driver.net"))
generate_netlist(file_=out)
print("Stage 4 netlist ->", out)

24
hardware/eda/run.sh Executable file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Build (first time) and run the PM_K-1 EDA container with the repo mounted.
#
# ./run.sh # interactive shell in hardware/kicad/
# ./run.sh kicad-cli sch erc pm_k1_core.kicad_sch
# ./run.sh ngspice -b sim/some.cir
#
# Override the runtime with RUNTIME=docker ./run.sh ...
set -euo pipefail
IMG="pmk1-eda:9.0"
EDA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # hardware/eda
REPO_DIR="$(cd "$EDA_DIR/../.." && pwd)" # repo root
RUNTIME="${RUNTIME:-podman}"
if ! "$RUNTIME" image inspect "$IMG" >/dev/null 2>&1; then
echo ">> building $IMG (first run, a few minutes)…" >&2
"$RUNTIME" build -t "$IMG" "$EDA_DIR"
fi
# Mount the whole repo so KiCad/ngspice see hardware/ ; land in hardware/kicad.
flags=(--rm -v "$REPO_DIR":/work:Z -w /work/hardware/kicad)
[[ -t 0 && $# -eq 0 ]] && flags+=(-it)
exec "$RUNTIME" run "${flags[@]}" "$IMG" "${@:-bash}"

1
hardware/eda/sim/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.csv

View file

@ -0,0 +1,47 @@
* PM_K-1 : input loading -- line receiver (25k) vs Hi-Z instrument buffer (1M)
*
* This is the circuit-level proof of a decision we made by hand: why a guitar/bass
* needs a HIGH-impedance input. A passive pickup is inductive, so the load impedance
* it sees shapes its tone. Plug it into a low impedance and you damp its resonance
* and lose treble + level; a high-Z buffer preserves it.
*
* Pickup model: open-circuit source + winding resistance + inductance + self-capacitance.
* Plus a typical ~300 pF guitar cable. We drive two identical pickup+cable networks
* from one ideal source and load them differently, then compare.
*
* Run: ngspice -b sim/input_loading.cir (from inside the EDA container)
.title PM_K-1 input loading: line 25k vs instrument 1M
Vpu src 0 AC 1
* --- Branch A: LINE input, ~25k receiver impedance ---
RdcA src a1 6k
LpuA a1 a2 3
CpuA a2 0 150p
CcabA a2 0 300p
RinA a2 0 25k
* --- Branch B: INSTRUMENT input, 1M Hi-Z buffer ---
RdcB src b1 6k
LpuB b1 b2 3
CpuB b2 0 150p
CcabB b2 0 300p
RinB b2 0 1meg
.ac dec 100 10 100k
.control
run
meas ac a_1k find vdb(a2) at=1000
meas ac b_1k find vdb(b2) at=1000
meas ac a_peak max vdb(a2) from=1000 to=20000
meas ac b_peak max vdb(b2) from=1000 to=20000
echo
echo " Level @1kHz : line(25k)= $&a_1k dB inst(1M)= $&b_1k dB"
echo " Resonant peak : line(25k)= $&a_peak dB inst(1M)= $&b_peak dB"
echo " -> the 25k load drags the signal down and flattens the pickup's natural peak;"
echo " the 1M input preserves level and tone. Hence the switchable Hi-Z front end."
wrdata ../eda/sim/input_loading.csv vdb(a2) vdb(b2)
.endc
.end

View file

@ -0,0 +1,56 @@
* PM_K-1 Stage 1 : balanced line receiver -- why CMRR needs matched resistors
*
* A balanced receiver rejects "common-mode" noise -- hum/interference that appears
* equally on both signal wires -- by SUBTRACTING the two wires. How well it cancels
* (its Common-Mode Rejection Ratio, CMRR) depends entirely on how well its four
* resistors match. This is the circuit-level reason we chose the laser-trimmed
* THAT1240 instead of a discrete op-amp + four 1% resistors.
*
* Three identical unity-gain difference amplifiers, driven common-mode (same 1V on
* both inputs), with 0% / 0.1% / 1% mismatch on one resistor.
* CMRR(dB) = -20*log10(|Vout|) since differential gain = 1 and Vcm = 1V.
*
* Run: ngspice -b ../eda/sim/stage1_cmrr.cir
.title Stage1 line receiver CMRR vs resistor match
Vcm cm 0 AC 1 ; common-mode drive: both inputs tied to cm
* ---- A: perfectly matched ----
EA outa 0 vpa vma 1e6
R1a cm vma 10k
R2a outa vma 10k
R3a cm vpa 10k
R4a vpa 0 10k
* ---- B: 0.1% mismatch on one resistor ----
EB outb 0 vpb vmb 1e6
R1b cm vmb 10k
R2b outb vmb 10k
R3b cm vpb 10k
R4b vpb 0 10.01k
* ---- C: 1% mismatch on one resistor ----
EC outc 0 vpc vmc 1e6
R1c cm vmc 10k
R2c outc vmc 10k
R3c cm vpc 10k
R4c vpc 0 10.1k
.ac dec 10 100 10k
.control
run
meas ac acm_a find vm(outa) at=1000
meas ac acm_b find vm(outb) at=1000
meas ac acm_c find vm(outc) at=1000
let cmrr_a = -20*log10(acm_a)
let cmrr_b = -20*log10(acm_b)
let cmrr_c = -20*log10(acm_c)
echo
echo " CMRR perfect match : $&cmrr_a dB (limited only by the amplifier)"
echo " CMRR 0.1% mismatch : $&cmrr_b dB"
echo " CMRR 1% mismatch : $&cmrr_c dB"
echo " -> hand-matched 1% resistors give poor rejection; the THAT1240's"
echo " laser-trimmed internal resistors are how we get >90 dB for free."
.endc
.end

View file

@ -0,0 +1,39 @@
* PM_K-1 Stage 1 : input protection -- a +48V phantom-power step at the jack
*
* "Phantom power" is +48V DC that mixers put on their MIC inputs. If it ever reaches
* our input (miswire, wrong cable, our OUT plugged into a phantom'd input), a bare
* op-amp input would die. Our protection makes it a non-event:
* - a series DC-BLOCKING film cap passes audio (AC) but blocks the +48V (DC),
* - a series resistor + clamp diodes to the +/-15V rails survive the turn-on EDGE
* (the cap passes the fast step before it charges).
*
* We hit the input with a +48V step at t=1ms and watch the OP-AMP INPUT node.
*
* Run: ngspice -b ../eda/sim/stage1_phantom.cir
.title Stage1 phantom protection
Vin in 0 PWL(0 0 1m 0 1.001m 48 5 48) ; +48V appears at t=1ms, stays
C1 in a 1u ; DC-blocking film cap (audio passes, DC blocked)
Rs a n 1k ; series current limit
Rb n 0 1meg ; bias / input impedance (audio high-pass corner ~0.16 Hz)
.model Dclamp D(Is=1e-14 N=1 Rs=10)
Dp n vp Dclamp ; clamp to +15
Dn vn n Dclamp ; clamp to -15
Vp vp 0 15
Vn vn 0 -15
.tran 2m 5
.control
run
meas tran vn_peak max v(n) from=1m to=5
meas tran vn_final find v(n) at=4.9
echo
echo " op-amp input PEAK during the +48V step : $&vn_peak V (clamped near a +/-15 rail)"
echo " op-amp input STEADY-STATE : $&vn_final V (DC blocked by the cap)"
echo " -> +48V at the jack becomes a clamped blip that decays to ~0. Nothing is harmed;"
echo " a wrong patch only ever sounds wrong. (Audio passes -- the high-pass is ~0.16 Hz.)"
.endc
.end

View file

@ -0,0 +1,28 @@
* PM_K-1 Stage 1b : Hi-Z instrument DI buffer -- gain + flatness check
*
* An OPA1641 (JFET input, ~1e12 ohm) as a non-inverting amp. The INPUT impedance
* the instrument sees is set by the bias resistor (1 Mohm) -- which the earlier
* input_loading.cir proved is what preserves a pickup's tone. Here we just confirm
* the voltage gain: Av = 1 + Rf/Rg. Target +12 dB (x4) with Rf=3k, Rg=1k.
*
* Run: ngspice -b ../eda/sim/stage1b_di.cir
.title Stage1b DI buffer gain
Vin in 0 AC 1
Eop out 0 in vm 1e6 ; ideal op-amp: +IN = in, -IN = vm
Rf out vm 3k ; feedback
Rg vm 0 1k ; gain set
.ac dec 20 10 100k
.control
run
meas ac g_1k find vdb(out) at=1000
meas ac g_20k find vdb(out) at=20000
let av = pow(10, g_1k/20)
echo
echo " DI buffer gain @1kHz : $&g_1k dB ( x$&av ) target +12.04 dB (x4)"
echo " DI buffer gain @20kHz : $&g_20k dB (flat across audio band)"
echo " Input impedance is set by the 1Mohm bias R (JFET input ~1e12 ohm) -- see input_loading.cir"
.endc
.end

View file

@ -0,0 +1,35 @@
* PM_K-1 Stage 2 : DAC reconstruction filter (2nd-order Sallen-Key low-pass)
*
* The PCM5102A is a delta-sigma DAC: its analog output carries the audio plus
* shaped high-frequency quantization noise WAY above the audio band. A gentle
* low-pass cleans that residue before it reaches the pro output stage. We want it
* dead flat to 20 kHz and rolling off above ~80 kHz (Butterworth, Q~0.7).
*
* Unity-gain Sallen-Key, equal R. Target fc ~75 kHz with R=1.5k, C1=2.2n, C2=1n.
* Run: ngspice -b ../eda/sim/stage2_recon.cir
.title Stage2 DAC reconstruction filter
.param R=1.5k
.param Ca=2.2n
.param Cb=1n
Vin in 0 AC 1
R1 in a {R}
R2 a vp {R}
Ca a out {Ca} ; feedback cap
Cb vp 0 {Cb} ; cap to ground
Eop out 0 vp out 1e6 ; unity-gain follower op-amp (-in tied to out)
.ac dec 100 100 2meg
.control
run
meas ac g_1k find vdb(out) at=1k
meas ac g_20k find vdb(out) at=20k
meas ac f_3db when vdb(out)=-3
echo
echo " passband @1kHz : $&g_1k dB"
echo " @20kHz (audio edge) : $&g_20k dB (want ~0 dB = flat)"
echo " -3dB corner : $&f_3db Hz (well above audio; attenuates DAC HF residue)"
.endc
.end

View file

@ -0,0 +1,40 @@
* PM_K-1 Stage 3 : summing node (selected input + click)
*
* An inverting summing amp mixes STAGE1_OUT (line/instrument) and CLICK_OUT (DAC).
* Each source feeds the op-amp's inverting input through its own 10k resistor; the
* feedback holds that node at a "virtual ground" (~0V). Because BOTH sources see 0V
* there, neither can load or pull on the other -- they sum with no interaction.
* Vout = -(Rf/Ri1)*V1 - (Rf/Ri2)*V2 ; with Rf=Ri=10k -> Vout = -(V1+V2).
*
* We confirm: each input alone = 0 dB (gain -1), both together = +6 dB (they add),
* and each input's gain is unchanged by the other (isolation).
* Run: ngspice -b ../eda/sim/stage3_sum.cir
.title Stage3 inverting summer
Vinp inp 0 AC 1 ; selected input (STAGE1_OUT)
Vclk clk 0 AC 1 ; filtered click (CLICK_OUT)
Ri1 inp vm 10k
Ri2 clk vm 10k
Rf vm out 10k
Eop out 0 0 vm 1e6 ; +in = gnd, -in = vm -> inverting; feedback makes vm a virtual ground
.ac dec 10 100 20k
.control
* both sources active -> they sum
run
meas ac g_both find vdb(out) at=1k
echo " both = $&g_both dB (+6 dB over each-alone = the two sum)"
* input alone (click muted)
alter @Vclk[acmag]=0
run
meas ac g_in find vdb(out) at=1k
echo " input alone = $&g_in dB (gain -1, i.e. 0 dB)"
* click alone (input muted)
alter @Vinp[acmag]=0
alter @Vclk[acmag]=1
run
meas ac g_clk find vdb(out) at=1k
echo " click alone = $&g_clk dB (gain -1; unchanged by the input = virtual-ground isolation)"
.endc
.end

View file

@ -0,0 +1,38 @@
* PM_K-1 Stage 4 : balanced output driver -- antiphase + build-out into a cable
*
* The THAT1646 turns the single-ended mix into a balanced (differential) output:
* Out+ and Out- swing equal-and-opposite, so noise picked up equally on both wires
* cancels at the far end. It has a fixed +6 dB gain (differential = 2x input).
* We model it as two ideal sources (Out+ = +Vin, Out- = -Vin) and add the 47 ohm
* per-leg build-out resistors driving a 600 ohm load plus cable capacitance.
*
* Shows: audio band is dead flat, and the build-out + cable rolloff sits far above
* audio -- i.e. the build-out tames long/capacitive cables without dulling the sound.
* Run: ngspice -b ../eda/sim/stage4_driver.cir
.title Stage4 balanced driver
Vin in 0 AC 1
Eop op 0 in 0 1 ; Out+ = +Vin
Eon on 0 0 in 1 ; Out- = -Vin -> differential (op-on) = 2*Vin = +6 dB
Rbp op hot 47 ; build-out, hot leg
Rbn on cold 47 ; build-out, cold leg
Rl hot cold 600 ; receiver / load
Cch hot 0 1n ; ~cable capacitance per leg
Ccc cold 0 1n
.ac dec 20 10 10meg
.control
run
let vddb = db(v(hot) - v(cold)) ; differential magnitude in dB
meas ac d_1k find vddb at=1k
meas ac d_20k find vddb at=20k
meas ac d_1meg find vddb at=1meg
meas ac ph_hot find vp(hot) at=1k
meas ac ph_cold find vp(cold) at=1k
echo
echo " differential @1kHz : $&d_1k dB @20kHz : $&d_20k dB (flat across audio)"
echo " hot phase: $&ph_hot rad ; cold phase: $&ph_cold rad (~pi rad = 180 deg apart = balanced/antiphase)"
echo " differential @1MHz : $&d_1meg dB (build-out+cable rolloff stays above audio)"
.endc
.end

10
hardware/kicad/.gitignore vendored Normal file
View file

@ -0,0 +1,10 @@
# KiCad generated/derived outputs
*.pdf
*.net
*.svg
*.rpt
*-bak
*.kicad_prl
fp-info-cache
~*.lck
*.net

View file

@ -0,0 +1,40 @@
ERC WARNING: Only one pin (INPUT pin 3/3B of ULN2003A/U6) attached to net GNDLIFT_EN.
ERC WARNING: No drivers for net GNDLIFT_EN.
ERC WARNING: Insufficient drive current on net GNDLIFT_EN for pin INPUT pin 3/3B of ULN2003A/U6.
ERC WARNING: Only one pin (INPUT pin 15/LRCK of PCM5102A/U3) attached to net I2S_LRCK.
ERC WARNING: No drivers for net I2S_LRCK.
ERC WARNING: Insufficient drive current on net I2S_LRCK for pin INPUT pin 15/LRCK of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 2/2B of ULN2003A/U6) attached to net MUTE_EN.
ERC WARNING: No drivers for net MUTE_EN.
ERC WARNING: Insufficient drive current on net MUTE_EN for pin INPUT pin 2/2B of ULN2003A/U6.
ERC WARNING: Only one pin (INPUT pin 1/1B of ULN2003A/U6) attached to net SEL_LINST.
ERC WARNING: No drivers for net SEL_LINST.
ERC WARNING: Insufficient drive current on net SEL_LINST for pin INPUT pin 1/1B of ULN2003A/U6.
ERC WARNING: Only one pin (PASSIVE pin 1/~ of C/C2) attached to net AIN_COLD.
ERC WARNING: Only one pin (INPUT pin 14/DIN of PCM5102A/U3) attached to net I2S_DIN.
ERC WARNING: No drivers for net I2S_DIN.
ERC WARNING: Insufficient drive current on net I2S_DIN for pin INPUT pin 14/DIN of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 12/SCK of PCM5102A/U3) attached to net MCLK.
ERC WARNING: No drivers for net MCLK.
ERC WARNING: Insufficient drive current on net MCLK for pin INPUT pin 12/SCK of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 13/BCK of PCM5102A/U3) attached to net I2S_BCK.
ERC WARNING: No drivers for net I2S_BCK.
ERC WARNING: Insufficient drive current on net I2S_BCK for pin INPUT pin 13/BCK of PCM5102A/U3.
ERC WARNING: Only one pin (PASSIVE pin 3/P1_COM of TQ2SA-5V/K1) attached to net AIN_HOT.
ERC WARNING: Unconnected pin: INPUT pin 4/4B of ULN2003A/U6.
ERC WARNING: Unconnected pin: INPUT pin 5/5B of ULN2003A/U6.
ERC WARNING: Unconnected pin: INPUT pin 6/6B of ULN2003A/U6.
ERC WARNING: Unconnected pin: INPUT pin 7/7B of ULN2003A/U6.
ERC WARNING: Unconnected pin: OPEN-COLLECTOR pin 10/7C of ULN2003A/U6.
ERC WARNING: Unconnected pin: OPEN-COLLECTOR pin 11/6C of ULN2003A/U6.
ERC WARNING: Unconnected pin: OPEN-COLLECTOR pin 12/5C of ULN2003A/U6.
ERC WARNING: Unconnected pin: OPEN-COLLECTOR pin 13/4C of ULN2003A/U6.
ERC WARNING: Unconnected pin: PASSIVE pin 2/P1_NO of TQ2SA-5V/K2.
ERC WARNING: Unconnected pin: PASSIVE pin 9/P2_NO of TQ2SA-5V/K2.
ERC WARNING: Unconnected pin: PASSIVE pin 2/P1_NO of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 8/P2_COM of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 7/P2_NC of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 9/P2_NO of TQ2SA-5V/K3.
ERC INFO: 37 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,119 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on THAT1240 instantiated at /work/hardware/eda/circuits/audio_chain.py:88.
WARNING: Random tag NaoCkmuv6F generated for THAT1240.
WARNING: Missing tag on OPA1641 instantiated at /work/hardware/eda/circuits/audio_chain.py:89.
WARNING: Random tag bhZEXwD6Ma generated for OPA1641.
WARNING: Missing tag on PCM5102A instantiated at /work/hardware/eda/circuits/audio_chain.py:90.
WARNING: Random tag GKJv8Mk9db generated for PCM5102A.
WARNING: Missing tag on OPA1612 instantiated at /work/hardware/eda/circuits/audio_chain.py:91.
WARNING: Random tag ZiUYdGNUpf generated for OPA1612.
WARNING: Missing tag on THAT1646 instantiated at /work/hardware/eda/circuits/audio_chain.py:92.
WARNING: Random tag PA94RBoDz5 generated for THAT1646.
WARNING: Missing tag on ULN2003A instantiated at /work/hardware/eda/circuits/audio_chain.py:93.
WARNING: Random tag 7az6rCV1b8 generated for ULN2003A.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/audio_chain.py:94.
WARNING: Random tag a5p3EKoZSh generated for TQ2SA-5V.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/audio_chain.py:94.
WARNING: Random tag nzmU2EFKQ5 generated for TQ2SA-5V.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/audio_chain.py:94.
WARNING: Random tag PvvXwCojLV generated for TQ2SA-5V.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:101.
WARNING: Random tag 1BQDYOk7Jk generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:102.
WARNING: Random tag VJCWjFmo15 generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:102.
WARNING: Random tag F3pmXe3Ukl generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:103.
WARNING: Random tag GcBAEBfRsm generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:103.
WARNING: Random tag DxTLvXTPXw generated for D.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:101.
WARNING: Random tag TbYYz7sRwg generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:102.
WARNING: Random tag q4ANsoJP6d generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:102.
WARNING: Random tag XafPhdShAz generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:103.
WARNING: Random tag 3Dy7kKhUuS generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:103.
WARNING: Random tag srbPiv0wDA generated for D.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag cywPzV0BP5 generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag ys82MNo87q generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:116.
WARNING: Random tag GjB9TiUkLV generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:117.
WARNING: Random tag 5d0DxRmiVK generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:117.
WARNING: Random tag UiNgHe5ZoI generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/audio_chain.py:117.
WARNING: Random tag dREXHB06LQ generated for D.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:124.
WARNING: Random tag IzL4dTk5KE generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:124.
WARNING: Random tag aecw3XjGu8 generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag ZkkukzL1dw generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag SiWPcikSTD generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:139.
WARNING: Random tag XkEyeGCXrV generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:139.
WARNING: Random tag KtTaLAKE12 generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:139.
WARNING: Random tag kxLUIeU4MG generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:140.
WARNING: Random tag G0gy3YpApr generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:141.
WARNING: Random tag 54BIrx4GwW generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:142.
WARNING: Random tag D2ift49ZDj generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:143.
WARNING: Random tag 2xrRFHfzVn generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:145.
WARNING: Random tag ciywLsI1BV generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:147.
WARNING: Random tag iHP45ECBTV generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:150.
WARNING: Random tag SpTEBfUSZQ generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:150.
WARNING: Random tag GANJXVVJhX generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:150.
WARNING: Random tag cTcBdxazGj generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:150.
WARNING: Random tag WltiwW7vNV generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag K76qTGXZDl generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag qNcbyMvj04 generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:160.
WARNING: Random tag 8Z8qBauuqJ generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:160.
WARNING: Random tag ZvbhQbNpVf generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:160.
WARNING: Random tag XI22X1n2tP generated for R.
WARNING: Missing tag on R_Potentiometer instantiated at /work/hardware/eda/circuits/audio_chain.py:169.
WARNING: Random tag joe6s3brR6 generated for R_Potentiometer.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag MjG60k1gb_ generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:97.
WARNING: Random tag JNrbq6C1BQ generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:173.
WARNING: Random tag IwhpZyA9LO generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:173.
WARNING: Random tag cWxhi7FDib generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/audio_chain.py:182.
WARNING: Random tag GS23xEE7xZ generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/audio_chain.py:182.
WARNING: Random tag fJUwwIBJ1g generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 113 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,106 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
audio_chain = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'THAT1240', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'THAT1240'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='REF',func=pin_types.INPUT),
Pin(num='2',name='IN-',func=pin_types.INPUT),
Pin(num='3',name='IN+',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='SENSE',func=pin_types.PASSIVE),
Pin(num='6',name='OUT',func=pin_types.OUTPUT),
Pin(num='7',name='V+',func=pin_types.PWRIN),
Pin(num='8',name='NC',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'OPA1641', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'OPA1641'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='NC1',func=pin_types.NOCONNECT),
Pin(num='2',name='-IN',func=pin_types.INPUT),
Pin(num='3',name='+IN',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='NC5',func=pin_types.NOCONNECT),
Pin(num='6',name='OUT',func=pin_types.OUTPUT),
Pin(num='7',name='V+',func=pin_types.PWRIN),
Pin(num='8',name='NC8',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'PCM5102A', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'PCM5102A'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='CPVDD',func=pin_types.PWRIN),
Pin(num='2',name='CAPP',func=pin_types.PASSIVE),
Pin(num='3',name='CPGND',func=pin_types.PWRIN),
Pin(num='4',name='CAPM',func=pin_types.PASSIVE),
Pin(num='5',name='VNEG',func=pin_types.PASSIVE),
Pin(num='6',name='OUTL',func=pin_types.OUTPUT),
Pin(num='7',name='OUTR',func=pin_types.OUTPUT),
Pin(num='8',name='AVDD',func=pin_types.PWRIN),
Pin(num='9',name='AGND',func=pin_types.PWRIN),
Pin(num='10',name='DEMP',func=pin_types.INPUT),
Pin(num='11',name='FLT',func=pin_types.INPUT),
Pin(num='12',name='SCK',func=pin_types.INPUT),
Pin(num='13',name='BCK',func=pin_types.INPUT),
Pin(num='14',name='DIN',func=pin_types.INPUT),
Pin(num='15',name='LRCK',func=pin_types.INPUT),
Pin(num='16',name='FMT',func=pin_types.INPUT),
Pin(num='17',name='XSMT',func=pin_types.INPUT),
Pin(num='18',name='LDOO',func=pin_types.PWROUT),
Pin(num='19',name='DGND',func=pin_types.PWRIN),
Pin(num='20',name='DVDD',func=pin_types.PWRIN)] }),
Part(**{ 'name':'OPA1612', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'OPA1612'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='OUTA',func=pin_types.OUTPUT),
Pin(num='2',name='-INA',func=pin_types.INPUT),
Pin(num='3',name='+INA',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='+INB',func=pin_types.INPUT),
Pin(num='6',name='-INB',func=pin_types.INPUT),
Pin(num='7',name='OUTB',func=pin_types.OUTPUT),
Pin(num='8',name='V+',func=pin_types.PWRIN)] }),
Part(**{ 'name':'THAT1646', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'THAT1646'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='OUT-',func=pin_types.OUTPUT),
Pin(num='2',name='SNS-',func=pin_types.INPUT),
Pin(num='3',name='GND',func=pin_types.PWRIN),
Pin(num='4',name='IN',func=pin_types.INPUT),
Pin(num='5',name='V-',func=pin_types.PWRIN),
Pin(num='6',name='V+',func=pin_types.PWRIN),
Pin(num='7',name='SNS+',func=pin_types.INPUT),
Pin(num='8',name='OUT+',func=pin_types.OUTPUT)] }),
Part(**{ 'name':'ULN2003A', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'ULN2003A'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-16_3.9x9.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='1B',func=pin_types.INPUT),
Pin(num='2',name='2B',func=pin_types.INPUT),
Pin(num='3',name='3B',func=pin_types.INPUT),
Pin(num='4',name='4B',func=pin_types.INPUT),
Pin(num='5',name='5B',func=pin_types.INPUT),
Pin(num='6',name='6B',func=pin_types.INPUT),
Pin(num='7',name='7B',func=pin_types.INPUT),
Pin(num='8',name='GND',func=pin_types.PWRIN),
Pin(num='9',name='COM',func=pin_types.PWRIN),
Pin(num='10',name='7C',func=pin_types.OPENCOLL),
Pin(num='11',name='6C',func=pin_types.OPENCOLL),
Pin(num='12',name='5C',func=pin_types.OPENCOLL),
Pin(num='13',name='4C',func=pin_types.OPENCOLL),
Pin(num='14',name='3C',func=pin_types.OPENCOLL),
Pin(num='15',name='2C',func=pin_types.OPENCOLL),
Pin(num='16',name='1C',func=pin_types.OPENCOLL)] }),
Part(**{ 'name':'TQ2SA-5V', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'TQ2SA-5V'}), 'ref_prefix':'K', 'fplist':None, 'footprint':'Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='COIL_A',func=pin_types.PASSIVE),
Pin(num='10',name='COIL_B',func=pin_types.PASSIVE),
Pin(num='3',name='P1_COM',func=pin_types.PASSIVE),
Pin(num='4',name='P1_NC',func=pin_types.PASSIVE),
Pin(num='2',name='P1_NO',func=pin_types.PASSIVE),
Pin(num='8',name='P2_COM',func=pin_types.PASSIVE),
Pin(num='7',name='P2_NC',func=pin_types.PASSIVE),
Pin(num='9',name='P2_NO',func=pin_types.PASSIVE),
Pin(num='5',name='NC5',func=pin_types.NOCONNECT),
Pin(num='6',name='NC6',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_1206_3216Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'D', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'D'}), 'ref_prefix':'D', 'fplist':[''], 'footprint':'Diode_SMD:D_SOD-323', 'keywords':'diode', 'description':'Diode', 'datasheet':'~', 'pins':[
Pin(num='1',name='K',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='A',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R_Potentiometer', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R_Potentiometer'}), 'ref_prefix':'RV', 'fplist':[''], 'footprint':'Potentiometer_THT:Potentiometer_Bourns_3296W_Vertical', 'keywords':'resistor variable', 'description':'Potentiometer', 'datasheet':'~', 'pins':[
Pin(num='1',name='1',func=pin_types.PASSIVE,unit=1),
Pin(num='3',name='3',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='2',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

View file

@ -0,0 +1,92 @@
{
"board": {
"design_settings": {
"defaults": {},
"rules": {},
"track_widths": [],
"via_dimensions": []
},
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"rule_severities": {},
"rule_severitiesV2": {}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "pm_k1_core.kicad_pro",
"version": 1
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.2,
"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)",
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.25,
"via_diameter": 0.8,
"via_drill": 0.4,
"wire_width": 6
}
],
"meta": {
"version": 3
}
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"specctra_dsn": "",
"step": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"drawing": {},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"743b8308-6a69-48de-a6ff-2336bc6c804f",
""
]
],
"text_variables": {}
}

View file

@ -0,0 +1,56 @@
(kicad_sch (version 20230121) (generator eeschema)
(uuid 743b8308-6a69-48de-a6ff-2336bc6c804f)
(paper "A3")
(title_block
(title "PM_K-1 Core Board (brain) - VARASYS PolyMeter")
(date "2026-05-30")
(rev "A")
(company "VARASYS")
(comment 1 "Heirloom pro-audio modular core: RP2350 + +/-15V studio audio + balanced click-injector")
(comment 2 "Design-of-record canvas - see hardware/DESIGN.md. Symbol placement + wiring = interactive TODO in Eeschema.")
)
(lib_symbols)
(text "PM_K-1 CORE BOARD (\"brain\")\nHeirloom pro-audio modular core - design-of-record canvas.\nFull spec, BOM and interconnect pinouts: hardware/DESIGN.md + hardware/BOM.csv."
(at 20 18 0)
(effects (font (size 2.2 2.2)) (justify left top))
(uuid 00109097-7760-4eb7-b299-bd3f64383aab)
)
(text "POWER TREE\nUSB-C 5V -> AP2112K 3V3 (digital IO)\nRP2350 internal SMPS -> 1.1V core (ext L)\nTPS65131 dual boost/inverter -> raw +/-18V\nTPS7A4901 / TPS7A3001 LDO -> CLEAN +/-15V (audio)\nStar ground; switcher in a guarded corner."
(at 20 45 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid 4e184999-041e-4c11-9e86-faad487f2bad)
)
(text "MCU + DIGITAL\nRP2350A (QFN-60) - mind E9 erratum: external pulldowns on read inputs.\nW25Q128JV 16MB QSPI flash (wear-leveled).\n12MHz crystal. SWD 2x5 + labeled test points.\nI2S BCK/LRCK/DOUT + low-jitter 24.576MHz MCLK stay ON-CORE."
(at 110 45 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid 43233868-23ee-43fa-b873-a47bffeacf5f)
)
(text "CLICK SOURCE\nPCM5102A I2S DAC (Burr-Brown).\nFed by dedicated low-jitter audio XO (ASEM1-24.576MHz),\nnot PIO-jittered MCLK."
(at 200 45 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid a62ad51b-a1a9-4c34-b3d7-314c107afc7a)
)
(text "ANALOG INPUT (switchable line / instrument)\nProtection (non-negotiable): series DC-block film cap (blocks +48V phantom),\nclamp diodes/TVS to rails, series current-limit R.\nLINE: THAT1240 balanced receiver (laser-trimmed CMRR).\nINST: OPA1641 JFET Hi-Z buffer (>=1Mohm) + ~+10..15dB gain.\nK1 gold relay selects path (GPIO; touchscreen or optional face switch)."
(at 20 80 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid 737ed14f-66db-4799-a337-21438143ce09)
)
(text "MIX + OUTPUT\nMix = digital/firmware (click level via DAC); analog stage at unity.\nTHAT1646 balanced driver + 47ohm build-out per leg.\nOutput level: Bourns 3296W 25-turn trim -> DAC FS = +4dBu (~+24dBu headroom).\nMUTE relay K2: fail-safe (de-energized = muted); HW supervisor, not MCU-dependent.\nGROUND-LIFT: face panel switch IN SERIES with core relay K3; soft-lift 100ohm||10nF.\nNO electrolytics in signal path - WIMA film caps, 0.1% thin-film R."
(at 110 80 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid 99fe482f-1fde-4dd1-9579-56113dcb21bc)
)
(text "INDICATORS / MIDI / SPEAKER\nSIG/CLIP: peak detect -> LM393 -> RP2350 GPIO (UI) + LED lines on interconnect.\nMIDI: USB-MIDI default (firmware). DNP hardware option: H11L1 opto IN + 74LVC14 OUT/THRU.\nSpeaker: PAM8302 class-D, DNP per form factor.\nESD/EMI: USBLC6-2 on USB + CM choke; series R + ESD arrays on interconnect; ferrites at analog crossing."
(at 20 120 0)
(effects (font (size 1.6 1.6)) (justify left top))
(uuid 03eeefa8-7728-4e14-90c0-6d0496a0d207)
)
(text "INTERCONNECTS (see DESIGN.md s7)\nJ2 DIGITAL RIBBON 2x13 - Pico-pinout-compatible (SPI/I2C/ADC/buttons/LED + GNDLIFT_SW/LINEINST_SW/SIG_LED/CLIP_LED).\nJ3 ANALOG 2x5 - AOUT H/C, AIN H/C, AGND, CHASSIS/SHIELD, SPK+/- (DNP). Kept away from the fast digital ribbon.\nJ4 MIDI 1x6 - OUT A/B, IN A/B, +5V, GND (only if DNP MIDI populated).\nRelays + I2S + MCLK are core-only and NOT on the ribbon; a Pico test brain drives digital I/O but not the analog chain."
(at 110 120 0)
(effects (font (size 1.5 1.5)) (justify left top))
(uuid 0b1d2c3e-4f5a-6b7c-8d9e-0a1b2c3d4e5f)
)
(sheet_instances
(path "/" (page "1"))
)
)

0
hardware/kicad/skidl.erc Normal file
View file

9
hardware/kicad/skidl.log Normal file
View file

@ -0,0 +1,9 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD9_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.

View file

View file

@ -0,0 +1,9 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD9_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.

View file

@ -0,0 +1,5 @@
ERC WARNING: Only one pin (PASSIVE pin 1/~ of C/C1) attached to net AIN_HOT.
ERC WARNING: Only one pin (PASSIVE pin 1/~ of C/C2) attached to net AIN_COLD.
ERC INFO: 2 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,37 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on THAT1240 instantiated at /work/hardware/eda/circuits/stage1_input.py:47.
WARNING: Random tag QgKieWmNoe generated for THAT1240.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1_input.py:55.
WARNING: Random tag iOc5_iqozn generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1_input.py:56.
WARNING: Random tag pZqcRwtR8R generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1_input.py:56.
WARNING: Random tag YRKF1YkzBd generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1_input.py:57.
WARNING: Random tag 9_z9ZD6kzY generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1_input.py:57.
WARNING: Random tag BmXsrS9O5J generated for D.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1_input.py:55.
WARNING: Random tag AA57mtziiu generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1_input.py:56.
WARNING: Random tag OlgSnmTOjl generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1_input.py:56.
WARNING: Random tag cs8OUXPRRM generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1_input.py:57.
WARNING: Random tag i9mJhNwc3K generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1_input.py:57.
WARNING: Random tag UUQtRJt6zT generated for D.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1_input.py:76.
WARNING: Random tag MU_6d6hLHv generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1_input.py:76.
WARNING: Random tag jSRPH3NSVb generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 31 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,26 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
stage1_input = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'THAT1240', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'THAT1240'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='REF',func=pin_types.INPUT),
Pin(num='2',name='IN-',func=pin_types.INPUT),
Pin(num='3',name='IN+',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='SENSE',func=pin_types.PASSIVE),
Pin(num='6',name='OUT',func=pin_types.OUTPUT),
Pin(num='7',name='V+',func=pin_types.PWRIN),
Pin(num='8',name='NC',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_1206_3216Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'D', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'D'}), 'ref_prefix':'D', 'fplist':[''], 'footprint':'Diode_SMD:D_SOD-323', 'keywords':'diode', 'description':'Diode', 'datasheet':'~', 'pins':[
Pin(num='1',name='K',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='A',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

View file

@ -0,0 +1,11 @@
ERC WARNING: Only one pin (PASSIVE pin 10/COIL_B of TQ2SA-5V/K1) attached to net K1_DRV.
ERC WARNING: Only one pin (PASSIVE pin 3/P1_COM of TQ2SA-5V/K1) attached to net AIN_HOT.
ERC WARNING: Only one pin (PASSIVE pin 1/COIL_A of TQ2SA-5V/K1) attached to net +5V.
ERC WARNING: Only one pin (PASSIVE pin 4/P1_NC of TQ2SA-5V/K1) attached to net RX_HOT_IN.
ERC WARNING: Only one pin (PASSIVE pin 7/P2_NC of TQ2SA-5V/K1) attached to net RX_OUT.
ERC WARNING: No pins attached to net SEL_LINST.
ERC WARNING: No drivers for net SEL_LINST.
ERC WARNING: Only one pin (PASSIVE pin 8/P2_COM of TQ2SA-5V/K1) attached to net STAGE1_OUT.
ERC INFO: 8 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,31 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on OPA1641 instantiated at /work/hardware/eda/circuits/stage1b_select.py:55.
WARNING: Random tag JMxNatPRjd generated for OPA1641.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/stage1b_select.py:72.
WARNING: Random tag CfzYAxfHjh generated for TQ2SA-5V.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1b_select.py:97.
WARNING: Random tag Sd0rgwy5pV generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1b_select.py:98.
WARNING: Random tag OoRZG4Rm3b generated for R.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1b_select.py:99.
WARNING: Random tag RSLeGReAd9 generated for D.
WARNING: Missing tag on D instantiated at /work/hardware/eda/circuits/stage1b_select.py:99.
WARNING: Random tag UOfVnOhhVN generated for D.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1b_select.py:108.
WARNING: Random tag 3vZeT_Vysx generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage1b_select.py:108.
WARNING: Random tag u1DrTpQXsB generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1b_select.py:116.
WARNING: Random tag UbVM1UVNF7 generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage1b_select.py:116.
WARNING: Random tag 073687vTIh generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 25 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,37 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
stage1b_select = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'OPA1641', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'OPA1641'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='NC1',func=pin_types.NOCONNECT),
Pin(num='2',name='-IN',func=pin_types.INPUT),
Pin(num='3',name='+IN',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='NC5',func=pin_types.NOCONNECT),
Pin(num='6',name='OUT',func=pin_types.OUTPUT),
Pin(num='7',name='V+',func=pin_types.PWRIN),
Pin(num='8',name='NC8',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'TQ2SA-5V', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'TQ2SA-5V'}), 'ref_prefix':'K', 'fplist':None, 'footprint':'Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='COIL_A',func=pin_types.PASSIVE),
Pin(num='10',name='COIL_B',func=pin_types.PASSIVE),
Pin(num='3',name='P1_COM',func=pin_types.PASSIVE),
Pin(num='4',name='P1_NC',func=pin_types.PASSIVE),
Pin(num='2',name='P1_NO',func=pin_types.PASSIVE),
Pin(num='8',name='P2_COM',func=pin_types.PASSIVE),
Pin(num='7',name='P2_NC',func=pin_types.PASSIVE),
Pin(num='9',name='P2_NO',func=pin_types.PASSIVE),
Pin(num='5',name='NC5',func=pin_types.NOCONNECT),
Pin(num='6',name='NC6',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_1206_3216Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'D', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'D'}), 'ref_prefix':'D', 'fplist':[''], 'footprint':'Diode_SMD:D_SOD-323', 'keywords':'diode', 'description':'Diode', 'datasheet':'~', 'pins':[
Pin(num='1',name='K',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='A',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

View file

@ -0,0 +1,15 @@
ERC WARNING: Only one pin (INPUT pin 15/LRCK of PCM5102A/U3) attached to net I2S_LRCK.
ERC WARNING: No drivers for net I2S_LRCK.
ERC WARNING: Insufficient drive current on net I2S_LRCK for pin INPUT pin 15/LRCK of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 13/BCK of PCM5102A/U3) attached to net I2S_BCK.
ERC WARNING: No drivers for net I2S_BCK.
ERC WARNING: Insufficient drive current on net I2S_BCK for pin INPUT pin 13/BCK of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 12/SCK of PCM5102A/U3) attached to net MCLK.
ERC WARNING: No drivers for net MCLK.
ERC WARNING: Insufficient drive current on net MCLK for pin INPUT pin 12/SCK of PCM5102A/U3.
ERC WARNING: Only one pin (INPUT pin 14/DIN of PCM5102A/U3) attached to net I2S_DIN.
ERC WARNING: No drivers for net I2S_DIN.
ERC WARNING: Insufficient drive current on net I2S_DIN for pin INPUT pin 14/DIN of PCM5102A/U3.
ERC INFO: 12 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,45 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on PCM5102A instantiated at /work/hardware/eda/circuits/stage2_dac.py:65.
WARNING: Random tag SnNljvRqpw generated for PCM5102A.
WARNING: Missing tag on OPA1612 instantiated at /work/hardware/eda/circuits/stage2_dac.py:80.
WARNING: Random tag xlt7MeMuUW generated for OPA1612.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:90.
WARNING: Random tag APxc1ZswyC generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:90.
WARNING: Random tag 2Lpho8vvNO generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:90.
WARNING: Random tag ckOnqrtEN_ generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:91.
WARNING: Random tag u0XfWM3NJZ generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:95.
WARNING: Random tag 0nDWAElKnx generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:97.
WARNING: Random tag enBI6FilVb generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:98.
WARNING: Random tag BsHVUTUgsl generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage2_dac.py:106.
WARNING: Random tag MwfOtBOy0H generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage2_dac.py:115.
WARNING: Random tag T191zb3J0X generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage2_dac.py:118.
WARNING: Random tag PbFpsp3Kl4 generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage2_dac.py:118.
WARNING: Random tag uQ27wvkIQL generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:119.
WARNING: Random tag xSn6HarfSW generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:119.
WARNING: Random tag mU_gTQB1ji generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:131.
WARNING: Random tag u20v3iP1Ti generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage2_dac.py:131.
WARNING: Random tag ZBz2Y2o1hX generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 39 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,44 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
stage2_dac = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'PCM5102A', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'PCM5102A'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:TSSOP-20_4.4x6.5mm_P0.65mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='CPVDD',func=pin_types.PWRIN),
Pin(num='2',name='CAPP',func=pin_types.PASSIVE),
Pin(num='3',name='CPGND',func=pin_types.PWRIN),
Pin(num='4',name='CAPM',func=pin_types.PASSIVE),
Pin(num='5',name='VNEG',func=pin_types.PASSIVE),
Pin(num='6',name='OUTL',func=pin_types.OUTPUT),
Pin(num='7',name='OUTR',func=pin_types.OUTPUT),
Pin(num='8',name='AVDD',func=pin_types.PWRIN),
Pin(num='9',name='AGND',func=pin_types.PWRIN),
Pin(num='10',name='DEMP',func=pin_types.INPUT),
Pin(num='11',name='FLT',func=pin_types.INPUT),
Pin(num='12',name='SCK',func=pin_types.INPUT),
Pin(num='13',name='BCK',func=pin_types.INPUT),
Pin(num='14',name='DIN',func=pin_types.INPUT),
Pin(num='15',name='LRCK',func=pin_types.INPUT),
Pin(num='16',name='FMT',func=pin_types.INPUT),
Pin(num='17',name='XSMT',func=pin_types.INPUT),
Pin(num='18',name='LDOO',func=pin_types.PWROUT),
Pin(num='19',name='DGND',func=pin_types.PWRIN),
Pin(num='20',name='DVDD',func=pin_types.PWRIN)] }),
Part(**{ 'name':'OPA1612', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'OPA1612'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='OUTA',func=pin_types.OUTPUT),
Pin(num='2',name='-INA',func=pin_types.INPUT),
Pin(num='3',name='+INA',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='+INB',func=pin_types.INPUT),
Pin(num='6',name='-INB',func=pin_types.INPUT),
Pin(num='7',name='OUTB',func=pin_types.OUTPUT),
Pin(num='8',name='V+',func=pin_types.PWRIN)] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_0805_2012Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

View file

@ -0,0 +1,5 @@
ERC WARNING: Only one pin (PASSIVE pin 1/~ of R/R1) attached to net STAGE1_OUT.
ERC WARNING: Only one pin (PASSIVE pin 1/~ of R/R2) attached to net CLICK_OUT.
ERC INFO: 2 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,23 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on OPA1612 instantiated at /work/hardware/eda/circuits/stage3_sum.py:50.
WARNING: Random tag 5n2wh6xRRe generated for OPA1612.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage3_sum.py:53.
WARNING: Random tag PsTgUzSm6S generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage3_sum.py:53.
WARNING: Random tag MJ5MDnj_yt generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage3_sum.py:53.
WARNING: Random tag RyhIEBAL2O generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage3_sum.py:63.
WARNING: Random tag oOB0ftP80i generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage3_sum.py:63.
WARNING: Random tag NmK6U68Pr1 generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 17 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,23 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
stage3_sum = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'OPA1612', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'OPA1612'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='OUTA',func=pin_types.OUTPUT),
Pin(num='2',name='-INA',func=pin_types.INPUT),
Pin(num='3',name='+INA',func=pin_types.INPUT),
Pin(num='4',name='V-',func=pin_types.PWRIN),
Pin(num='5',name='+INB',func=pin_types.INPUT),
Pin(num='6',name='-INB',func=pin_types.INPUT),
Pin(num='7',name='OUTB',func=pin_types.OUTPUT),
Pin(num='8',name='V+',func=pin_types.PWRIN)] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_0805_2012Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

View file

@ -0,0 +1,12 @@
ERC WARNING: Only one pin (PASSIVE pin 10/COIL_B of TQ2SA-5V/K2) attached to net K2_DRV.
ERC WARNING: Only one pin (PASSIVE pin 1/1 of R_Potentiometer/RV1) attached to net MIX_OUT.
ERC WARNING: Only one pin (PASSIVE pin 10/COIL_B of TQ2SA-5V/K3) attached to net K3_DRV.
ERC WARNING: Unconnected pin: PASSIVE pin 2/P1_NO of TQ2SA-5V/K2.
ERC WARNING: Unconnected pin: PASSIVE pin 9/P2_NO of TQ2SA-5V/K2.
ERC WARNING: Unconnected pin: PASSIVE pin 2/P1_NO of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 8/P2_COM of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 7/P2_NC of TQ2SA-5V/K3.
ERC WARNING: Unconnected pin: PASSIVE pin 9/P2_NO of TQ2SA-5V/K3.
ERC INFO: 9 warnings found while running ERC.
ERC INFO: 0 errors found while running ERC.

View file

@ -0,0 +1,31 @@
WARNING: KICAD8_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/<frozen importlib._bootstrap_external>:995=>/work/hardware/kicad/<frozen importlib._bootstrap>:488]
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: fp-lib-table file was not found. Component footprints are not available.
WARNING: Missing tag on THAT1646 instantiated at /work/hardware/eda/circuits/stage4_driver.py:57.
WARNING: Random tag jOycpJtysP generated for THAT1646.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/stage4_driver.py:75.
WARNING: Random tag 8YvHjEQvvL generated for TQ2SA-5V.
WARNING: Missing tag on TQ2SA-5V instantiated at /work/hardware/eda/circuits/stage4_driver.py:75.
WARNING: Random tag gbZbaJRgHN generated for TQ2SA-5V.
WARNING: Missing tag on R_Potentiometer instantiated at /work/hardware/eda/circuits/stage4_driver.py:82.
WARNING: Random tag TtWL0siwIj generated for R_Potentiometer.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage4_driver.py:91.
WARNING: Random tag Il2xsDxaas generated for C.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage4_driver.py:91.
WARNING: Random tag h4JnvQCBxp generated for C.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage4_driver.py:94.
WARNING: Random tag 49MKQ_ERoo generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage4_driver.py:94.
WARNING: Random tag L4esGcufU5 generated for R.
WARNING: Missing tag on R instantiated at /work/hardware/eda/circuits/stage4_driver.py:105.
WARNING: Random tag xtw8oitG50 generated for R.
WARNING: Missing tag on C instantiated at /work/hardware/eda/circuits/stage4_driver.py:105.
WARNING: Random tag 5KGv1Z40Bi generated for C.
WARNING: Missing tag on instantiated at /work/hardware/kicad/<frozen importlib._bootstrap>:488.
INFO: 25 warnings found while generating netlist.
INFO: 0 errors found while generating netlist.

View file

@ -0,0 +1,38 @@
from collections import defaultdict
from skidl import Pin, Part, Alias, SchLib, SKIDL, TEMPLATE
from skidl.pin import pin_types
SKIDL_lib_version = '0.0.1'
stage4_driver = SchLib(tool=SKIDL).add_parts(*[
Part(**{ 'name':'THAT1646', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'THAT1646'}), 'ref_prefix':'U', 'fplist':None, 'footprint':'Package_SO:SOIC-8_3.9x4.9mm_P1.27mm', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='OUT-',func=pin_types.OUTPUT),
Pin(num='2',name='SNS-',func=pin_types.INPUT),
Pin(num='3',name='GND',func=pin_types.PWRIN),
Pin(num='4',name='IN',func=pin_types.INPUT),
Pin(num='5',name='V-',func=pin_types.PWRIN),
Pin(num='6',name='V+',func=pin_types.PWRIN),
Pin(num='7',name='SNS+',func=pin_types.INPUT),
Pin(num='8',name='OUT+',func=pin_types.OUTPUT)] }),
Part(**{ 'name':'TQ2SA-5V', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'TQ2SA-5V'}), 'ref_prefix':'K', 'fplist':None, 'footprint':'Relay_SMD:Relay_DPDT_Panasonic_TQ2-SA', 'keywords':None, 'description':'', 'datasheet':None, 'pins':[
Pin(num='1',name='COIL_A',func=pin_types.PASSIVE),
Pin(num='10',name='COIL_B',func=pin_types.PASSIVE),
Pin(num='3',name='P1_COM',func=pin_types.PASSIVE),
Pin(num='4',name='P1_NC',func=pin_types.PASSIVE),
Pin(num='2',name='P1_NO',func=pin_types.PASSIVE),
Pin(num='8',name='P2_COM',func=pin_types.PASSIVE),
Pin(num='7',name='P2_NC',func=pin_types.PASSIVE),
Pin(num='9',name='P2_NO',func=pin_types.PASSIVE),
Pin(num='5',name='NC5',func=pin_types.NOCONNECT),
Pin(num='6',name='NC6',func=pin_types.NOCONNECT)] }),
Part(**{ 'name':'R_Potentiometer', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R_Potentiometer'}), 'ref_prefix':'RV', 'fplist':[''], 'footprint':'Potentiometer_THT:Potentiometer_Bourns_3296W_Vertical', 'keywords':'resistor variable', 'description':'Potentiometer', 'datasheet':'~', 'pins':[
Pin(num='1',name='1',func=pin_types.PASSIVE,unit=1),
Pin(num='3',name='3',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='2',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'C', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'C'}), 'ref_prefix':'C', 'fplist':[''], 'footprint':'Capacitor_SMD:C_0805_2012Metric', 'keywords':'cap capacitor', 'description':'Unpolarized capacitor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] }),
Part(**{ 'name':'R', 'dest':TEMPLATE, 'tool':SKIDL, 'aliases':Alias({'R'}), 'ref_prefix':'R', 'fplist':[''], 'footprint':'Resistor_SMD:R_0805_2012Metric', 'keywords':'R res resistor', 'description':'Resistor', 'datasheet':'~', 'pins':[
Pin(num='1',name='~',func=pin_types.PASSIVE,unit=1),
Pin(num='2',name='~',func=pin_types.PASSIVE,unit=1)], 'unit_defs':[] })])

169
info-explorer.html Normal file
View file

@ -0,0 +1,169 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>VARASYS PM_X-1 Explorer - wiring, parts &amp; firmware (Pimoroni Explorer / RP2350)</title>
<meta name="description" content="PM_X-1 Explorer - the Pimoroni Explorer (PIM744, RP2350) as a 6-button polymeter metronome with live-sync to the web editor. Pinout, parts list, and the precompiled CircuitPython firmware bundle." />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;base64,@BUILD:favicon@">
<script>
(function(){ try{ var p = localStorage.getItem("metronome.theme");
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
</script>
<style>
/*@BUILD:include:src/base.css@*/
:root{ --bg1:#12151c; --bg2:#05070a; --txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
--panel-bg:#161b22; --panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#2a313c; --silk:#aab2bc; }
:root[data-theme="light"]{ --bg1:#f5f8fc; --bg2:#dde4ec; --txt:#1e2630; --muted:#5c6776; --link:#1769c4;
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#d2dae4; }
body{ margin:0; min-height:100vh; padding:22px 16px 56px; color:var(--txt);
background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2)); }
a{ color:var(--link); }
main{ width:100%; max-width:980px; margin:0 auto; }
.info-hero{ text-align:center; padding:16px 8px 2px; }
.info-hero h1{ font-size:clamp(24px,5vw,36px); margin:0; letter-spacing:-.01em; }
.info-hero .sub{ margin:9px auto 0; max-width:64ch; font-size:14.5px; }
.steps{ width:100%; max-width:760px; margin:8px auto 0; color:var(--muted); font-size:14px; line-height:1.6; }
.steps li{ margin:5px 0; }
.steps code, .about code, .sub code { background:var(--field-bg); border:1px solid var(--field-bd); border-radius:5px; padding:1px 5px; font-size:12.5px; }
.dl{ display:inline-flex; align-items:center; gap:7px; margin:4px 10px 4px 0; padding:9px 14px; border-radius:10px;
background:linear-gradient(180deg,#34c6ff,var(--cyan)); color:#04121b; font-weight:700; text-decoration:none; font-size:13.5px; }
.dl.alt{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); font-weight:600; }
</style>
</head>
<body>
/*@BUILD:include:src/header.html@*/
<main>
<section class="info-hero">
<h1>PM_X-1 Explorer</h1>
<p class="sub">The off-the-shelf <b>Pimoroni Explorer Kit</b> (RP2350, 2.8&quot; LCD, 6 buttons, piezo) as a polymeter metronome - sibling to the PM_K-1 Kit, sharing the engine, program-string grammar, and live-sync protocol with the web editor.</p>
</section>
<section class="about">
<h2>What it is</h2>
<div class="ff-tags"><span class="hw">Buildable now</span><span>RP2350 (Pico 2 class)</span><span>Pimoroni Explorer PIM744</span><span>~$60</span></div>
<p>The <a href="https://shop.pimoroni.com/products/explorer" target="_blank" rel="noopener">Pimoroni Explorer Kit (PIM744)</a> is a finished
development board: <b>RP2350B</b> built in, <b>2.8&quot; ST7789V 320x240 IPS LCD</b>, <b>6 user buttons</b> (A/B/C
on the left of the screen, X/Y/Z on the right), a <b>piezo speaker</b>, USB-C, a JST-PH battery
connector, and a mini breadboard with 6 GPIOs / 3 ADCs broken out for sensor projects. No soldering;
you flash CircuitPython and drop the firmware on. <b>No touchscreen, no joystick, no RGB LED</b> -
everything is driven from the 6 buttons.</p>
<p>It runs the same <b>polymeter engine</b> and the same <b>program strings</b> as the web editor.
Beat editing is done in the browser; <b>Live sync</b> mirrors edits to the device in real time
(HELLO/FULL/DELTA over USB-MIDI), and the device mirrors play/stop/tempo/track changes back. The
piezo clicks; a tiny on-screen dot shows run state.</p>
</section>
<details class="spec" open>
<summary>Wiring - the Pimoroni Explorer fixed pinout (no breadboarding required)</summary>
<div class="spec-body">
<p class="sub">Everything is wired on the board; this is what the firmware reads. The display is driven by an 8-bit parallel bus initialised by CircuitPython's official board definition - we use <code>board.DISPLAY</code> directly.</p>
<table class="bom">
<thead><tr><th>Component</th><th>RP2350 pins</th></tr></thead>
<tbody>
<tr class="grp"><td colspan="2">Display - 2.8&quot; ST7789V, 320x240 (8-bit parallel 8080)</td></tr>
<tr><td class="part">BL / CS / DC / WR / RD / D0-D7</td><td>GP26 / GP27 / GP28 / GP30 / GP31 / GP32-GP39 (board.c)</td></tr>
<tr class="grp"><td colspan="2">Buttons (digital, pull-up)</td></tr>
<tr><td class="part">A (play/stop) / B (tap tempo) / C (menu)</td><td>GP16 / GP15 / GP14 (left side, top to bottom)</td></tr>
<tr><td class="part">X (prev track) / Y (-bpm) / Z (next track)</td><td>GP17 / GP18 / GP19 (right side, top to bottom)</td></tr>
<tr class="grp"><td colspan="2">Audio</td></tr>
<tr><td class="part">Piezo PWM</td><td>GP12</td></tr>
<tr><td class="part">Amp enable</td><td>GP13</td></tr>
<tr class="grp"><td colspan="2">I2C (QwSTEMMA - unused by the firmware, free for sensors)</td></tr>
<tr><td class="part">SDA / SCL</td><td>GP20 / GP21</td></tr>
</tbody>
</table>
</div>
</details>
<details class="spec" open>
<summary>Controls</summary>
<div class="spec-body">
<table class="bom">
<thead><tr><th>Button</th><th>Action</th></tr></thead>
<tbody>
<tr><td class="part">A</td><td>play / stop</td></tr>
<tr><td class="part">B</td><td>tap tempo</td></tr>
<tr><td class="part">C</td><td>menu (Settings / Practice log / Help / About)</td></tr>
<tr><td class="part">X</td><td>prev track (hold to repeat)</td></tr>
<tr><td class="part">Z</td><td>next track (hold to repeat)</td></tr>
<tr><td class="part">Y</td><td>tempo -1 (hold = -5 after 1.5 s)</td></tr>
<tr><td class="part">X + Z (chord)</td><td>tempo +1 (same hold rule as Y)</td></tr>
<tr class="grp"><td colspan="2">In a menu</td></tr>
<tr><td class="part">X / Z</td><td>move cursor up / down (Help: prev / next page)</td></tr>
<tr><td class="part">Y</td><td>decrement the focused value</td></tr>
<tr><td class="part">A</td><td>cycle / increment / select</td></tr>
<tr><td class="part">B</td><td>back (cancel)</td></tr>
<tr><td class="part">C</td><td>close the menu</td></tr>
</tbody>
</table>
</div>
</details>
<details class="spec" open>
<summary>Parts</summary>
<div class="spec-body">
<p class="sub">A finished development board, not a custom build - ballpark one-off price (USD).</p>
<table class="bom">
<thead><tr><th>Part</th><th class="q">Qty</th><th class="c">~$</th></tr></thead>
<tbody>
<tr><td class="part">Pimoroni Explorer Kit (PIM744) <span class="spec">- RP2350B, 2.8&quot; ST7789V, 6 buttons, piezo + amp, USB-C</span></td><td class="q">1</td><td class="c">60</td></tr>
<tr><td class="part">USB-C cable <span class="spec">- power + flashing</span></td><td class="q">1</td><td class="c">2</td></tr>
<tr class="total"><td>Total (one-off)</td><td class="q"></td><td class="c">&asymp; $62</td></tr>
</tbody>
</table>
<p class="sub" style="margin-top:10px">Reference: <a href="https://shop.pimoroni.com/products/explorer" target="_blank" rel="noopener">Pimoroni Explorer product page</a>
&middot; <a href="https://github.com/pimoroni/explorer" target="_blank" rel="noopener">vendor code</a>
&middot; <a href="https://circuitpython.org/board/pimoroni_explorer2350/" target="_blank" rel="noopener">CircuitPython for Pimoroni Explorer (RP2350)</a>.</p>
</div>
</details>
<details class="spec" open>
<summary>Firmware - self-contained appliance (USB drive &middot; web-driven editing via Live sync &middot; MIDI audio &middot; practice log)</summary>
<div class="spec-body">
<p class="sub">The firmware turns the Explorer into a self-contained appliance: it mounts as a
<b>USB drive</b> carrying the (precompiled) firmware, your tracks and an offline copy of this editor;
drives a lanes/pads display with <b>web-driven editing</b> via <b>Live sync</b>; <b>logs your practice</b> to
<code>history.json</code>; takes new set lists <b>pushed from the editor over USB-MIDI</b>; and plays
out your <b>computer's speakers over USB-MIDI</b>. By default the firmware owns the drive (read-only to
the computer - so it can log and can't be accidentally erased); hold <b>button A</b> at power-on for
editor mode (drive writable).</p>
<p>
<a class="dl" href="/pm_x1_circuitpy.zip" download>Download CircuitPython bundle &darr;</a>
<a class="dl alt" href="https://codeberg.org/VARASYS/metronome/src/branch/main/pico-explorer" target="_blank" rel="noopener">Source + README &nearr;</a>
</p>
<ol class="steps">
<li>Flash <b>CircuitPython for Pimoroni Explorer (RP2350)</b>
(<a href="https://circuitpython.org/board/pimoroni_explorer2350/" target="_blank" rel="noopener">download</a>)
via BOOTSEL, unzip the bundle onto <code>CIRCUITPY</code>, and power-cycle. It boots into appliance mode.</li>
<li><b>Edit on the web:</b> open the <a href="/editor-beta.html">editor (beta)</a> in Chrome / Edge / Firefox,
click <b>&#x1f517; Live sync</b>, and the Explorer mirrors your edits live (beats, tempo, track changes).</li>
<li><b>Save a set list to the device</b> for offline use: set-list <b>&middot;&middot;&middot;</b> menu &rarr;
<b>&#x1f4DF; Save to device</b>. It's pushed over USB-MIDI; the device persists it to
<code>/programs.json</code>.</li>
<li><b>Play through your computer:</b> click <b>&#x1f3b9; Device audio</b>, then press <b>A</b> on the device -
the full groove sounds through your speakers over USB-MIDI, in sync. A <b>MIDI</b> badge appears in the
header and the piezo auto-mutes.</li>
<li><b>Practice log:</b> press <b>C</b> &rarr; <b>Practice log</b>. Plays over 5 s appear (time &middot; BPM &middot; duration &middot; bars).</li>
<li><b>Firmware updates:</b> &middot;&middot;&middot; menu &rarr; <b>&#x2B06; Update firmware</b> - the editor reads
the device id (X = Explorer), fetches the matching <code>pico-explorer-app.mpy</code>, and pushes it
over USB-MIDI. The device A/B-updates with automatic rollback if a build won't boot.</li>
</ol>
</div>
</details>
<p class="sub" style="max-width:760px;margin:14px auto 0">Pairs with the touch-driven <a href="/info-kit.html">PM_K-1 Kit</a> - same engine, same programs.json, same web editor.</p>
</main>
/*@BUILD:include:src/footer.html@*/
<script>
const APP_VERSION = "v0.0.1-dev";
/*@BUILD:include:src/chrome.js@*/
</script>
</body>
</html>

View file

@ -18,7 +18,8 @@
import board, busio, digitalio, analogio, pwmio, displayio, vectorio, time, json, gc, os, supervisor
supervisor.runtime.autoreload = False # we write our own files (log + pushed programs); never self-restart
APP_VERSION = "0.0.22" # firmware version (the A/B updater pushes/compares this)
APP_VERSION = "0.0.23" # firmware version (the A/B updater pushes/compares this)
DEVICE_ID = "K" # 'K' = 52Pi kit, 'X' = Pimoroni Explorer (per docs/livesync-protocol.md and the version reply)
try:
import rtc # set from the editor's clock SysEx so the log has real timestamps
except ImportError:
@ -1541,8 +1542,10 @@ class App:
if cmd == 0x01 and len(sx) >= 8 and rtc is not None: # set clock: yr-2000, mo, dd, hh, mm, ss
try: rtc.RTC().datetime = time.struct_time((2000 + sx[2], sx[3], sx[4], sx[5], sx[6], sx[7], 0, -1, -1))
except Exception: pass
elif cmd == 0x02: # version query -> reply 0x03 + APP_VERSION
if self.midi: self.midi.write(bytes([0xF0, 0x7D, 0x03]) + APP_VERSION.encode() + bytes([0xF7]))
elif cmd == 0x02: # version query -> reply 0x03 + "<device_id>;<APP_VERSION>"
if self.midi: # old firmware sent bare APP_VERSION; editor parses "contains ';'?" for back-compat
payload = DEVICE_ID + ";" + APP_VERSION
self.midi.write(bytes([0xF0, 0x7D, 0x03]) + payload.encode() + bytes([0xF7]))
elif cmd == 0x40 or cmd == 0x41 or cmd == 0x42 or cmd == 0x43: # Live sync (see src/livesync.js)
try: text = "".join(chr(b) if 0x20 <= b < 0x7F else "" for b in sx[2:])
except Exception: return

74
pico-explorer/README.md Normal file
View file

@ -0,0 +1,74 @@
# PM_X-1 "Explorer" — CircuitPython edition (Pimoroni Explorer · RP2350)
The **CircuitPython** firmware for the [Pimoroni Explorer Kit (PIM744)](https://shop.pimoroni.com/products/explorer),
set up as a self-contained appliance. Sibling to the PM_K-1 build in `../pico-cp/` (the 52Pi EP-0172
kit) — same engine, same program strings, same `programs.json`, same web editor.
This board is a **2.8″ ST7789V 320×240 LCD + 6 user buttons (A/B/C on the left, X/Y/Z on the right)
+ piezo speaker** built around an RP2350B (Pico 2 class chip). **No touchscreen, no joystick, no
RGB LED.** Editing is done in the web editor with **Live sync** on; the device mirrors changes in
real time and emits its own play/stop/bpm/sel deltas back.
## Controls
| Button | Action |
| ------ | --------------------------------------------------------------------- |
| **A** | play / stop |
| **B** | tap tempo |
| **C** | menu (Settings / Help / About / Practice log) |
| **X** | prev track (hold to repeat) |
| **Y** | tempo 1 (hold to repeat; after ~1.5 s the step grows to 5) |
| **Z** | next track (hold to repeat) |
| **X + Z** | tempo +1 (chord; same hold-repeat as Y) |
In a menu: **X / Z** move the cursor up / down, **Y** decrements the focused value, **A** commits or
cycles, **B** = back, **C** = close.
## Install
1. **Flash CircuitPython for Pico 2 / RP2350.** Hold **BOOTSEL** on the Explorer, plug it in over
USB-C, drop the [Pimoroni Explorer (RP2350) CircuitPython `.uf2`](https://circuitpython.org/board/pimoroni_explorer2350/)
onto the `RP2350` drive. A `CIRCUITPY` drive appears.
2. **Copy the bundle onto `CIRCUITPY`**`boot.py`, `code.py`, **`app.mpy`**, `programs.json`,
`font_s.bin` / `font_m.bin` / `font_l.bin`, `logo.bin` / `midi.bin` / `usb.bin`, `editor.html`
(offline editor). If an old `app.py` is on the drive, delete it.
3. **Power-cycle.** It boots into appliance mode and runs.
## Program it from the web
Open <https://metronome.varasys.io> in Chrome / Edge / Firefox. The set-list **⋯** menu →
**📟 Save to device** pushes a `programs.json` over USB-MIDI; the device persists it and reloads.
Click **🔗 Live sync** to mirror edits in real time.
## Pin reference
The display, buttons, and audio are wired into the board — no jumpers required. CircuitPython's
official board definition for `pimoroni_explorer2350` exposes `board.DISPLAY` pre-initialized, so
the firmware just uses it.
| Function | GPIO |
| ----------------- | -------- |
| Button A | GP16 |
| Button B | GP15 |
| Button C | GP14 |
| Button X | GP17 |
| Button Y | GP18 |
| Button Z | GP19 |
| Piezo audio (PWM) | GP12 |
| Piezo amp enable | GP13 |
| I²C SDA (QwSTEMMA) | GP20 |
| I²C SCL (QwSTEMMA) | GP21 |
| Display | `board.DISPLAY` (8080 parallel bus on GP26..GP39, initialized by board.c) |
## Calibration (flags at the top of `app.py`)
- **Speaker too loud / quiet:** the piezo + amp gain is fixed in hardware. `MUTE_SPEAKER`
silences the click; `SPEAKER_AUTO_MUTE` auto-mutes when a MIDI host is listening.
- **Buttons feel inverted:** the polarity is hard-coded to active-low (pull-up). If a button
fires on release instead of press, check the `BTN_*` pin map at the top of `app.py`.
- **Display orientation:** the board's CircuitPython init mounts the panel landscape
(320 × 240). If your screen looks rotated, that's a board.c-level thing — file a CircuitPython
bug, don't patch the firmware.
If `app.py` ever errors, CircuitPython prints the traceback **on the screen and over USB serial**
send me that.

1444
pico-explorer/app.py Normal file

File diff suppressed because it is too large Load diff

22
pico-explorer/boot.py Normal file
View file

@ -0,0 +1,22 @@
# boot.py - runs once at power-on (before USB connects); decides who owns the filesystem.
#
# DEFAULT = appliance mode: the FIRMWARE owns the drive, so it can save your practice log to
# /history.json and write /programs.json that the editor pushes over USB-MIDI. The drive is then
# READ-ONLY to the computer - which also protects the firmware from accidental deletion.
#
# HOLD BUTTON A (GP16 on the Pimoroni Explorer) WHILE PLUGGING IN = editor mode: the drive is
# writable by the computer, so you can drag programs.json / code.py on from any OS or browser
# (the universal fallback). Reset afterwards to return to appliance mode.
#
# Also frees a USB endpoint (disables unused HID) and makes sure USB-MIDI is available.
import board, digitalio, storage, usb_hid, usb_midi
try: usb_hid.disable()
except Exception: pass
usb_midi.enable()
a = digitalio.DigitalInOut(board.GP16)
a.switch_to_input(pull=digitalio.Pull.UP)
appliance = a.value # value True (pull-up, not pressed) -> appliance mode
a.deinit()
if appliance:
try: storage.remount("/", readonly=False) # writable by code, read-only to the computer
except Exception: pass

24
pico-explorer/code.py Normal file
View file

@ -0,0 +1,24 @@
# code.py - PM_X-1 A/B firmware loader (stable; rarely changes).
#
# The real application is the PRECOMPILED app.mpy (CircuitPython compiles a big .py at boot, which
# fragments the heap and OOMs; a .mpy loads without compiling). app.bak holds the previous known-good
# build. The web editor pushes a new app.mpy to a "trial" slot over USB-MIDI; this loader runs it, and
# if it fails to boot it AUTOMATICALLY ROLLS BACK to app.bak. (Unbrickable: BOOTSEL -> drag a .uf2.)
# app.mpy clears the /trial marker once it has run healthily for ~5s.
import supervisor, os
supervisor.runtime.autoreload = False # updates reboot explicitly; never auto-restart on our own writes
def _trial():
try: os.stat("/trial"); return True
except OSError: return False
try:
import app # runs the application (app.mpy; ends with App().run())
except Exception:
if _trial(): # a freshly-pushed build crashed on startup -> roll back
try:
os.remove("/app.mpy"); os.rename("/app.bak", "/app.mpy"); os.remove("/trial")
except Exception: pass
supervisor.reload() # reboot into the restored known-good build
else:
raise # the active build failed unexpectedly (rare) -> on-screen traceback

View file

@ -0,0 +1,3 @@
{
"setlists": []
}