Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / DC amplifiers

Power booster

SBOA092B pages 71 and 72, Power Booster: a compound amplifier. A precision OPA277 runs the outer loop at G = 1 + 20k/1k = +21; a power OPA512 inside it runs a local loop at 1 + 10k/4.7k = 3.13 (10 pF across its 10 kΩ); two 0.1 Ω in parallel sit between the OPA512 and EO, inside both loops; 47 pF from the OPA277's output to its - input adds phase lead for stability. EI is on the OPA277's + input with 100 kΩ to ground.

G = +21
Table 1, compound: V_OS 20 µV, V_OUT ±35 V, I_OUT 10 A, SR 2.4 V/µs
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/power_booster.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.

models gives each op amp its column of Table 1: the OPA277 swings ±13 V with 20 µV of offset, the OPA512 ±35 V with 6 mV. The table gives no bandwidth, so the gain-bandwidths are the parts’ data-sheet figures, 1 MHz and 4 MHz. The macro-model has no slew rate or current limit, so those rows are recorded and not simulated. load adds the 10 Ω the figure does not draw. The constraints hold G = 21 and the local 3.13 to the resistors; say that 30 V out needs 1.4286 V in, is beyond the OPA277’s own +13 V and inside the OPA512’s +35 V, while the OPA277 itself supplies only E_O / 3.13 = 9.6 V; and put the compound’s output offset at 21 times the OPA277’s.

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

RunMeasuredClaimed
thirty_volts, gain, E_I = 1.4286 V into 10 Ω2121 (g), holds
thirty_volts, E_O30 V (3 A)30 V (e_out), holds
thirty_volts, OPA277 output9.598 V9.6 V (e_front), holds
thirty_volts, OPA512 output30.15 Vnot a claim
offset, E_O with E_I = 0-420.1 µV-420 µV (v_os_out), holds
stability, gain at 100 Hz26.44 dB20 log 21, holds
stability, peak above the DC gain0 dBat most 1 dB, holds
stability, -3 dB point139 kHznot a claim
step, 0.1 V step, final value2.1 V2.1 V, holds
step, overshoot0under 5%, holds

The OPA277 swings 9.6 V while the load gets 30 V and 3 A, and the output offset is the OPA277’s 20 µV times 21 (negative here only because the model subtracts its offset from the + input); the OPA512’s 6 mV shows up at the OPA277’s output, not at E_O. With single-pole models at 1 MHz and 4 MHz the two loops do not peak and the step does not overshoot: the response is monotonic, rolling off at 139 kHz. That is a statement about these models; a real OPA512’s extra poles and its slew rate are not in them.

