Examples / TI op amp handbook / Current output
Feedback loop
SBOA092B page 79, Feedback Loop: EI through R1 (1 kΩ) into the summing point, and the load RL from there to the output, where an inverting amplifier's feedback resistor would be. The + input is on ground.
I = E_I / R1 = E_I mA, Z_in = R1 = 1 kΩThe 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/feedback_loop.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 summing point sits at ground, so the current through R1 is E_I / R1, and
it has nowhere to go but through the load. The figure draws R_L between two
terminals with no value, so the program makes it a Load a run can change,
and records the values used as a decision (load). The two claims are
parameters tied to R1:
require(equals(self.i_per_volt, over(1 * ratio, self.r1.resistance)))require(equals(self.z_in, self.r1.resistance))What the simulation found
Section titled “What the simulation found”out/simulation.txt, E_I = 1 V throughout:
| R_L | I / E_I | Z_in | E_O |
|---|---|---|---|
| 100 Ω | 1 mA/V, holds | 1 kΩ, holds | -0.1 V |
| 1 kΩ | 1 mA/V, holds | 1 kΩ, holds | -1 V |
| 10 kΩ | 1 mA/V, holds | 1 kΩ, holds | -10 V |
| 20 kΩ | 0.69 mA/V, not a claim | -13.5 V, claimed, holds |
The current does not move with the load until the output runs out of swing. At 20 kΩ the load needs -20 V; the output stops at -13.5 V and the current falls to 14.5 V / 21 kΩ.
Running it
Section titled “Running it”fang check examples/ti_opamp_handbook/current_output/feedback_loop/feedback_loop.pypython examples/regenerate.py ti_opamp_handbook/current_output/feedback_loop # needs ngspiceThe whole program
Section titled “The whole program”"""The feedback-loop current source, SBOA092B page 79.Show 15 more lines
I = E_I / R_1 = E_I mA, Z_in = R_1 = 1 kOhm
The load sits where the feedback resistor of an inverting amplifier would, sothe current through R_1 is the current through the load, whatever the loadis. The figure draws R_L between two terminals and gives it no value: it isthe thing being driven, not part of the circuit. The program makes it a`Load` whose resistance a run can set, because a claim that the current doesnot depend on R_L means nothing until R_L has been moved.
The bench drives E_I with 1 V and reads the load current for 100 Ohm, 1 kOhmand 10 kOhm, then asks for 20 kOhm, which needs -20 V at the output: the opamp stops at -13.5 V and the current falls short, which is where the claimends."""
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 fang.lang import Parameter, System, UnitLiteral, kOhm, requirefrom fang.parts import Resistor, TwoPinfrom fang.rationale import Chooses, Citesfrom fang.simulation import OperatingPoint
from handbook import Bench, Claim, Ground, OpAmp, Run, Terminal, equals, over, ratio
#: A transconductance: the current out per volt in.mA_per_V = UnitLiteral("mA/V")
class Load(TwoPin): """What the current is delivered into: a resistance a run may change.Show 5 more lines
SPICE knows a resistor, but the bench only rewrites a part it writes itself, so the load writes its own card, and a zero-volt source after it so a measurement reads the load current as `i(v<ref>_sense)`. """
designator_prefix = "RL" resistance = Parameter("Ohm")
def spice(self, ref, node, value): return ( [ f"R{ref} {node('1')} sense_{ref} {value('resistance'):.6g}", f"V{ref}_SENSE sense_{ref} {node('2')} DC 0", ], {}, )
def describe(self, value) -> str: return f"load of {value('resistance'):.6g} Ohm, with its current sensed"
class FeedbackLoop(System): """E_I through R_1 into the summing point, the load from there to the output."""
figure = Cites( "I = E_I / R_I = E_I mA; Z_in = R_I = 1 kOhm", document="SBOA092B, Handbook of Operational Amplifier Applications", locator="page 79, Feedback Loop", )
load = Chooses( "What is R_L?", selected="1 kOhm by default, and 100 Ohm, 10 kOhm and 20 kOhm in the runs", alternatives=[ { "option": "one fixed load", "reason": "the claim is that the current does not depend on R_L, " "which one value cannot show", }, ], rationale=( "the figure draws R_L between two terminals with no value: it is " "the thing driven, not part of the source", "10 kOhm at 1 mA puts the output at -10 V, inside the swing; 20 kOhm " "asks for -20 V and shows where the claim stops", ), )
i_per_volt = Parameter("A/V", default=1 * mA_per_V, description="I / E_I") z_in = Parameter("Ohm", default=1 * kOhm, description="what E_I sees")
e_in = Terminal() e_out = Terminal() r1 = Resistor(resistance=1 * kOhm) r_load = Load(resistance=1 * kOhm) amp = OpAmp() ground = Ground()
def architecture(self): self.e_in.probe >> self.r1.p1 self.r1.p2 >> self.amp.inverting.signal self.amp.inverting.signal >> self.r_load.p1 self.r_load.p2 >> self.amp.output.signal self.amp.output.signal >> self.e_out.probe self.amp.non_inverting.signal >> self.ground.node
def constraints(self): require(equals(self.i_per_volt, over(1 * ratio, self.r1.resistance))) require(equals(self.z_in, self.r1.resistance))
def _load(resistance: float) -> Run: """One operating point with the load set to a resistance.""" return Run( f"load_{resistance:g}_ohm", OperatingPoint(), drive={"e_in": "DC 1"}, settings={"r_load": {"resistance": resistance}}, measure={ "i_per_volt": "i(vrl1_sense) / v({e_in.1})", "z_in": "v({e_in.1}) / (-i(vdrive_e_in))", "e_out": "v({e_out.1})", }, claims=[ Claim("i_per_volt", "i_per_volt", within=0.001, unit="A/V"), Claim("z_in", "z_in", within=0.001, unit="Ohm"), ], units={"e_out": "V"}, )
BENCH = Bench( page=79, title="Feedback Loop", runs=[ _load(100), _load(1000), _load(10000), Run( "load_20000_ohm", OperatingPoint(), drive={"e_in": "DC 1"}, settings={"r_load": {"resistance": 20000}}, measure={ "i_per_volt": "i(vrl1_sense) / v({e_in.1})", "e_out": "v({e_out.1})", }, claims=[ Claim( "e_out", -13.5, within=0.01, unit="V", note="20 kOhm at 1 mA needs -20 V; the output stops at the " "-13.5 V swing, and the current falls to 14.5 V / 21 kOhm", ), ], units={"i_per_volt": "A/V"}, note="Past the swing: the claim I = E_I / R_1 holds only while " "E_I R_L / R_1 fits inside the output swing.", ), ],)The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
GND1 Ground -R1 1 kOhm -RL1 Load -TP1 Terminal -TP2 Terminal -U1 OpAmp -Net-(GND1-Pad1) GND1.1 U1.IN+Net-(R1-Pad1) R1.1 TP1.1Net-(R1-Pad2) R1.2 RL1.1 U1.IN-Net-(RL1-Pad2) RL1.2 TP2.1 U1.OUTEvery check that ran, and every one left undecided.
2 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
1 block 6 component 12 connection 2 constraint 1 decision 1 evidence 3 interface 10 pin 10 port 46 totalsnapshot sha256:6cd12b7075f3c74eb6bf4ed5f6e6a5d54a3b9ef1a7ce8375487a7c027d6b75eeAll of it, including the KiCad netlist, is in
examples/ti_opamp_handbook/current_output/feedback_loop/out/. Rebuild it with:
fang build examples/ti_opamp_handbook/current_output/feedback_loop/feedback_loop.py