Skip to content
copperhead.sh
Get started

Examples / TI op amp handbook / Differentiators

Differentiator

SBOA092B page 61, Differentiators (also Figure 43 on page 38): EI through CI into the summing point, RO from the output back to it, and the non-inverting input on ground.

E_O = -R_O C_I dE_I/dt
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/differentiator.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 figure names C_I and R_O and gives them no values, so the program chooses them and records the choice as a decision (values): 0.1 µF and 100 kΩ, the same pair as the “with stop” figure below it, for a 10 ms time constant. That puts the unity-gain point, f_unity = 1/(2π R_O C_I), at 15.9 Hz, where Figure 43 draws X_C = R_O.

The page warns that the simple circuit is not usable. Figure 43 shows why: the gain rises at 20 dB per decade and meets the op amp’s falling open-loop gain, and at that crossing the loop has almost no phase margin. The program claims where that happens, which follows from equating the two lines, 2π f R_O C_I = GBW / f:

require(within(product(self.f_peak, self.f_peak),
product(self.amp.gain_bandwidth, self.f_unity), 0.0001))

so f_peak = √(10 MHz × 15.9 Hz) = 12.6 kHz. Change the op amp’s gain-bandwidth and the check fails until f_peak moves with it.

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

RunMeasuredClaimed
derivative, unity-gain frequency15.92 Hz15.92 Hz (f_unity), holds
derivative, gain at 100 Hz6.2842π × 100 Hz × 10 ms = 6.283, holds
derivative, phase at 100 Hz-1.571 rad-π/2, holds
peak, where the output turns real12.62 kHz12.62 kHz (f_peak), holds
peak, height3.86 × 10^5 (111.7 dB)not a claim

Below the peak the circuit is a derivative, gain and phase both. At 12.6 kHz the ideal gain would be 793; the circuit gives about 486 times that. The height is set by how little damping the op amp model’s one pole leaves, which is the model’s and not the handbook’s, so it is reported, not claimed. The point stands either way: any noise near 12.6 kHz comes out enormous, which is what the page means by “susceptible to high frequency noise”.

