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>
This commit is contained in:
Me Here 2026-05-30 20:15:17 -05:00
parent 2f44be6f63
commit 6b6a58fa56
5 changed files with 160 additions and 0 deletions

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