Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / Differential amplifiers

Common mode rejection

SBOA092B page 75, Common Mode Rejection: "subtraction by inverting and summing". The bottom op amp inverts E2 (R4 and R5, 10 kΩ each). The top op amp sums: E1 through R1, and the inverted E2 through R2 (9.1 kΩ) in series with R3 (a 5 kΩ pot wired as a rheostat), both into its inverting input, with R0 (100 kΩ) as feedback.

E_O = -(R_O / R1)(E1 - E2) = 10 (E2 - E1)
R2 + R3 = R1
R3: common mode adjustment. Set for zero output when E1 = E2.
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/common_mode_rejection.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.

Written out, the top amplifier gives

E_O = -(R0/R1) E1 + (R0/(R2 + R3))(R5/R4) E2

so the output is zero for E1 = E2 when R2 + R3 = R1 (R5/R4), which is the page’s rule since R5 = R4.

The program records a reading (reading) because the drawn values satisfy neither printed equation (see below). It takes R1 = 10 kΩ, and records the trim (trim): the rheostat’s wiper is tied to its summing-point end, and it is set 0.82 of the way along, leaving 0.9 kΩ in circuit so R2 + R3 = 10 kΩ. The constraints hold the gains to the parts (a_e1 = -R0/R1, a_e2 = R0/(R2 + R3) · R5/R4, a_inv = -R5/R4), the trim rule, that the pot can reach it (R2 ≤ R1 ≤ R2 + R3), and that the two paths are equal and opposite.

As drawn, R1 is 1 kΩ. That makes the gain R0/R1 = 100, not the printed 10, and R2 + R3 can only range from 9.1 to 14.1 kΩ, so it can never equal R1. The E2 path is then worth at most 100/9.1 = 11 at the output against E1’s 100, and no setting of R3 nulls the output for E1 = E2. The trim-range constraint would fail on the drawn value.

One change fixes both printed equations: R1 = 10 kΩ. The gain becomes 10, as printed, and the trim point falls at 0.9 kΩ of the 5 kΩ pot. The other candidates each fix only one equation (R0 = 10 kΩ gives the gain but not the trim) or need two misprints (R2 = 910 Ω and R3 = 500 Ω), and reading lists them.

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

RunMeasuredClaimed
e1_alone, E1 = 1 V-10-10 (a_e1), holds
e2_alone, E2 = 1 V1010 (a_e2), holds
e2_alone: bottom amplifier-1-1 (a_inv), holds
difference, E1 = 0.4 V, E2 = 0.6 V2 V2 V, holds
common_mode_trimmed, E1 = E2 = 1 V-20 µV0 V ± 100 µV, holds
common_mode_untrimmed, R3 at mid-travel-1.379 V-10 + 100/11.6, holds

The 20 µV left at the trim point is the inverter’s finite open-loop gain: its gain is short of -1 by about 2 parts in 10^6, and the top amplifier multiplies that by 10. The untrimmed run is what the adjustment is for.