Terminal window
fang check examples/ti_opamp_handbook/dc_amplifiers/power_booster/power_booster.py
python examples/regenerate.py ti_opamp_handbook/dc_amplifiers/power_booster # needs ngspice
examples/ti_opamp_handbook/dc_amplifiers/power_booster/power_booster.py
"""The power booster, a compound amplifier, SBOA092B pages 71 and 72.
Show 21 more lines
G = 1 + 20k / 1k = +21
Page 71 says any of the circuits before it can drive more current if a power
booster is put inside its loop; page 72 draws one. A precision OPA277 runs
the outer loop: E_I reaches its + input across 100 kOhm to ground, and 20 kOhm
from the final output over 1 kOhm to ground sets the gain at 21. A power
OPA512 inside that loop runs a local one: its + input is the OPA277's output,
and 10 kOhm with 10 pF across it, over 4.7 kOhm to ground, make it a gain of
1 + 10k/4.7k = 3.13. Two 0.1 Ohm in parallel sit between the OPA512 and E_O,
inside both loops, and 47 pF from the OPA277's output to its - input adds the
"small amount of phase shift to help stabilize the system".
The point of the figure is Table 1: the compound keeps the OPA277's 20 uV of
offset and reaches the OPA512's +/-35 V. The program models each op amp with
its column of the table (`models`), and the claims are that the gain is 21,
that 30 V reaches the load while the OPA277 swings only 9.6 V, which is
inside its own +/-13 V, that the output offset is the OPA277's times 21, and
that the two loops together are stable. The load is not drawn; `load` is the
program's.
"""
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 MHz, Ohm, Parameter, System, V, kOhm, mOhm, mV, pF, require, uV
from fang.parts import Capacitor, Resistor
from fang.rationale import Chooses, Cites
from fang.simulation import ACSweep, OperatingPoint, Transient
from handbook import (
Bench,
Claim,
Ground,
OpAmp,
Run,
Terminal,
at_least,
at_most,
equals,
negative,
over,
product,
ratio,
total,
within,
)
class PowerBooster(System):
"""OPA277 in the outer loop at a gain of 21, OPA512 in a local loop inside it."""
figure = Cites(
"G = +21. The compound amplifier: V_OS 20 uV, V_OUT +/-35 V, I_OUT 10 A "
"(Table 1). The 47pF capacitor provides a small amount of phase shift "
"to help stabilize the system.",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 72, Power Booster (Table 1, Compound Amplifier, Resulting Performance)",
)
models = Chooses(
"What does each op amp's model hold?",
selected=(
"Table 1's columns: OPA277 +/-13 V swing and 20 uV offset, OPA512 "
"+/-35 V swing and 6 mV offset; gain-bandwidth 1 MHz for the "
"OPA277 and 4 MHz for the OPA512, from their data sheets; open-loop "
"gain left at the bench's 120 dB"
),
alternatives=[
{
"option": "the bench's default op amp for both",
"reason": (
"its +/-13.5 V swing is the very limit the booster exists "
"to pass, and its 10 MHz would hide whether the two loops "
"are stable at the parts' real speeds"
),
},
{
"option": "model slew rate and output current limit too",
"reason": (
"the bench's macro-model has neither; the table's 2.4 V/us "
"and 10 A are recorded here and not simulated"
),
},
],
rationale=(
"the table gives swing and offset for each part",
"the table gives no bandwidth; 1 MHz and 4 MHz are the OPA277's and OPA512's data-sheet figures",
),
)
load = Chooses(
"What does E_O drive?",
selected="10 Ohm to ground, so 30 V out is 3 A, well past the OPA277's 5 mA",
alternatives=[
{
"option": "no load",
"reason": "a power stage with nothing to drive shows nothing a single OPA277 could not do, except swing",
},
{
"option": "a lower resistance, near the 10 A the table allows",
"reason": "the macro-model has no current limit, so a heavier load would prove nothing more",
},
],
rationale=("the figure draws no load",),
)
g = Parameter("1", default=21 * ratio, description="E_O / E_I, the outer loop")
g_local = Parameter("1", default=Decimal("3.13") * ratio, description="the OPA512's own gain, rounded")
e_drive = Parameter("V", default=Decimal("1.4286") * V, description="the bench's E_I")
e_out = Parameter("V", default=30 * V, description="what that E_I puts across the load")
e_front = Parameter("V", default=Decimal("9.6") * V, description="the OPA277's output meanwhile")
v_os_out = Parameter("V", default=-420 * uV, description="E_O with E_I at zero")
e_in = Terminal()
e_out_terminal = Terminal()
r_bias = Resistor(resistance=100 * kOhm)
r_ground = Resistor(resistance=1 * kOhm)
r_feedback = Resistor(resistance=20 * kOhm)
c_front = Capacitor(capacitance=47 * pF)
r_local_ground = Resistor(resistance=Decimal("4.7") * kOhm)
r_local = Resistor(resistance=10 * kOhm)
c_local = Capacitor(capacitance=10 * pF)
r_ballast_a = Resistor(resistance=100 * mOhm)
r_ballast_b = Resistor(resistance=100 * mOhm)
r_load = Resistor(resistance=10 * Ohm)
front = OpAmp(
gain_bandwidth=1 * MHz,
output_high=13 * V,
output_low=-13 * V,
input_offset=20 * uV,
)
booster = OpAmp(
gain_bandwidth=4 * MHz,
output_high=35 * V,
output_low=-35 * V,
input_offset=6 * mV,
)
ground = Ground()
def architecture(self):
# The OPA277: E_I on its + input, the outer loop on its - input.
self.e_in.probe >> self.front.non_inverting.signal
self.front.non_inverting.signal >> self.r_bias.p1
self.front.inverting.signal >> self.r_ground.p1
self.front.inverting.signal >> self.r_feedback.p1
self.front.inverting.signal >> self.c_front.p1
self.c_front.p2 >> self.front.output.signal
# The OPA512, driven by the OPA277, with its own loop from E_O.
self.front.output.signal >> self.booster.non_inverting.signal
self.booster.inverting.signal >> self.r_local_ground.p1
self.booster.inverting.signal >> self.r_local.p1
self.booster.inverting.signal >> self.c_local.p1
# The two 0.1 Ohm from the OPA512 to E_O, and everything E_O feeds.
self.booster.output.signal >> self.r_ballast_a.p1
self.booster.output.signal >> self.r_ballast_b.p1
self.r_ballast_a.p2 >> self.e_out_terminal.probe
self.r_ballast_b.p2 >> self.e_out_terminal.probe
self.e_out_terminal.probe >> self.r_local.p2
self.e_out_terminal.probe >> self.c_local.p2
self.e_out_terminal.probe >> self.r_feedback.p2
self.e_out_terminal.probe >> self.r_load.p1
self.r_bias.p2 >> self.ground.node
self.r_ground.p2 >> self.ground.node
self.r_local_ground.p2 >> self.ground.node
self.r_load.p2 >> self.ground.node
def constraints(self):
require(
equals(
self.g,
total(1 * ratio, over(self.r_feedback.resistance, self.r_ground.resistance)),
)
)
require(
within(
self.g_local,
total(1 * ratio, over(self.r_local.resistance, self.r_local_ground.resistance)),
0.001,
)
)
# 30 V out for the bench's drive, past the OPA277's own swing and
# inside the OPA512's, while the OPA277 supplies only E_O / 3.13.
require(within(self.e_out, product(self.g, self.e_drive), 0.001))
require(at_least(self.e_out, self.front.output_high))
require(at_most(self.e_out, self.booster.output_high))
require(within(self.e_front, over(self.e_out, self.g_local), 0.005))
require(at_most(self.e_front, self.front.output_high))
# The compound's offset is the OPA277's, times the outer gain. The
# macro-model subtracts its offset from the + input, so a positive
# offset shows as a negative output.
require(equals(self.v_os_out, negative(product(self.g, self.front.input_offset))))
BENCH = Bench(
page=72,
title="Power Booster",
runs=[
Run(
"thirty_volts",
OperatingPoint(),
drive={"e_in": "DC 1.4286"},
measure={
"gain": "v({e_out_terminal.1}) / v({e_in.1})",
"e_out": "v({e_out_terminal.1})",
"e_front": "v({front.OUT})",
"i_load": "v({e_out_terminal.1}) / 10",
"e_booster": "v({booster.OUT})",
},
claims=[
Claim(
"gain",
"g",
within=0.001,
note="the OPA277's 20 uV offset, times 21, moves this by 0.001%",
),
Claim("e_out", "e_out", within=0.001, unit="V"),
Claim(
"e_front",
"e_front",
within=0.005,
unit="V",
note=(
"30 V x 4.7k / 14.7k is 9.592 V, plus the OPA512's 6 mV "
"of offset, which its own input carries; the claim "
"rounds to 9.6 V, hence 0.5%"
),
),
],
units={"e_booster": "V", "i_load": "A"},
note=(
"E_I = 1.4286 V into a 10 Ohm load. The OPA277 alone would stop "
"at 13 V; here it swings 9.6 V while E_O reaches 30 V and 3 A."
),
),
Run(
"offset",
OperatingPoint(),
drive={"e_in": "DC 0"},
measure={"v_os_out": "v({e_out_terminal.1})"},
claims=[
Claim(
"v_os_out",
"v_os_out",
within=0.01,
unit="V",
note=(
"21 x the OPA277's 20 uV, negative because the model "
"subtracts its offset from the + input. The OPA512's "
"6 mV is inside the outer loop and divided by the "
"OPA277's gain"
),
)
],
),
Run(
"stability",
ACSweep(points=40, start="10", stop="100meg"),
drive={"e_in": "DC 0 AC 1"},
measure={
"gain_db": "find vdb({e_out_terminal.1}) at=100",
"peak_db": "max vdb({e_out_terminal.1}) from=10 to=100meg",
"f_3db": "when vdb({e_out_terminal.1})=23.44 fall=1",
"peaking": "peak_db - gain_db",
},
claims=[
Claim("gain_db", 26.444, within=0.01, absolute=True, note="20 log 21"),
Claim(
"peaking",
0,
within=1,
absolute=True,
note="no more than 1 dB of peak above the DC gain: the two loops together do not ring",
),
],
units={"f_3db": "Hz"},
note=(
"The -3 dB point is not a handbook claim; it is where the "
"OPA277's 1 MHz, over a noise gain of 21 and helped by the "
"OPA512's 3.13, runs out."
),
),
Run(
"step",
Transient(stop="40u", step="10n"),
drive={"e_in": "PULSE(0 0.1 1u 10n 10n 100u 200u)"},
measure={
"final": "find v({e_out_terminal.1}) at=39u",
"peak": "max v({e_out_terminal.1}) from=1u to=39u",
"overshoot": "(peak - final) / final",
},
claims=[
Claim("final", 2.1, within=0.001, unit="V", note="21 x 0.1 V"),
Claim(
"overshoot",
0,
within=0.05,
absolute=True,
note="a 0.1 V step settles with under 5% overshoot",
),
],
units={"peak": "V"},
note="A small step, so the missing slew limit does not matter.",
),
],
)

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

