Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / DC amplifiers

Linear gain control

SBOA092B page 71, Linear Gain Control: an inverting amplifier with RI 10 kΩ in and a 100 kΩ potentiometer wired as a rheostat across it, its wiper tied to the end at the output.

E_O = 0 to -10 E_I
Z_in = R_I = 10 kΩ
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/linear_gain_control.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 rheostat puts the setting times 100 kΩ in the loop, so the gain is -10 times the setting, a straight line. The figure gives no setting, so the program records one decision (rheostat): the setting counts from the summing-point end, and the claims are held at full travel, the printed -10. Two constraints tie the claims to the parts:

require(equals(self.a_v, negative(over(product(self.r_out.setting, self.r_out.resistance), self.r_in.resistance))))
require(equals(self.z_in, self.r_in.resistance))

out/simulation.txt, from the decks under out/spice/, each an operating point with E_I = 1 V:

SettingGainZ_inClaimed
1 (full travel)-1010 kΩ-10 (a_v), 10 kΩ (z_in), hold
0.75-7.5-7.5, holds
0.5-5-5, holds
0.25-2.5-2.5, holds
0E_O = -100 nV10 kΩ0 V, 10 kΩ, hold

The gain is linear in the setting and Z_in does not move, which is the difference from the simple gain control before it on page 70. At the bottom of the range only the model’s 1 mΩ wiper contact is in the loop.

Terminal window
fang check examples/ti_opamp_handbook/dc_amplifiers/linear_gain_control/linear_gain_control.py
python examples/regenerate.py ti_opamp_handbook/dc_amplifiers/linear_gain_control # needs ngspice
examples/ti_opamp_handbook/dc_amplifiers/linear_gain_control/linear_gain_control.py
"""Linear gain control, SBOA092B page 71.
Show 15 more lines
E_O = 0 to -10 E_I
Z_in = R_I = 10 kOhm
An inverting amplifier whose feedback resistor is a 100 kOhm potentiometer
wired as a rheostat: the wiper is tied to the end at the output, so the
resistance in the loop is the setting times 100 kOhm and the gain is
-10 times the setting, a straight line from 0 to -10. The input resistor is
fixed, so Z_in stays 10 kOhm whatever the setting.
The figure draws the wiper joined to the right-hand end, which is the output.
The program had to decide which end of the travel the setting counts from
(`rheostat`) and which setting the claims are held at (full travel, the
printed -10). The bench then moves the wiper to show the line is straight.
"""
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,
equals,
negative,
over,
product,
ratio,
)
class LinearGainControl(System):
"""E_I through R_I into the summing point, a rheostat back from the output."""
figure = Cites(
"E_O = 0 to -10 E_I, Z_in = R_I = 10 kOhm; variable from 0 to 10",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 71, Linear Gain Control",
)
rheostat = Chooses(
"Which end of the 100 kOhm pot is in the loop, and at what setting are the claims held?",
selected=(
"end 1 on the summing point, wiper and end 3 on the output, so the "
"loop sees setting x 100 kOhm; claims held at full travel, -10"
),
alternatives=[
{
"option": "count the setting from the output end",
"reason": (
"the gain would still be linear but would fall as the "
"setting rises; counting from the summing point makes the "
"setting and the gain magnitude move together"
),
},
{
"option": "hold the claims at mid travel",
"reason": "the printed figure is the end of the range, -10; mid travel is checked on the bench",
},
],
rationale=(
"the figure ties the wiper to the end at the output, a rheostat",
"the page prints the range, 0 to -10, and full travel is its end",
),
)
a_v = Parameter("1", default=-10 * ratio, description="E_O / E_I at full travel")
z_in = Parameter("Ohm", default=10 * kOhm, description="what E_I sees")
e_in = Terminal()
e_out = Terminal()
r_in = Resistor(resistance=10 * kOhm)
r_out = Potentiometer(resistance=100 * kOhm, setting=Decimal("1") * ratio)
amp = OpAmp()
ground = Ground()
def architecture(self):
self.e_in.probe >> self.r_in.p1
self.r_in.p2 >> self.amp.inverting.signal
self.amp.inverting.signal >> self.r_out.end_a
self.r_out.wiper >> self.amp.output.signal
self.r_out.end_b >> self.amp.output.signal
self.amp.output.signal >> self.e_out.probe
self.amp.non_inverting.signal >> self.ground.node
def constraints(self):
# The rheostat puts setting x R_O in the loop, so the gain is linear in it.
require(
equals(
self.a_v,
negative(
over(
product(self.r_out.setting, self.r_out.resistance),
self.r_in.resistance,
)
),
)
)
# The summing point is a virtual ground, so E_I sees R_I alone.
require(equals(self.z_in, self.r_in.resistance))
def _setting(value: float, gain: float) -> Run:
return Run(
f"setting_{int(value * 100):03d}",
OperatingPoint(),
drive={"e_in": "DC 1"},
settings={"r_out": {"setting": value}},
measure={"gain": "v({e_out.1}) / v({e_in.1})"},
claims=[
Claim(
"gain",
gain,
within=0.001,
note=f"-10 x {value}: the rheostat puts {value * 100:g} kOhm in the loop",
)
],
)
BENCH = Bench(
page=71,
title="Linear Gain Control",
runs=[
Run(
"full_travel",
OperatingPoint(),
drive={"e_in": "DC 1"},
measure={
"gain": "v({e_out.1}) / v({e_in.1})",
"z_in": "-v({e_in.1}) / i(vdrive_e_in)",
},
claims=[
Claim("gain", "a_v", within=0.001),
Claim("z_in", "z_in", within=0.001, unit="Ohm"),
],
),
_setting(0.75, -7.5),
_setting(0.5, -5.0),
_setting(0.25, -2.5),
Run(
"setting_000",
OperatingPoint(),
drive={"e_in": "DC 1"},
settings={"r_out": {"setting": 0}},
measure={
"e_out": "v({e_out.1})",
"z_in": "-v({e_in.1}) / i(vdrive_e_in)",
},
claims=[
Claim(
"e_out",
0,
within=1e-6,
absolute=True,
unit="V",
note="the bottom of the range: only the pot's 1 mOhm contact is in the loop",
),
Claim("z_in", "z_in", within=0.001, unit="Ohm"),
],
note="Z_in does not move with the setting, unlike the simple gain control just before it in the handbook.",
),
],
)

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

out/netlist.txt
GND1 Ground -
R1 10 kOhm -
RV1 Potentiometer -
TP1 Terminal -
TP2 Terminal -
U1 OpAmp -
Net-(GND1-Pad1) GND1.1 U1.IN+
Net-(R1-Pad1) R1.1 TP1.1
Net-(R1-Pad2) R1.2 RV1.1 U1.IN-
Net-(RV1-Pad2) RV1.2 RV1.3 TP2.1 U1.OUT

Every check that ran, and every one left undecided.

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

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
6 component
14 connection
2 constraint
1 decision
1 evidence
3 interface
11 pin
11 port
50 total
snapshot sha256:ed1f9631f3b3995525ac922d05ef3700909f90d820d0f69fd14751e072647997

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

Terminal window
fang build examples/ti_opamp_handbook/dc_amplifiers/linear_gain_control/linear_gain_control.py