Examples / TI op amp handbook / Summers
Weighted average
SBOA092B page 66, Weighted Average: E1, E2 and E3 through 10, 20 and 30 kΩ into the summing point, and back from the output 5.1 kΩ RO in series with a 1 kΩ pot RO' wired as a rheostat.
E_O = -(R_O + R_O')(E1/R1 + E2/R2 + E3/R3)R_O + R_O' = R1 ∥ R2 ∥ R3The circuit
Section titled “The circuit”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/weighted_average.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 is fang’s own projection. It names the parts as the program does, so it reads against the code below.
What the program says
Section titled “What the program says”The page’s rule sets the pot: with equal inputs E_O should be as large as
E_I, so the three weights must add to 1, and they do when the feedback equals
R1 ∥ R2 ∥ R3 = 5.4545 kΩ. The pot then supplies 354.5 Ω, 0.3545 of its travel.
The figure draws the pot and leaves the setting to the rule, so the program
records it as a decision (setting) and holds it to the rule:
require(within(feedback, parallel(parallel(inputs[0], inputs[1]), inputs[2]), 0.00001))The weights a_1..a_3 are the feedback over each input resistor, and
a_equal is their sum.
Where the handbook is off
Section titled “Where the handbook is off”- The page prints E_O = -(16.4 E1 + 8.2 E2 + 5.4 E3)/30. The exact numerators are 16.36, 8.18 and 5.45. The first two are rounded; the third is cut short (5.45 rounds to 5.5), and the printed three add to 30 only because of it.
- It asks that R_O’ be set “so E_O = E_I”. The circuit inverts, as the page’s own formula says, so the rule gives E_O = -E_I.
What the simulation found
Section titled “What the simulation found”out/simulation.txt, from the decks under
out/spice/:
| Run | Measured | Claimed |
|---|---|---|
e1_alone, E1 = 1 V | -0.5455 | -0.5455 (a_1), holds; printed 16.4/30 = 0.5467 |
e2_alone, E2 = 1 V | -0.2727 | -0.2727 (a_2), holds; printed 8.2/30 = 0.2733 |
e3_alone, E3 = 1 V | -0.1818 | -0.1818 (a_3), holds; printed 5.4/30 = 0.18 |
equal_inputs, all three 1 V | -1 | -1 (a_equal), holds |
weighted, E1..E3 = 3, -1.5, 1.2 V: E_O | -1.445 V | -1.446 V, holds |
Running it
Section titled “Running it”fang check examples/ti_opamp_handbook/summers/weighted_average/weighted_average.pypython examples/regenerate.py ti_opamp_handbook/summers/weighted_average # needs ngspiceThe whole program
Section titled “The whole program”"""The weighted average, SBOA092B page 66.Show 20 more lines
E_O = -(R_O + R_O')(E1/R1 + E2/R2 + E3/R3), R_O + R_O' = R1 || R2 || R3
A summer whose feedback is 5.1 kOhm in series with a 1 kOhm pot wired as arheostat, its wiper tied to the output end. The page's rule sets the pot:with E1 = E2 = E3, E_O should be the same size as E_I, which asks that thethree weights add to 1, and they do when the feedback equals R1 || R2 || R3.For 10, 20 and 30 kOhm that is 5.4545 kOhm, so the pot supplies 354.5 Ohm,a setting of 0.3545 of its travel. `setting` records that as a decision:the figure draws the pot and leaves where to turn it to the rule.
The weights are then 5.4545 kOhm over each input resistor, 0.5455, 0.2727and 0.1818, which the page writes over 30 as 16.4, 8.2 and 5.4. The first twoare the exact 16.36 and 8.18 rounded; the third is 5.45 cut short (it roundsto 5.5), and the three printed numbers add to 30 only because of it. Theprogram holds the weights the parts give, and the bench measures them.
The page also asks that E_O = E_I when the inputs are equal. The circuitinverts, as its own E_O formula says, so what the rule gives is E_O = -E_I."""
import sysfrom 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 decimal import Decimal
from fang.lang import Parameter, System, kOhm, requirefrom fang.parts import Resistorfrom fang.rationale import Chooses, Citesfrom fang.simulation import OperatingPoint
from handbook import ( Bench, Claim, Ground, OpAmp, Potentiometer, Run, Terminal, negative, over, parallel, product, ratio, total, within,)
class WeightedAverage(System): """E1, E2, E3 through 10, 20 and 30 kOhm; 5.1 kOhm and a 1 kOhm rheostat back."""
figure = Cites( "For E1 = E2 = E3, set R_O' so E_O = E_I. Then, R_O + R_O' = R1 || R2 || R3. " "E_O = -(R_O + R_O')E1/R1 - (R_O + R_O')E2/R2 - (R_O + R_O')E3/R3 = " "-(16.4 E1 + 8.2 E2 + 5.4 E3)/30", document="SBOA092B, Handbook of Operational Amplifier Applications", locator="page 66, Weighted Average", )
setting = Chooses( "Where is R_O' set?", selected="0.3545 of its 1 kOhm travel, 354.5 Ohm, so R_O + R_O' = R1 || R2 || R3", alternatives=[ { "option": "the pot at its midpoint, 500 Ohm", "reason": "5.6 kOhm of feedback makes the weights add to 1.027, and " "equal inputs come out 2.7% large", }, { "option": "the pot's full 1 kOhm", "reason": "6.1 kOhm of feedback makes the weights add to 1.118", }, ], rationale=( "the figure draws the pot and gives the rule that sets it, not a setting", "R1 || R2 || R3 is 5.4545 kOhm, and 5.1 kOhm leaves 354.5 Ohm for the pot", ), )
a_1 = Parameter("1", default=Decimal("-0.545455") * ratio, description="-(R_O + R_O')/R1") a_2 = Parameter("1", default=Decimal("-0.272727") * ratio, description="-(R_O + R_O')/R2") a_3 = Parameter("1", default=Decimal("-0.181818") * ratio, description="-(R_O + R_O')/R3") a_equal = Parameter( "1", default=-1 * ratio, description="E_O / E_I with all three inputs at E_I" )
e1 = Terminal() e2 = Terminal() e3 = Terminal() common = Terminal() e_out = Terminal()
r_1 = Resistor(resistance=10 * kOhm) r_2 = Resistor(resistance=20 * kOhm) r_3 = Resistor(resistance=30 * kOhm) r_out = Resistor(resistance=Decimal("5.1") * kOhm) trim = Potentiometer(resistance=1 * kOhm, setting=Decimal("0.354545") * ratio) amp = OpAmp() ground = Ground()
def architecture(self): self.e1.probe >> self.r_1.p1 self.e2.probe >> self.r_2.p1 self.e3.probe >> self.r_3.p1 self.r_1.p2 >> self.amp.inverting.signal self.r_2.p2 >> self.amp.inverting.signal self.r_3.p2 >> self.amp.inverting.signal
# R_O, then the pot as a rheostat: its wiper tied to its far end. self.amp.inverting.signal >> self.r_out.p1 self.r_out.p2 >> self.trim.end_a self.trim.wiper >> self.trim.end_b self.trim.end_b >> self.amp.output.signal
self.amp.output.signal >> self.e_out.probe self.amp.non_inverting.signal >> self.ground.node self.common.probe >> self.ground.node
def constraints(self): # The pot contributes the part of its travel between pin 1 and the wiper. feedback = total(self.r_out.resistance, product(self.trim.resistance, self.trim.setting)) inputs = (self.r_1.resistance, self.r_2.resistance, self.r_3.resistance)
# The page's rule for setting R_O'. require(within(feedback, parallel(parallel(inputs[0], inputs[1]), inputs[2]), 0.00001))
# `within` takes its band as a fraction of the target, so the weights # are compared as magnitudes: a negative target would turn the band over. for weight, r_n in zip((self.a_1, self.a_2, self.a_3), inputs): require(within(negative(weight), over(feedback, r_n), 0.00001)) require( within( negative(self.a_equal), negative(total(self.a_1, self.a_2, self.a_3)), 0.00001, ) )
def _alone(n: int, printed: str) -> Run: drive = {f"e{k}": ("DC 1" if k == n else "DC 0") for k in range(1, 4)} return Run( f"e{n}_alone", OperatingPoint(), drive=drive, measure={f"weight_e{n}": f"v({{e_out.1}}) / v({{e{n}.1}})"}, claims=[Claim(f"weight_e{n}", f"a_{n}", within=0.001, note=printed)], )
BENCH = Bench( page=66, title="Weighted Average", runs=[ _alone(1, "The handbook prints 16.4/30 = 0.5467; 5.4545k/10k is 0.5455 (16.36/30)."), _alone(2, "The handbook prints 8.2/30 = 0.2733; 5.4545k/20k is 0.2727 (8.18/30)."), _alone(3, ( "The handbook prints 5.4/30 = 0.18; 5.4545k/30k is 0.1818 (5.45/30, " "which rounds to 5.5, not 5.4)." )), Run( "equal_inputs", OperatingPoint(), drive={"e1": "DC 1", "e2": "DC 1", "e3": "DC 1"}, measure={"gain": "v({e_out.1}) / v({e1.1})"}, claims=[ Claim("gain", "a_equal", within=0.001, note=( "The rule the pot is set by. The handbook asks for " "E_O = E_I; the circuit inverts, so it is E_O = -E_I." )), ], ), Run( "weighted", OperatingPoint(), drive={"e1": "DC 3", "e2": "DC -1.5", "e3": "DC 1.2"}, measure={"e_o": "v({e_out.1})"}, claims=[ Claim("e_o", -1.4455, within=0.001, unit="V", note="-(0.54545 x 3 + 0.27273 x (-1.5) + 0.18182 x 1.2) = -1.4455 V"), ], ), ],)The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
GND1 Ground -R1 10 kOhm -R2 20 kOhm -R3 30 kOhm -R4 5.1 kOhm -RV1 Potentiometer -TP1 Terminal -TP2 Terminal -TP3 Terminal -TP4 Terminal -TP5 Terminal -U1 OpAmp -Net-(GND1-Pad1) GND1.1 TP1.1 U1.IN+Net-(R1-Pad1) R1.1 TP2.1Net-(R1-Pad2) R1.2 R2.2 R3.2 R4.1 U1.IN-Net-(R2-Pad1) R2.1 TP3.1Net-(R3-Pad1) R3.1 TP4.1Net-(R4-Pad2) R4.2 RV1.1Net-(RV1-Pad2) RV1.2 RV1.3 TP5.1 U1.OUTEvery check that ran, and every one left undecided.
5 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
1 block 12 component 26 connection 5 constraint 1 decision 1 evidence 3 interface 20 pin 20 port 89 totalsnapshot sha256:d9043dafe5d5c7b910a192d4c3eab5a8b89612915ac39e64bb1973f70b4a1f48All of it, including the KiCad netlist, is in
examples/ti_opamp_handbook/summers/weighted_average/out/. Rebuild it with:
fang build examples/ti_opamp_handbook/summers/weighted_average/weighted_average.py