Terminal window
fang check examples/ti_opamp_handbook/differential_amplifiers/common_mode_rejection/common_mode_rejection.py
python examples/regenerate.py ti_opamp_handbook/differential_amplifiers/common_mode_rejection # needs ngspice
examples/ti_opamp_handbook/differential_amplifiers/common_mode_rejection/common_mode_rejection.py
"""Common mode rejection by inverting and summing, SBOA092B page 75.
Show 23 more lines
E_O = -(R_O / R1)(E1 - E2) = 10 (E2 - E1), R2 + R3 = R1
Two op amps. The bottom one inverts E2 (R4 in, R5 across, both 10 kOhm). The
top one is a summing amplifier: E1 reaches its inverting input through R1,
the inverted E2 reaches the same point through R2 (9.1 kOhm) in series with
R3, a 5 kOhm potentiometer wired as a rheostat, and R0 (100 kOhm) is the
feedback. Its output is
E_O = -(R0/R1) E1 + (R0/(R2 + R3))(R5/R4) E2
which is zero for E1 = E2 when R2 + R3 = R1 (R5/R4) = R1. R3 is the page's
"common mode adjustment".
As drawn the page cannot be right. R1 is 1 kOhm, so the gain would be
R0/R1 = 100, not the printed 10, and R2 + R3 runs from 9.1 to 14.1 kOhm, so it
can never equal R1: the E2 path is worth at most 100/9.1 = 11 against E1's
100, and the trim cannot null anything. One value fixes both printed
equations at once: R1 = 10 kOhm. Then R0/R1 is 10, as printed, and
R2 + R3 = R1 with R3 at 0.9 kOhm, inside the pot's range. `reading` records
that, and the program is built on it; the figure's "1 kOhm" for R1 is the
erratum. `trim` records where the rheostat is set.
"""
import sys
from decimal import Decimal
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 Chooses, Cites
from fang.simulation import OperatingPoint
from handbook import (
Bench,
Claim,
Ground,
OpAmp,
Potentiometer,
Run,
Terminal,
at_least,
at_most,
equals,
minus,
negative,
over,
product,
ratio,
total,
)
class CommonModeRejection(System):
"""An inverter on E2, summed with E1 into one inverting amplifier."""
figure = Cites(
"E_O = -(R_O/R_1)(E1 - E2) = 10(E2 - E1); R2 + R3 = R1. "
"R3 - common mode adjustment. Set for zero output when E1 = E2. "
"(R1 drawn as 1 kOhm, R0 100 kOhm, R2 9.1 kOhm, R3 5 kOhm, R4 and R5 10 kOhm)",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 75, Common Mode Rejection",
)
reading = Chooses(
"Which of the figure's values is misprinted?",
selected=(
"R1: it is 10 kOhm, not the 1 kOhm drawn. Then R0/R1 = 10 as printed, "
"and R2 + R3 = R1 at R3 = 0.9 kOhm, inside the 5 kOhm pot"
),
alternatives=[
{
"option": "R1 = 1 kOhm, as drawn",
"reason": (
"the gain is then 100, not the printed 10, and R2 + R3 is at "
"least 9.1 kOhm, so it can never equal R1: the E2 path is "
"worth at most 11 against E1's 100, and no trim nulls E1 = E2"
),
},
{
"option": "R0 = 10 kOhm, with R1 = 1 kOhm",
"reason": (
"gives the printed gain of 10 but leaves R2 + R3 = R1 = 1 kOhm "
"out of the pot's reach, so it fixes one printed equation of two"
),
},
{
"option": "R2 = 910 Ohm and R3 = 500 Ohm, with R1 = 1 kOhm",
"reason": (
"makes the trim reachable but leaves the gain at 100, and "
"needs two values misprinted where the chosen reading needs one"
),
},
],
rationale=(
"the page prints two equations, a gain of 10 and R2 + R3 = R1, and "
"the drawn values satisfy neither",
"R1 = 10 kOhm is the one change that satisfies both, and matches R4 "
"and R5 beside it",
),
)
trim = Chooses(
"Where is the R3 rheostat set?",
selected=(
"0.82 of its travel from the summing-point end, which leaves 0.9 kOhm "
"in circuit and makes R2 + R3 = R1"
),
alternatives=[
{
"option": "mid-travel",
"reason": (
"2.5 kOhm in circuit makes R2 + R3 = 11.6 kOhm and leaves 1.38 V "
"out for 1 V on both inputs; the bench shows it, untrimmed"
),
},
],
rationale=(
"the page says to set R3 for zero output when E1 = E2",
"the wiper is tied to the end at the summing point, so the part left "
"in circuit is the travel from the wiper to R2",
),
)
a_e1 = Parameter("1", default=-10 * ratio, description="E_O / E1, with E2 at ground")
a_e2 = Parameter("1", default=10 * ratio, description="E_O / E2, with E1 at ground")
a_inv = Parameter("1", default=-1 * ratio, description="the bottom amplifier's gain")
e1 = Terminal()
e2 = Terminal()
common = Terminal()
e_out = Terminal()
r1 = Resistor(resistance=10 * kOhm)
r0 = Resistor(resistance=100 * kOhm)
r2 = Resistor(resistance=Decimal("9.1") * kOhm)
r3 = Potentiometer(resistance=5 * kOhm, setting=Decimal("0.82") * ratio)
r4 = Resistor(resistance=10 * kOhm)
r5 = Resistor(resistance=10 * kOhm)
amp_sum = OpAmp()
amp_invert = OpAmp()
ground = Ground()
def architecture(self):
# The top amplifier: E1 through R1 into the summing point, R0 back.
self.e1.probe >> self.r1.p1
self.r1.p2 >> self.amp_sum.inverting.signal
self.amp_sum.inverting.signal >> self.r0.p1
self.r0.p2 >> self.amp_sum.output.signal
self.amp_sum.output.signal >> self.e_out.probe
# The bottom amplifier: E2 inverted by R4 and R5.
self.e2.probe >> self.r4.p1
self.r4.p2 >> self.amp_invert.inverting.signal
self.amp_invert.inverting.signal >> self.r5.p1
self.r5.p2 >> self.amp_invert.output.signal
# The inverted E2 back to the summing point through R2 and the R3
# rheostat, whose wiper is tied to its summing-point end.
self.amp_invert.output.signal >> self.r2.p1
self.r2.p2 >> self.r3.end_b
self.r3.end_a >> self.amp_sum.inverting.signal
self.r3.wiper >> self.amp_sum.inverting.signal
# Both + inputs, and the terminal at the bottom left, on ground.
self.amp_sum.non_inverting.signal >> self.ground.node
self.amp_invert.non_inverting.signal >> self.ground.node
self.common.probe >> self.ground.node
def constraints(self):
# What is left of R3 in circuit: the travel from the wiper to R2.
r3_in_circuit = product(self.r3.resistance, minus(1 * ratio, self.r3.setting))
e2_path = total(self.r2.resistance, r3_in_circuit)
require(equals(self.a_inv, negative(over(self.r5.resistance, self.r4.resistance))))
require(equals(self.a_e1, negative(over(self.r0.resistance, self.r1.resistance))))
require(
equals(
self.a_e2,
product(over(self.r0.resistance, e2_path), negative(self.a_inv)),
)
)
# The page's trim rule, R2 + R3 = R1, with the inverter's gain written
# in (it is 1 here); and that the pot can reach it at all, which is
# what the drawn R1 of 1 kOhm fails.
require(equals(e2_path, product(self.r1.resistance, negative(self.a_inv))))
require(at_most(self.r2.resistance, self.r1.resistance))
require(at_least(total(self.r2.resistance, self.r3.resistance), self.r1.resistance))
# Common-mode rejection: the two paths are equal and opposite.
require(equals(self.a_e2, negative(self.a_e1)))
ERRATUM = (
"on the reading that R1 is 10 kOhm; the figure's 1 kOhm would give "
"a gain of 100 and no reachable null"
)
BENCH = Bench(
page=75,
title="Common Mode Rejection",
runs=[
Run(
"e1_alone",
OperatingPoint(),
drive={"e1": "DC 1", "e2": "DC 0"},
measure={"gain_e1": "v({e_out.1}) / v({e1.1})"},
claims=[Claim("gain_e1", "a_e1", within=0.001, note=ERRATUM)],
),
Run(
"e2_alone",
OperatingPoint(),
drive={"e1": "DC 0", "e2": "DC 1"},
measure={
"gain_e2": "v({e_out.1}) / v({e2.1})",
"inverted": "v({amp_invert.OUT}) / v({e2.1})",
},
claims=[
Claim("gain_e2", "a_e2", within=0.001,
note="R0 / (R2 + 0.9 kOhm of R3), through the inverter"),
Claim("inverted", "a_inv", within=0.001),
],
),
Run(
"difference",
OperatingPoint(),
drive={"e1": "DC 0.4", "e2": "DC 0.6"},
measure={"e_o": "v({e_out.1})"},
claims=[Claim("e_o", 2.0, within=0.001, unit="V",
note="10 (E2 - E1) = 10 x 0.2 V, as printed")],
),
Run(
"common_mode_trimmed",
OperatingPoint(),
drive={"e1": "DC 1", "e2": "DC 1"},
measure={"e_o": "v({e_out.1})"},
claims=[
Claim(
"e_o", 0, within=1e-4, absolute=True, unit="V",
note=(
"1 V on both inputs with R3 trimmed so R2 + R3 = R1. What "
"is left is the two op amps' finite gain"
),
)
],
),
Run(
"common_mode_untrimmed",
OperatingPoint(),
drive={"e1": "DC 1", "e2": "DC 1"},
settings={"r3": {"setting": 0.5}},
measure={"e_o": "v({e_out.1})"},
claims=[
Claim(
"e_o", -10 + 100 / 11.6, within=0.001, unit="V",
note=(
"R3 at mid-travel: R2 + R3 = 11.6 kOhm, so E_O = "
"-10 + 100/11.6 = -1.379 V for 1 V on both inputs. "
"This is what the trim is for"
),
)
],
),
],
)

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

