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>
This commit is contained in:
Me Here 2026-05-30 20:06:26 -05:00
parent e6f425ee6f
commit 2f44be6f63
6 changed files with 282 additions and 4 deletions

View file

@ -17,10 +17,12 @@ 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. * 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) * 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). -- 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): RESOLVED from the Panasonic TQ-SMD connection diagram (single-side-stable, top view)
* which throw is NC (closed when de-energized) -> we use 4 & 7 for LINE; swap if the + the contact-resistance terminal pairs (2-3,3-4,7-8,8-9):
exact connection diagram differs (so LINE stays the default). * coil = pins 1 & 10 (polarity irrelevant -- non-latching single coil, no diode).
* coil terminal pair (using 1 & 10); coil polarity irrelevant (no internal 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 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 (sinks the coil) + SEL_LINST (the RP2350 GPIO). The ULN2003 is instantiated once in
the power/control block, not per-relay. the power/control block, not per-relay.

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,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,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':[] })])