Examples / TI op amp handbook / Current output
Linear current source
SBOA092B page 81, Linear Current Source, and its Table 2. The first op amp sums EI (through R1) and the load voltage (through the upper R2) against R0, with CO across R0; the second inverts its output; R3 runs from the second output into the load.
When R_L << R2, I_L / E_I = R2 / (R1 R3) = 1 mA / VoltThe 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/linear_current_source.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 second output is R2 E_I / R1 + V_L, so the current R3 delivers is
R2 E_I / (R1 R3) whatever V_L is. The upper R2 takes V_L / R2 of it back,
which is the “R_L << R2” condition: 0.1% for the 100 Ω load chosen. Table 2
changes R3 alone, so R3 is a part a run can change. C_O has no value in the
figure; c_o_value records 100 pF, and load records the 100 Ω load.
What the simulation found
Section titled “What the simulation found”| Run | R3 | I_L / E_I | Claimed (Table 2) |
|---|---|---|---|
r3_1000_ohm | 1 kΩ | 0.999 mA/V | 1 mA/V (i_per_volt) ±0.2%, holds |
r3_100_ohm | 100 Ω | 9.99 mA/V | 10 mA/V ±0.2%, holds |
r3_10_ohm (E_I = 100 mV) | 10 Ω | 99.89 mA/V | 100 mA/V ±0.2%, holds |
step, 1 V step, current at 500 µs | 1 kΩ | 0.999 mA, no overshoot | holds |
Every row is low by R_L / R2, the current the upper R2 takes back.
Running it
Section titled “Running it”fang check examples/ti_opamp_handbook/current_output/linear_current_source/linear_current_source.pypython examples/regenerate.py ti_opamp_handbook/current_output/linear_current_source # needs ngspiceThe whole program
Section titled “The whole program”"""The linear current source, SBOA092B page 81.Show 18 more lines
When R_L << R2, I_L / E_I = R2 / (R1 R3) = 1 mA / Volt
Two op amps. The first sums E_I through R1 and the load's voltage throughthe upper R2, both 100 kOhm, against R0 (100 kOhm, with C_O across it). Thesecond inverts the first's output through a second R0 and R2, so its outputis R2 E_I / R1 + V_L, and R3 turns the difference between that and V_L intoa current, R2 E_I / (R1 R3), whatever V_L is. The upper R2 takes V_L / R2 ofit back, which is the error "R_L << R2" waves away: with 100 Ohm of load itis 0.1%.
Table 2 scales the current by R3 alone: 1 kOhm for 1 mA per volt, 100 Ohmfor 10, 10 Ohm for 100. The program makes R3 a part a run can change, andthe bench runs all three rows.
The figure gives C_O no value and R_L none either; `c_o` and `load` recordwhat was taken."""
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 Ohm, Parameter, System, UnitLiteral, kOhm, pF, requirefrom fang.parts import Capacitor, Resistor, TwoPinfrom fang.rationale import Chooses, Citesfrom fang.simulation import OperatingPoint, Transient
from handbook import Bench, Claim, Ground, OpAmp, Run, Terminal, equals, over, product
#: A transconductance: the current out per volt in.mA_per_V = UnitLiteral("mA/V")
class SettableResistor(TwoPin): """A resistor a run may change: the load, or Table 2's scaling resistor.Show 5 more lines
SPICE knows a resistor, but the bench only rewrites a part it writes itself, so this one writes its own card, and a zero-volt source after it so a measurement reads its current as `i(v<ref>_sense)`. """
designator_prefix = "RS" 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"{value('resistance'):.6g} Ohm, with its current sensed"
class LinearCurrentSource(System): """A summing inverter, an inverter after it, and R3 into the load."""
figure = Cites( "When R_L << R2, I_L / E_I = R2 / (R1 R3) = 1 mA / Volt. C_O added for " "high frequency stability. Change R3 for current scaling: 1 kOhm, " "1 mA / Volt; 100 Ohm, 10 mA / Volt; 10 Ohm, 100 mA / Volt.", document="SBOA092B, Handbook of Operational Amplifier Applications", locator="page 81, Linear Current Source, and Table 2", )
c_o_value = Chooses( "What is C_O?", selected="100 pF, across the first amplifier's R0", alternatives=[ { "option": "leave it out", "reason": "the page says it is there for stability; the d.c. " "claims do not depend on it, but the drawn circuit has it", }, ], rationale=( "the figure draws C_O across R0 and gives no value", "100 pF against 100 kOhm rolls the first stage off at 16 kHz, far " "above anything the d.c. runs ask of it", ), )
load = Chooses( "What is R_L?", selected="100 Ohm", alternatives=[ { "option": "10 kOhm", "reason": "the upper R2 would then take 10% of the current, " "where the formula assumes R_L << R2", }, ], rationale=( "the current falls short by R_L / R2, 0.1% at 100 Ohm", "at 10 mA per volt the load sits at 1 V, and the second output at 2 V", ), )
i_per_volt = Parameter( "mA/V", default=1 * mA_per_V, description="I_L / E_I: R2 / (R1 R3)" )
e_in = Terminal() r1 = Resistor(resistance=100 * kOhm) r0_first = Resistor(resistance=100 * kOhm) c_o = Capacitor(capacitance=100 * pF) r0_second = Resistor(resistance=100 * kOhm) r2_second = Resistor(resistance=100 * kOhm) r2_upper = Resistor(resistance=100 * kOhm) r3 = SettableResistor(resistance=1 * kOhm) r_load = SettableResistor(resistance=100 * Ohm) first = OpAmp() second = OpAmp() ground = Ground()
def architecture(self): # The first amplifier sums E_I and the load voltage. self.e_in.probe >> self.r1.p1 self.r1.p2 >> self.first.inverting.signal self.first.inverting.signal >> self.r0_first.p1 self.r0_first.p1 >> self.c_o.p1 self.r0_first.p1 >> self.r2_upper.p1 self.r0_first.p2 >> self.first.output.signal self.c_o.p2 >> self.first.output.signal self.first.non_inverting.signal >> self.ground.node
# The second inverts it. self.first.output.signal >> self.r0_second.p1 self.r0_second.p2 >> self.second.inverting.signal self.second.inverting.signal >> self.r2_second.p1 self.r2_second.p2 >> self.second.output.signal self.second.non_inverting.signal >> self.ground.node
# R3 into the load, and the upper R2 back from it. self.second.output.signal >> self.r3.p1 self.r3.p2 >> self.r_load.p1 self.r_load.p1 >> self.r2_upper.p2 self.r_load.p2 >> self.ground.node
def constraints(self): require( equals( self.i_per_volt, over(self.r2_upper.resistance, product(self.r1.resistance, self.r3.resistance)), ) ) # The cancellation of V_L needs the second stage's gain to be the # first stage's R0 over the upper R2. require( equals( over(self.r2_second.resistance, self.r0_second.resistance), over(self.r2_upper.resistance, self.r0_first.resistance), ) )
SHORT = "Low by R_L / R2, 0.1% for 100 Ohm: the current the upper R2 takes back."
def _row(r3: float, gain: float, drive: float, note: str = "") -> Run: """One row of Table 2: R3 set, and the load current per volt measured.""" claim = ( Claim("i_per_volt", "i_per_volt", within=0.002, unit="A/V", note=SHORT) if r3 == 1000 else Claim("i_per_volt", gain, within=0.002, unit="A/V", note=f"Table 2: R3 = {r3:g} Ohm gives {gain * 1000:g} mA / Volt. {SHORT}") ) return Run( f"r3_{r3:g}_ohm", OperatingPoint(), drive={"e_in": f"DC {drive:g}"}, settings={"r3": {"resistance": r3}}, measure={ "i_per_volt": "i(vrs2_sense) / v({e_in.1})", "v_load": "v({r_load.1})", "second_out": "v({second.OUT})", }, claims=[claim], units={"v_load": "V", "second_out": "V"}, note=note, )
BENCH = Bench( page=81, title="Linear Current Source", runs=[ _row(1000, 0.001, 1), _row(100, 0.01, 1), _row(10, 0.1, 0.1, note="E_I is 100 mV here, so the load current is 10 mA " "rather than 100."), Run( "step", Transient(stop="1m", step="0.5u"), drive={"e_in": "PULSE(0 1 10u 1u 1u 2m 4m)"}, measure={ "i_settled": "find i(vrs2_sense) at=500u", "i_peak": "max i(vrs2_sense) from=0 to=500u", }, claims=[ Claim("i_settled", 0.000999, within=0.002, unit="A", note="1 mA per volt, less the 0.1% the upper R2 takes"), Claim("i_peak", 0.000999, within=0.01, unit="A", note="no overshoot worth the name: the loop through the " "upper R2 settles with C_O in place"), ], note="A 1 V step into E_I, to see the two loops settle rather than ring.", ), ],)The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
C1 100 pF -GND1 Ground -R1 100 kOhm -R2 100 kOhm -R3 100 kOhm -R4 100 kOhm -R5 100 kOhm -RS1 SettableResistor -RS2 SettableResistor -TP1 Terminal -U1 OpAmp -U2 OpAmp -Net-(C1-Pad1) C1.1 R1.1 R3.2 R5.1 U1.IN-Net-(C1-Pad2) C1.2 R1.2 R2.1 U1.OUTNet-(GND1-Pad1) GND1.1 RS2.2 U1.IN+ U2.IN+Net-(R2-Pad2) R2.2 R4.1 U2.IN-Net-(R3-Pad1) R3.1 TP1.1Net-(R4-Pad2) R4.2 RS1.1 U2.OUTNet-(R5-Pad2) R5.2 RS1.2 RS2.1Every check that ran, and every one left undecided.
2 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
1 block 12 component 34 connection 2 constraint 2 decision 1 evidence 3 interface 24 pin 24 port 103 totalsnapshot sha256:e0a0f562f869cc26fd3c552047accd79a68826607dd1efed867787298d7e34deAll of it, including the KiCad netlist, is in
examples/ti_opamp_handbook/current_output/linear_current_source/out/. Rebuild it with:
fang build examples/ti_opamp_handbook/current_output/linear_current_source/linear_current_source.py