From 6b6a58fa56d851bd4016f40fc12e7ca452684766 Mon Sep 17 00:00:00 2001 From: Me Here Date: Sat, 30 May 2026 20:15:17 -0500 Subject: [PATCH] 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) --- hardware/eda/circuits/stage3_sum.py | 69 +++++++++++++++++++++++++++++ hardware/eda/sim/stage3_sum.cir | 40 +++++++++++++++++ hardware/kicad/stage3_sum.erc | 5 +++ hardware/kicad/stage3_sum.log | 23 ++++++++++ hardware/kicad/stage3_sum_sklib.py | 23 ++++++++++ 5 files changed, 160 insertions(+) create mode 100644 hardware/eda/circuits/stage3_sum.py create mode 100644 hardware/eda/sim/stage3_sum.cir create mode 100644 hardware/kicad/stage3_sum.erc create mode 100644 hardware/kicad/stage3_sum.log create mode 100644 hardware/kicad/stage3_sum_sklib.py diff --git a/hardware/eda/circuits/stage3_sum.py b/hardware/eda/circuits/stage3_sum.py new file mode 100644 index 0000000..6efeaf0 --- /dev/null +++ b/hardware/eda/circuits/stage3_sum.py @@ -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) diff --git a/hardware/eda/sim/stage3_sum.cir b/hardware/eda/sim/stage3_sum.cir new file mode 100644 index 0000000..4681ad4 --- /dev/null +++ b/hardware/eda/sim/stage3_sum.cir @@ -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 diff --git a/hardware/kicad/stage3_sum.erc b/hardware/kicad/stage3_sum.erc new file mode 100644 index 0000000..6f583ca --- /dev/null +++ b/hardware/kicad/stage3_sum.erc @@ -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. + diff --git a/hardware/kicad/stage3_sum.log b/hardware/kicad/stage3_sum.log new file mode 100644 index 0000000..6383c2d --- /dev/null +++ b/hardware/kicad/stage3_sum.log @@ -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/: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 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/:488. +INFO: 17 warnings found while generating netlist. +INFO: 0 errors found while generating netlist. + diff --git a/hardware/kicad/stage3_sum_sklib.py b/hardware/kicad/stage3_sum_sklib.py new file mode 100644 index 0000000..5e7bac3 --- /dev/null +++ b/hardware/kicad/stage3_sum_sklib.py @@ -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':[] })]) \ No newline at end of file