Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / Differential input

Adder subtractor

SBOA092B page 68, Adder-Subtractor or Floating Input Combiner: E1 and E2 each through a 10 kΩ R into the inverting input with a third R back from the output, and E3 and E4 each through a 10 kΩ RI into the non-inverting input with a third RI from there to ground.

E_O = -E1 - E2 + E3 + E4
the schematic, drawn by copperhead from the circuit's netlist
The schematic, drawn by copperhead from the circuit's netlist

The schematic is drawn by copperhead’s drafting engine from this circuit’s netlist, with KiCad’s own library symbols, and it opens in KiCad as figure/adder_subtractor.kicad_sch. The op amp is KiCad’s generic one, since the handbook’s are ideal, and each terminal is a test point named as the program names it. KiCad reads back from the sheet exactly the connections the circuit has; draw_figures.py refuses to write one that does not.

the interconnect view, fang's own projection
The interconnect view, fang's own projection

The interconnect view is fang’s own projection. It names the parts as the program does, so it reads against the code below.

The figure gives every value, so nothing is chosen. What needed working out is why the + inputs are worth exactly 1 when the two sides are not built alike. The + input sits at (E3 + E4)/3, the average of E3, E4 and ground through three equal R_I. The - side’s noise gain is 1 + R/(R ∥ R) = 3. The two meet: 3 × 1/3 = 1. So the printed formula holds, and it holds for any R against R_I as long as each side is three equal resistors, which is what “R and R_I not necessarily equal” means.

Each input’s weight is a parameter written from the resistors (a_1 to a_4, and noise_gain = 3), for example:

require(
equals(
self.a_3,
over(
product(parallel(self.r_4.resistance, r_i), self.noise_gain),
total(self.r_3.resistance, parallel(self.r_4.resistance, r_i)),
),
)
)

A fifth input on either side would upset the 3 × 1/3, and the check would say so.

out/simulation.txt, from the decks under out/spice/:

RunMeasuredClaimed
e1_alone, E1 = 1 V-1-1 (a_1), holds
e2_alone, E2 = 1 V-1-1 (a_2), holds
e3_alone, E3 = 1 V11 (a_3), holds
e4_alone, E4 = 1 V11 (a_4), holds
all_four, E1..E4 = 0.1, 0.2, 0.4, 0.8 V: E_O0.9 V0.9 V, holds
all_four: the + input0.4 V(E3 + E4)/3, holds
Terminal window
fang check examples/ti_opamp_handbook/differential_input/adder_subtractor/adder_subtractor.py
python examples/regenerate.py ti_opamp_handbook/differential_input/adder_subtractor # needs ngspice
examples/ti_opamp_handbook/differential_input/adder_subtractor/adder_subtractor.py
"""The adder-subtractor, or floating input combiner, SBOA092B page 68.
Show 21 more lines
E_O = -E1 - E2 + E3 + E4
E1 and E2 each reach the inverting input through a 10 kOhm R, with a third R
from the output back to it. E3 and E4 each reach the non-inverting input
through a 10 kOhm R_I, and a third R_I runs from that input to ground.
The printed result is not the obvious one, because the two sides are not
alike: the - side has two inputs and a feedback resistor, the + side two
inputs and a resistor to ground. It holds because the numbers happen to meet.
The + input sits at the average of E3, E4 and ground, (E3 + E4)/3, and the
noise gain of the - side is 1 + R/(R || R) = 3, so each + input is worth
exactly 1 at the output. With R and R_I each three equal resistors, the ratio
between R and R_I does not matter, which is what the page means by "R and R_I
not necessarily equal". The program writes each input's weight from the
resistors, so a fourth input on either side, which would break the 3 x 1/3,
would fail the check.
The figure gives every value, so nothing was chosen. The bench drives each
input alone, then all four at once with distinct voltages.
"""
import sys
from pathlib import Path
# The handbook's shared parts and bench live in the folder above the sections.
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from fang.lang import Parameter, System, kOhm, require
from fang.parts import Resistor
from fang.rationale import Cites
from fang.simulation import OperatingPoint
from handbook import (
Bench,
Claim,
Ground,
OpAmp,
Run,
Terminal,
equals,
negative,
over,
parallel,
product,
ratio,
total,
)
class AdderSubtractor(System):
"""E1, E2 into the - input; E3, E4 into the + input over a third R_I to ground."""
figure = Cites(
"E_O = -E1 - E2 + E3 + E4. R and R_I not necessarily equal",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 68, Adder-Subtractor or Floating Input Combiner",
)
a_1 = Parameter("1", default=-1 * ratio, description="E_O / E1, the others at ground")
a_2 = Parameter("1", default=-1 * ratio, description="E_O / E2, the others at ground")
a_3 = Parameter("1", default=1 * ratio, description="E_O / E3, the others at ground")
a_4 = Parameter("1", default=1 * ratio, description="E_O / E4, the others at ground")
noise_gain = Parameter(
"1", default=3 * ratio, description="1 + R / (R || R): what the + input is worth"
)
e1 = Terminal()
e2 = Terminal()
e3 = Terminal()
e4 = Terminal()
common = Terminal()
e_out = Terminal()
r_1 = Resistor(resistance=10 * kOhm)
r_2 = Resistor(resistance=10 * kOhm)
r_feedback = Resistor(resistance=10 * kOhm)
r_3 = Resistor(resistance=10 * kOhm)
r_4 = Resistor(resistance=10 * kOhm)
r_ground = Resistor(resistance=10 * kOhm)
amp = OpAmp()
ground = Ground()
def architecture(self):
# The subtracting side: E1 and E2 into the summing point, R back.
self.e1.probe >> self.r_1.p1
self.e2.probe >> self.r_2.p1
self.r_1.p2 >> self.amp.inverting.signal
self.r_2.p2 >> self.amp.inverting.signal
self.amp.inverting.signal >> self.r_feedback.p1
self.r_feedback.p2 >> self.amp.output.signal
self.amp.output.signal >> self.e_out.probe
# The adding side: E3 and E4 into the + input, a third R_I to ground.
self.e3.probe >> self.r_3.p1
self.e4.probe >> self.r_4.p1
self.r_3.p2 >> self.amp.non_inverting.signal
self.r_4.p2 >> self.amp.non_inverting.signal
self.amp.non_inverting.signal >> self.r_ground.p1
# The common terminal the figure draws at the bottom left, and E_O's.
self.r_ground.p2 >> self.ground.node
self.common.probe >> self.ground.node
def constraints(self):
r, r_i = self.r_feedback.resistance, self.r_ground.resistance
# The - side: each input is an inverting amplifier of its own.
require(equals(self.a_1, negative(over(r, self.r_1.resistance))))
require(equals(self.a_2, negative(over(r, self.r_2.resistance))))
# The noise gain the + input sees: R over the two input resistors in
# parallel, plus one.
require(
equals(
self.noise_gain,
total(1 * ratio, over(r, parallel(self.r_1.resistance, self.r_2.resistance))),
)
)
# The + side: each input divides against the other two resistors in
# parallel, then the noise gain multiplies it back up.
require(
equals(
self.a_3,
over(
product(parallel(self.r_4.resistance, r_i), self.noise_gain),
total(self.r_3.resistance, parallel(self.r_4.resistance, r_i)),
),
)
)
require(
equals(
self.a_4,
over(
product(parallel(self.r_3.resistance, r_i), self.noise_gain),
total(self.r_4.resistance, parallel(self.r_3.resistance, r_i)),
),
)
)
def _alone(n: int) -> Run:
drive = {f"e{k}": ("DC 1" if k == n else "DC 0") for k in range(1, 5)}
return Run(
f"e{n}_alone",
OperatingPoint(),
drive=drive,
measure={f"gain_e{n}": f"v({{e_out.1}}) / v({{e{n}.1}})"},
claims=[Claim(f"gain_e{n}", f"a_{n}", within=0.001)],
)
BENCH = Bench(
page=68,
title="Adder-Subtractor or Floating Input Combiner",
runs=[
_alone(1),
_alone(2),
_alone(3),
_alone(4),
Run(
"all_four",
OperatingPoint(),
drive={"e1": "DC 0.1", "e2": "DC 0.2", "e3": "DC 0.4", "e4": "DC 0.8"},
measure={"e_o": "v({e_out.1})", "e_plus": "v({amp.IN+})"},
claims=[
Claim("e_o", 0.9, within=0.001, unit="V",
note="-0.1 - 0.2 + 0.4 + 0.8 = 0.9 V"),
Claim("e_plus", 0.4, within=0.001, unit="V",
note="(E3 + E4)/3: the + input sits at the average of E3, E4 and ground"),
],
),
],
)

The parts, then the nets and the pads on them.

out/netlist.txt
GND1 Ground -
R1 10 kOhm -
R2 10 kOhm -
R3 10 kOhm -
R4 10 kOhm -
R5 10 kOhm -
R6 10 kOhm -
TP1 Terminal -
TP2 Terminal -
TP3 Terminal -
TP4 Terminal -
TP5 Terminal -
TP6 Terminal -
U1 OpAmp -
Net-(GND1-Pad1) GND1.1 R6.2 TP1.1
Net-(R1-Pad1) R1.1 TP2.1
Net-(R1-Pad2) R1.2 R2.2 R5.1 U1.IN-
Net-(R2-Pad1) R2.1 TP3.1
Net-(R3-Pad1) R3.1 TP4.1
Net-(R3-Pad2) R3.2 R4.2 R6.1 U1.IN+
Net-(R4-Pad1) R4.1 TP5.1
Net-(R5-Pad2) R5.2 TP6.1 U1.OUT

Every check that ran, and every one left undecided.

out/checks.txt
5 checks, 0 failed, 0 undecided

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
14 component
28 connection
5 constraint
1 evidence
3 interface
22 pin
22 port
96 total
snapshot sha256:20214dee37dcf3f266296f3aebc03dca0cfa37d0169d7ee8318571c970c640a2

All of it, including the KiCad netlist, is in examples/ti_opamp_handbook/differential_input/adder_subtractor/out/. Rebuild it with:

Terminal window
fang build examples/ti_opamp_handbook/differential_input/adder_subtractor/adder_subtractor.py