Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / Current output

Current injector

SBOA092B page 80, Current Injector: a Howland current source. EI through R1 into the - input, R0 from the output back to it; R3 from the output to the + input, R2 from there to ground, and the load RL from there to ground. All four resistors are 1 kΩ.

I = -E_I / R_L = -E_I mA, R1 / R2 = R0 / R3
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/current_injector.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.

With the + input at V_P, the current delivered into the load is

I = -R0 E_I / (R1 R3) + V_P (R0 / (R1 R3) - 1 / R2)

and the page’s ratio condition makes the bracket zero, so I = -R0 E_I / (R1 R3) = -E_I / R2, whatever R_L is. The program holds the ratio condition and the transconductance as constraints, and makes the load a Load a run can change (load records the values).

out/simulation.txt, E_I = 1 V:

R_LI / E_I+ inputOutput
100 Ω-1 mA/V, holds-0.1 V-1.2 V
1 kΩ-1 mA/V, holds-1 V-3 V
4.7 kΩ-1 mA/V, holds-4.7 V-10.4 V

The + input swings with the load, which is the common-mode limit the page warns about: at 10 kΩ the output would need -21 V.

The page prints I = -E_I / R_L. A current that depended on R_L would not be a current source. For the drawn circuit it is -R0 E_I / (R1 R3), or -E_I / R2; with 1 kΩ throughout that is the -E_I mA the page prints, so the number is right and the formula is not.

Terminal window
fang check examples/ti_opamp_handbook/current_output/current_injector/current_injector.py
python examples/regenerate.py ti_opamp_handbook/current_output/current_injector # needs ngspice
examples/ti_opamp_handbook/current_output/current_injector/current_injector.py
"""The current injector, SBOA092B page 80: a Howland current source.
Show 25 more lines
I = -E_I / R_L = -E_I mA, R1 / R2 = R0 / R3
E_I drives R1 into the - input and R0 closes the loop from the output; the
output also drives R3 onto the + input, where R2 returns to ground and the
load R_L takes the rest to ground. With the + input at V_P, the - input's
node gives the output as V_P - R0 (E_I - V_P) / R1, and the current R3
delivers into the + node, less what R2 takes, is
I = -R0 E_I / (R1 R3) + V_P (R0 / (R1 R3) - 1 / R2)
The ratio condition R1 / R2 = R0 / R3 makes the bracket zero, so
I = -R0 E_I / (R1 R3) = -E_I / R2
which does not depend on V_P, and so does not depend on R_L: that is what
makes it a current source. The page prints -E_I / R_L, which would make the
current depend on the one thing a current source is built not to depend on.
With every resistor 1 kOhm, R2, R3 and the printed R_L-free value all give
-1 mA per volt, so the number the page prints is right and its formula is
not.
The load is the thing driven, and the program makes it a `Load` a run can
change, then moves it across a decade and a half to show the current stays.
"""
import sys
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, UnitLiteral, kOhm, require
from fang.parts import Resistor, TwoPin
from fang.rationale import Chooses, Cites
from fang.simulation import OperatingPoint
from handbook import (
Bench,
Claim,
Ground,
OpAmp,
Run,
Terminal,
equals,
negative,
over,
product,
)
#: 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 CurrentInjector(System):
"""A difference amplifier whose + input node is the output, into a grounded load."""
figure = Cites(
"I = -E_I / R_L = -E_I mA; R1 / R2 = R0 / R3. Single terminal current "
"available to ground. Observe common mode voltage limit.",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 80, Current Injector",
)
load = Chooses(
"What is R_L?",
selected="1 kOhm by default, and 100 Ohm and 4.7 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",
},
{
"option": "10 kOhm",
"reason": "at 1 mA the + input would sit at -10 V and the output "
"at -21 V, past the swing: the common-mode limit the page warns of",
},
],
rationale=(
"the figure draws R_L between two terminals with no value",
"at 4.7 kOhm the + input is at -4.7 V and the output at -10.4 V, "
"inside the swing",
),
)
i_per_volt = Parameter(
"mA/V", default=-1 * mA_per_V, description="I / E_I: -R0 / (R1 R3), or -1 / R2"
)
e_in = Terminal()
r1 = Resistor(resistance=1 * kOhm)
r0 = Resistor(resistance=1 * kOhm)
r2 = Resistor(resistance=1 * kOhm)
r3 = 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.r0.p1
self.r0.p2 >> self.amp.output.signal
self.amp.output.signal >> self.r3.p1
self.r3.p2 >> self.amp.non_inverting.signal
self.amp.non_inverting.signal >> self.r2.p1
self.r2.p2 >> self.ground.node
self.amp.non_inverting.signal >> self.r_load.p1
self.r_load.p2 >> self.ground.node
def constraints(self):
# The page's own condition, which is what makes the current independent
# of the load.
require(
equals(
over(self.r1.resistance, self.r2.resistance),
over(self.r0.resistance, self.r3.resistance),
)
)
require(
equals(
self.i_per_volt,
negative(
over(self.r0.resistance, product(self.r1.resistance, self.r3.resistance))
),
)
)
ERRATUM = (
"The page prints I = -E_I / R_L. For the drawn circuit with R1/R2 = R0/R3 "
"it is -R0 E_I / (R1 R3), or -E_I / R2, whatever R_L is; with 1 kOhm "
"throughout both are the -E_I mA the page prints."
)
def _load(resistance: float, note: str = "") -> 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})",
"common_mode": "v({amp.IN+})",
"e_out": "v({amp.OUT})",
},
claims=[Claim("i_per_volt", "i_per_volt", within=0.001, unit="A/V", note=note)],
units={"common_mode": "V", "e_out": "V"},
)
BENCH = Bench(
page=80,
title="Current Injector",
runs=[_load(100), _load(1000, note=ERRATUM), _load(4700)],
)

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

out/netlist.txt
GND1 Ground -
R1 1 kOhm -
R2 1 kOhm -
R3 1 kOhm -
R4 1 kOhm -
RL1 Load -
TP1 Terminal -
U1 OpAmp -
Net-(GND1-Pad1) GND1.1 R3.2 RL1.2
Net-(R1-Pad1) R1.1 R2.2 U1.IN-
Net-(R1-Pad2) R1.2 R4.1 U1.OUT
Net-(R2-Pad1) R2.1 TP1.1
Net-(R3-Pad1) R3.1 R4.2 RL1.1 U1.IN+

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
8 component
20 connection
2 constraint
1 decision
1 evidence
3 interface
15 pin
15 port
66 total
snapshot sha256:8e21a7b3f7b8fb983986c551771a6015b57eca84a9fbfc6473102d4179384d42

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

Terminal window
fang build examples/ti_opamp_handbook/current_output/current_injector/current_injector.py