Terminal window
fang check examples/ti_opamp_handbook/differentiators/differentiator/differentiator.py
python examples/regenerate.py ti_opamp_handbook/differentiators/differentiator # needs ngspice
examples/ti_opamp_handbook/differentiators/differentiator/differentiator.py
"""The differentiator, SBOA092B page 61 (and Figure 43 on page 38).
Show 20 more lines
E_O = -R_O C_I dE_I/dt
C_I carries E_I into the summing point and R_O runs from the output back to
it. The current through C_I is C_I dE_I/dt, and R_O turns it into a voltage,
so the gain rises with frequency, 2 pi f R_O C_I, passing unity where C_I's
reactance equals R_O.
The figure names the two parts and gives them no values, so `values` records
the pair chosen here: 0.1 uF and 100 kOhm, the C_I and R_O of the "with stop"
figure below it on the same page, which puts the unity-gain point at 15.9 Hz,
where Figure 43 draws X_C = R_O.
The page says the circuit is not usable as drawn, and Figure 43 shows why: the
rising gain meets the op amp's falling open-loop gain, and there the loop has
nothing left to damp it. The two lines cross where 2 pi f R_O C_I equals
GBW / f, at f = sqrt(GBW f_1) with f_1 = 1/(2 pi R_O C_I). With the bench's
10 MHz op amp that is 12.6 kHz, and the program claims the peak there. How
tall the peak is depends on the op amp's own pole and is reported as measured.
"""
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 decimal import Decimal
from fang.lang import Hz, Parameter, System, kHz, kOhm, ms, require, uF
from fang.parts import Capacitor, Resistor
from fang.rationale import Chooses, Cites
from fang.simulation import ACSweep
from handbook import (
Bench,
Claim,
Ground,
OpAmp,
Run,
Terminal,
corner,
equals,
product,
within,
)
class Differentiator(System):
"""E_I through C_I into the summing point, R_O back from the output."""
figure = Cites(
"E_O = -R_O C_I dE_I/dt. The ideal differentiator circuit is not generally "
"usable in its simple form",
document="SBOA092B, Handbook of Operational Amplifier Applications",
locator="page 61, Differentiators",
)
values = Chooses(
"What are C_I and R_O?",
selected="0.1 uF and 100 kOhm, a 10 ms time constant",
alternatives=[
{
"option": "leave them unknown",
"reason": "a derivative with no time constant is not a claim anything can check",
},
{
"option": "a shorter time constant, such as 0.1 uF and 10 kOhm",
"reason": "it moves the peak up to 40 kHz and does not change what the figure shows",
},
],
rationale=(
"the figure names C_I and R_O and gives no values",
"the same page's differentiator with stop uses 0.1 uF and 100 kOhm",
"Figure 43 draws X_C = R_O near 16 Hz, which 1/(2 pi 100k 0.1u) is",
),
)
time_constant = Parameter("s", default=10 * ms, description="R_O C_I")
f_unity = Parameter(
"Hz", default=Decimal("15.9155") * Hz, description="where 2 pi f R_O C_I = 1"
)
f_peak = Parameter(
"Hz",
default=Decimal("12.6157") * kHz,
description="sqrt(GBW f_unity): where the rising gain meets the open-loop roll-off",
)
e_in = Terminal()
common = Terminal()
e_out = Terminal()
c_in = Capacitor(capacitance=Decimal("0.1") * uF)
r_out = Resistor(resistance=100 * kOhm)
amp = OpAmp()
ground = Ground()
def architecture(self):
self.e_in.probe >> self.c_in.p1
self.c_in.p2 >> self.amp.inverting.signal
self.amp.inverting.signal >> self.r_out.p1
self.r_out.p2 >> self.amp.output.signal
self.amp.output.signal >> self.e_out.probe
self.amp.non_inverting.signal >> self.ground.node
self.common.probe >> self.ground.node
def constraints(self):
r_o, c_i = self.r_out.resistance, self.c_in.capacitance
require(equals(self.time_constant, product(r_o, c_i)))
require(within(self.f_unity, corner(r_o, c_i), 0.00001))
# Written squared, since the expression tree has no square root:
# f_peak^2 = GBW f_unity.
require(
within(
product(self.f_peak, self.f_peak),
product(self.amp.gain_bandwidth, self.f_unity),
0.0001,
)
)
BENCH = Bench(
page=61,
title="Differentiators",
runs=[
Run(
"derivative",
ACSweep(points=100, start="1", stop="1k"),
drive={"e_in": "DC 0 AC 1"},
measure={
"f_unity": "when vm({e_out.1})=1",
"gain_100": "find vm({e_out.1}) at=100",
"phase_100": "find vp({e_out.1}) at=100",
},
claims=[
Claim("f_unity", "f_unity", within=0.001, unit="Hz"),
Claim("gain_100", 6.2832, within=0.001,
note="2 pi x 100 Hz x 10 ms: the gain of a derivative rises with f"),
Claim("phase_100", -1.5708, within=0.001,
note="-pi/2 radians: -j 2 pi f R_O C_I, a derivative inverted"),
],
note=(
"Well below the peak the circuit is what the page says: a gain of "
"2 pi f R_O C_I, 90 degrees behind the inversion."
),
),
Run(
"peak",
ACSweep(variation="lin", points=2401, start="12k", stop="13.2k"),
drive={"e_in": "DC 0 AC 1"},
measure={
"f_peak": "when vi({e_out.1})=0",
"gain_peak": "max vm({e_out.1})",
"gain_peak_db": "max vdb({e_out.1})",
},
claims=[
Claim("f_peak", "f_peak", within=0.001, unit="Hz",
note=(
"Where the output's imaginary part changes sign, which for "
"a resonance this narrow is the top of the peak."
)),
],
units={"gain_peak_db": "dB"},
note=(
"The handbook's Figure 43 point. The rising gain meets the 10 MHz "
"op amp's roll-off at sqrt(GBW f_unity) and the loop is left with "
"almost no damping, so a gain the page calls 2 pi f R_O C_I (793 "
"there) peaks hundreds of times higher. The height is the op amp "
"model's, and is reported, not claimed."
),
),
],
)

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

out/netlist.txt
C1 0.1 uF -
GND1 Ground -
R1 100 kOhm -
TP1 Terminal -
TP2 Terminal -
TP3 Terminal -
U1 OpAmp -
Net-(C1-Pad1) C1.1 TP2.1
Net-(C1-Pad2) C1.2 R1.1 U1.IN-
Net-(GND1-Pad1) GND1.1 TP1.1 U1.IN+
Net-(R1-Pad2) R1.2 TP3.1 U1.OUT

Every check that ran, and every one left undecided.

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

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
7 component
14 connection
3 constraint
1 decision
1 evidence
3 interface
11 pin
11 port
52 total
snapshot sha256:ab6b1416c3168398c7c25765b6c8e7be918b89e4c7a38316b7da9eb2ee8ba8a7

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

Terminal window
fang build examples/ti_opamp_handbook/differentiators/differentiator/differentiator.py