From 5a75dbbbdc3b5391fccc8b78311ae6812f55fd7e Mon Sep 17 00:00:00 2001 From: Me Here Date: Sat, 30 May 2026 20:27:04 -0500 Subject: [PATCH] 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) --- hardware/eda/circuits/stage4_driver.py | 113 +++++++++++++++++++++++++ hardware/eda/sim/stage4_driver.cir | 38 +++++++++ hardware/kicad/stage4_driver.erc | 12 +++ hardware/kicad/stage4_driver.log | 31 +++++++ hardware/kicad/stage4_driver_sklib.py | 38 +++++++++ 5 files changed, 232 insertions(+) create mode 100644 hardware/eda/circuits/stage4_driver.py create mode 100644 hardware/eda/sim/stage4_driver.cir create mode 100644 hardware/kicad/stage4_driver.erc create mode 100644 hardware/kicad/stage4_driver.log create mode 100644 hardware/kicad/stage4_driver_sklib.py diff --git a/hardware/eda/circuits/stage4_driver.py b/hardware/eda/circuits/stage4_driver.py new file mode 100644 index 0000000..cba3467 --- /dev/null +++ b/hardware/eda/circuits/stage4_driver.py @@ -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) diff --git a/hardware/eda/sim/stage4_driver.cir b/hardware/eda/sim/stage4_driver.cir new file mode 100644 index 0000000..22dd16f --- /dev/null +++ b/hardware/eda/sim/stage4_driver.cir @@ -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 diff --git a/hardware/kicad/stage4_driver.erc b/hardware/kicad/stage4_driver.erc new file mode 100644 index 0000000..0cc1ee5 --- /dev/null +++ b/hardware/kicad/stage4_driver.erc @@ -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. + diff --git a/hardware/kicad/stage4_driver.log b/hardware/kicad/stage4_driver.log new file mode 100644 index 0000000..7015c8f --- /dev/null +++ b/hardware/kicad/stage4_driver.log @@ -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/:995=>/work/hardware/kicad/:488] +WARNING: KICAD6_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/:995=>/work/hardware/kicad/:488] +WARNING: KICAD7_SYMBOL_DIR environment variable is missing, so the default KiCad symbol libraries won't be searched. @ [/work/hardware/kicad/:995=>/work/hardware/kicad/: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/:488. +INFO: 25 warnings found while generating netlist. +INFO: 0 errors found while generating netlist. + diff --git a/hardware/kicad/stage4_driver_sklib.py b/hardware/kicad/stage4_driver_sklib.py new file mode 100644 index 0000000..8b7b63e --- /dev/null +++ b/hardware/kicad/stage4_driver_sklib.py @@ -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':[] })]) \ No newline at end of file