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>
This commit is contained in:
Me Here 2026-05-30 20:27:04 -05:00
parent 6b6a58fa56
commit 5a75dbbbdc
5 changed files with 232 additions and 0 deletions

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)

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

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