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>
This commit is contained in:
Me Here 2026-05-30 19:56:55 -05:00
parent 075c1786af
commit e6f425ee6f
5 changed files with 228 additions and 0 deletions

View file

@ -0,0 +1,121 @@
#!/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).
CROSS-CHECK BEFORE LAYOUT (standard single-side-stable diagram, not over-claimed):
* which throw is NC (closed when de-energized) -> we use 4 & 7 for LINE; swap if the
exact connection diagram differs (so LINE stays the default).
* coil terminal pair (using 1 & 10); coil polarity irrelevant (no internal diode).
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,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,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':[] })])