out/netlist.txt
GND1 Ground -
R1 100 kOhm -
R2 10 kOhm -
R3 9.1 kOhm -
R4 10 kOhm -
R5 10 kOhm -
RV1 Potentiometer -
TP1 Terminal -
TP2 Terminal -
TP3 Terminal -
TP4 Terminal -
U1 OpAmp -
U2 OpAmp -
Net-(GND1-Pad1) GND1.1 TP1.1 U1.IN+ U2.IN+
Net-(R1-Pad1) R1.1 R2.2 RV1.1 RV1.2 U2.IN-
Net-(R1-Pad2) R1.2 TP4.1 U2.OUT
Net-(R2-Pad1) R2.1 TP2.1
Net-(R3-Pad1) R3.1 R5.2 U1.OUT
Net-(R3-Pad2) R3.2 RV1.3
Net-(R4-Pad1) R4.1 TP3.1
Net-(R4-Pad2) R4.2 R5.1 U1.IN-

Every check that ran, and every one left undecided.

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

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
13 component
32 connection
7 constraint
2 decision
1 evidence
3 interface
24 pin
24 port
107 total
snapshot sha256:e1be2efe806b42ae6038a2ab3d5ae5290561e137becc141bb4dd9495d38ee923

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

Terminal window
fang build examples/ti_opamp_handbook/differential_amplifiers/common_mode_rejection/common_mode_rejection.py