out/netlist.txt
C1 47 pF -
C2 10 pF -
GND1 Ground -
R1 100 mOhm -
R2 100 mOhm -
R3 100 kOhm -
R4 20 kOhm -
R5 1 kOhm -
R6 10 Ohm -
R7 10 kOhm -
R8 4.7 kOhm -
TP1 Terminal -
TP2 Terminal -
U1 OpAmp -
U2 OpAmp -
Net-(C1-Pad1) C1.1 R4.1 R5.1 U2.IN-
Net-(C1-Pad2) C1.2 U1.IN+ U2.OUT
Net-(C2-Pad1) C2.1 R7.1 R8.1 U1.IN-
Net-(C2-Pad2) C2.2 R1.2 R2.2 R4.2 R6.1 R7.2 TP2.1
Net-(GND1-Pad1) GND1.1 R3.2 R5.2 R6.2 R8.2
Net-(R1-Pad1) R1.1 R2.1 U1.OUT
Net-(R3-Pad1) R3.1 TP1.1 U2.IN+

Every check that ran, and every one left undecided.

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

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
15 component
44 connection
8 constraint
2 decision
1 evidence
3 interface
29 pin
29 port
132 total
snapshot sha256:c4f10de51d0312fc6bc5a515313acebfb468d993eabbbf2f96461292d6a366bd

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

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