Examples / TI op amp handbook / Oscillators
Wien bridge oscillator
SBOA092B page 83, Wien Bridge Oscillator: R and C in series from the output to the + input, R and C in parallel from there to ground; R2 (1.8 kΩ) and the R1 rheostat (500 Ω) from the output to the - input, R3 (220 Ω) and a GE 1869 lamp from there to ground.
f_O = 1 / (2 pi R C), 100 to 6000 HzThe 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/wien_bridge_oscillator.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 Wien network passes a third of the output in phase at 1 / (2 pi R C), so the op amp oscillates there at a gain of 3, which needs R3 + R_lamp = (R1 + R2) / 2. A cold lamp is a small resistance, so the gain starts high and the oscillation grows until the lamp warms to that point.
The program defines a Lamp part: the resistance is linear in the power the
filament has taken, filtered with a 50 ms thermal time constant, from 70 Ω
cold to 714 Ω at the 1869’s 10 V, 14 mA rating (lamp_model). R and C are two
ganged 100 kΩ rheostats and two 15.9 nF capacitors, which covers the page’s
100 Hz to 6000 Hz (tuning). R1 is set to 50 Ω, so the lamp must settle at
705 Ω (gain_trim), and a constraint checks that is no more than the lamp’s
714 Ω at rating.
What the simulation found
Section titled “What the simulation found”| Run | Frequency | Output peak | Lamp | Claimed |
|---|---|---|---|---|
as_drawn, ±13.5 V swing, 1 kHz setting | 635 Hz | clipped at ±13.5 V | 84 Ω | clipping, holds |
regulating_1khz, ±60 V swing | 1.001 kHz | 54.95 V, settled | 705 Ω | f ±0.5%, settled, lamp ±1%: hold |
regulating_6khz, ±60 V swing | 5.987 kHz | 54.95 V, settled | 705 Ω | 6.006 kHz ±0.5%, settled, lamp ±1%: hold |
With room to swing, the lamp does what the page intends: the amplitude settles, the lamp sits at the gain-3 point, and the frequency is 1 / (2 pi R C) within 0.3% at both ends of the range.
Where the handbook is off
Section titled “Where the handbook is off”The drawn values need the lamp at (R1 + R2) / 2 - R3, 680 Ω to 930 Ω across
R1’s travel. A GE 1869 is 714 Ω only at its 10 V, 14 mA rating, so the lamp
has to run near rating, and the output then carries 3 × 14 mA × 925 Ω, about
39 V rms. On the ±15 V supplies the handbook assumes elsewhere, the output
clips first: the lamp warms only to about 84 Ω, the gain stays far above 3,
and the output is a clipped wave at 635 Hz rather than a sine at 1 kHz. The
program keeps the drawn values; the regulating runs give the op amp a ±60 V
swing (swing) to show the circuit working as drawn. Most of R1’s travel is
also out of reach: past 68 Ω the lamp would need more than its rated
resistance.
Running it
Section titled “Running it”fang check examples/ti_opamp_handbook/oscillators/wien_bridge_oscillator/wien_bridge_oscillator.pypython examples/regenerate.py ti_opamp_handbook/oscillators/wien_bridge_oscillator # needs ngspiceThe whole program
Section titled “The whole program”"""The Wien bridge oscillator, SBOA092B page 83.Show 25 more lines
f_O = 1 / (2 pi R C), 100 to 6000 Hz
R and C in series from the output to the + input, R and C in parallel fromthere to ground: at 1 / (2 pi R C) the network passes a third of the outputwith no phase shift, so the op amp oscillates there if its gain is exactly3. The gain is set on the - input: R2 (1.8 kOhm) and the R1 rheostat(500 Ohm) from the output, R3 (220 Ohm) and the lamp LP1 to ground. Gain 3needs R3 + R_lamp = (R1 + R2) / 2. A cold lamp is a small resistance, so thegain starts above 3 and the oscillation grows; the lamp warms, itsresistance rises, and the amplitude stops where it balances.
The handbook has no lamp part, so the program defines one: a GE 1869 is a10 V, 14 mA bulb, 714 Ohm hot, and `lamp_model` records how the rest wasmodelled. The figure draws R and C as ganged variable parts, and `tuning`records them as two 100 kOhm rheostats and a fixed 15.9 nF, which covers thepage's 100 Hz to 6000 Hz.
The drawn values ask the lamp for (R1 + R2) / 2 - R3, 680 to 930 Ohm, whicha 714 Ohm bulb reaches only near its rating. At that point the lamp carries14 mA and the output about 39 V rms. On the +/-13.5 V swing the handbookassumes elsewhere the output clips long before the lamp gets there: thefirst run shows that. The runs that show the lamp regulating give the op ampa +/-60 V swing, which `swing` records."""
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 decimal import Decimal
from fang.lang import Hz, Ohm, Parameter, System, V, kHz, kOhm, mA, ms, nF, requirefrom fang.parts import Capacitor, Resistor, TwoPinfrom fang.rationale import Chooses, Citesfrom fang.simulation import Transient
from handbook import ( Bench, Claim, Ground, OpAmp, Potentiometer, Run, Terminal, at_most, corner, equals, minus, over, product, ratio, total, within,)
class Lamp(TwoPin): """An incandescent lamp as a resistance that rises with its own heating.Show 8 more lines
The filament's temperature rise is a first-order lag on the power it takes: a thermal node whose voltage is that filtered power, in watts, with `time_constant` as its RC. The resistance is linear in it, from `cold_resistance` with no power to rated voltage over rated current at rated power. The bench writes it as two behavioural sources: the filament's current, and the power that heats it. """
designator_prefix = "LP" rated_voltage = Parameter("V") rated_current = Parameter("A") cold_resistance = Parameter("Ohm") time_constant = Parameter("s")
def spice(self, ref, node, value): volts, amps = value("rated_voltage"), value("rated_current") cold, tau = value("cold_resistance"), value("time_constant") slope = (volts / amps - cold) / (volts * amps) a, b, th = node("1"), node("2"), f"th_{ref}" resistance = f"({cold:.6g} + {slope:.6g} * V({th}))" return ( [ f"B{ref} {a} {b} I = V({a},{b}) / {resistance}", f"B{ref}_HEAT 0 {th} I = V({a},{b}) * V({a},{b}) / {resistance}", f"R{ref}_TH {th} 0 1", f"C{ref}_TH {th} 0 {tau:.6g}", ], {}, )
def describe(self, value) -> str: volts, amps = value("rated_voltage"), value("rated_current") return ( f"lamp: {value('cold_resistance'):.6g} Ohm cold, {volts / amps:.4g} Ohm at " f"{volts:.6g} V and {amps * 1000:.6g} mA, heating with a " f"{value('time_constant') * 1000:.6g} ms time constant" )
class WienBridgeOscillator(System): """A Wien network on the + input, and a lamp-stabilised gain of 3 on the - input."""
figure = Cites( "f_O = 1 / (2 pi R C), 100 to 6000 Hz. High purity sine wave generation.", document="SBOA092B, Handbook of Operational Amplifier Applications", locator="page 83, Wien Bridge Oscillator", )
lamp_model = Chooses( "How does the GE 1869 behave?", selected=( "10 V and 14 mA rated, so 714 Ohm hot; 70 Ohm cold, a tenth of hot; " "resistance linear in the power it has taken, filtered with a 50 ms " "thermal time constant" ), alternatives=[ { "option": "a power law in the filament's power", "reason": "closer to tungsten over the whole range, but near the " "rating, where this circuit runs the lamp, both give the same " "resistance, and a power law has no slope at zero to start from", }, { "option": "a thermal time constant of a few ms", "reason": "the resistance would then follow each cycle, and at " "100 Hz that is distortion, where the page claims high purity", }, ], rationale=( "the handbook names the lamp and no model; its rating is 10 V at 14 mA", "tungsten runs about ten to one hot to cold", "what the oscillator uses is the lamp's resistance at its operating " "point, which is pinned by the gain condition, not by the model", ), )
tuning = Chooses( "What are R and C?", selected=( "two 100 kOhm rheostats and two 15.9 nF capacitors; 0.1 of the travel " "(10 kOhm) is 1 kHz, all of it 100 Hz, and 1/60 of it 6 kHz" ), alternatives=[ { "option": "fixed resistors", "reason": "the figure draws R and C as variable, and a range of " "100 to 6000 Hz is a tuning range", }, ], rationale=( "one rheostat of 60:1 with a fixed capacitor covers exactly the " "range the page prints", ), )
gain_trim = Chooses( "Where is the R1 rheostat set?", selected="50 Ohm in circuit, so the lamp must settle at 705 Ohm", alternatives=[ { "option": "half its travel, 250 Ohm", "reason": "the lamp would have to reach 805 Ohm, past its 714 " "Ohm rating, and burn out before the gain came down to 3", }, ], rationale=( "gain 3 needs R3 + R_lamp = (R1 + R2) / 2; with R2 at 1.8 kOhm and " "R3 at 220 Ohm the lamp needs 680 Ohm even with R1 at zero", "50 Ohm is as much of R1 as a 714 Ohm lamp leaves room for", ), )
swing = Chooses( "What output swing does the op amp have?", selected=( "+/-13.5 V in the first run, as elsewhere in the handbook; +/-60 V in " "the runs that show the lamp regulating" ), alternatives=[ { "option": "change the resistors so the lamp regulates on +/-15 V supplies", "reason": "that is a different circuit from the one drawn", }, { "option": "a lamp model that is hot at a lower current", "reason": "a GE 1869 is 714 Ohm only at its 14 mA rating; a model " "that got there sooner would be a different lamp", }, ], rationale=( "with a 705 Ohm lamp and 220 Ohm beside it, the - input carries " "14 mA and the output 3 x 14 mA x 925 Ohm, 39 V rms", "the first run shows what the drawn circuit does on the usual swing, " "and the others show what it was drawn to do", ), )
f_o = Parameter("Hz", default=1 * kHz, description="1 / (2 pi R C) at the chosen setting") f_low = Parameter("Hz", default=100 * Hz, description="1 / (2 pi R C) with all of R") lamp_resistance = Parameter( "Ohm", default=705 * Ohm, description="where the lamp must settle: (R1 + R2) / 2 - R3" )
e_out = Terminal() amp = OpAmp() ground = Ground()
r_series = Potentiometer(resistance=100 * kOhm, setting=Decimal("0.1") * ratio) c_series = Capacitor(capacitance=Decimal("15.9") * nF) r_shunt = Potentiometer(resistance=100 * kOhm, setting=Decimal("0.1") * ratio) c_shunt = Capacitor(capacitance=Decimal("15.9") * nF)
r1 = Potentiometer(resistance=500 * Ohm, setting=Decimal("0.9") * ratio) r2 = Resistor(resistance=Decimal("1.8") * kOhm) r3 = Resistor(resistance=220 * Ohm) lamp = Lamp( rated_voltage=10 * V, rated_current=14 * mA, cold_resistance=70 * Ohm, time_constant=50 * ms, )
def architecture(self): # The Wien network: R and C in series from the output, R || C to ground. self.amp.output.signal >> self.r_series.end_a self.r_series.wiper >> self.r_series.end_b self.r_series.end_b >> self.c_series.p1 self.c_series.p2 >> self.amp.non_inverting.signal self.amp.non_inverting.signal >> self.r_shunt.end_a self.r_shunt.wiper >> self.r_shunt.end_b self.r_shunt.end_b >> self.ground.node self.amp.non_inverting.signal >> self.c_shunt.p1 self.c_shunt.p2 >> self.ground.node
# The gain: R1 (wiper tied to the output end) and R2 down to the - input. self.amp.output.signal >> self.r1.end_a self.r1.wiper >> self.r1.end_a self.r1.end_b >> self.r2.p1 self.r2.p2 >> self.amp.inverting.signal
# R3 and the lamp from the - input to ground. self.amp.inverting.signal >> self.r3.p1 self.r3.p2 >> self.lamp.p1 self.lamp.p2 >> self.ground.node
self.amp.output.signal >> self.e_out.probe
def constraints(self): r = product(self.r_series.resistance, self.r_series.setting) require(within(self.f_o, corner(r, self.c_series.capacitance), 0.002)) require(within(self.f_low, corner(self.r_series.resistance, self.c_series.capacitance), 0.002)) # The two arms are ganged: the same R and the same C. require(equals(self.r_shunt.resistance, self.r_series.resistance)) require(equals(self.r_shunt.setting, self.r_series.setting)) require(equals(self.c_shunt.capacitance, self.c_series.capacitance))
# Gain 3: R3 + R_lamp = (R1 + R2) / 2, with R1 in circuit from the wiper down. r1 = product(self.r1.resistance, minus(1 * ratio, self.r1.setting)) require( equals( self.lamp_resistance, minus(over(total(r1, self.r2.resistance), 2 * ratio), self.r3.resistance), ) ) # And the lamp can get there: no further than its resistance at rating. require( at_most( self.lamp_resistance, over(self.lamp.rated_voltage, self.lamp.rated_current), ) )
#: 60 V either side: what the drawn circuit needs for its lamp to regulate.WIDE = {"output_high": 60, "output_low": -60}
#: The lamp's resistance: the voltage across it over the current through R3.#: The lamp barely changes within a cycle, so the voltages at the - input and#: across the lamp are in phase, and the difference of their rms values is the#: rms across R3.LAMP = "220 * lamp_rms / (summing_rms - lamp_rms)"
def _regulating(name: str, setting: float, stop: float, step: str, f: float | str, note: str) -> Run: """A run with the wide swing, at one tuning, measured after the lamp settles.""" late, later = stop * 0.75, stop * 0.95 return Run( name, Transient(stop=f"{stop:g}", step=step), settings={"amp": WIDE, "r_series": {"setting": setting}, "r_shunt": {"setting": setting}}, cards=[".ic v({c_series.1})=0.1"], measure={ "t_first": f"when v({{e_out.1}})=0 rise=1 td={later:g}", "t_last": f"when v({{e_out.1}})=0 rise=21 td={later:g}", "f_o": "20 / (t_last - t_first)", "peak_late": f"max v({{e_out.1}}) from={late:g} to={late + stop * 0.05:g}", "peak_end": f"max v({{e_out.1}}) from={later:g} to={stop:g}", "settled": "peak_end / peak_late", "lamp_rms": f"rms v({{lamp.1}}) from={later:g} to={stop:g}", "summing_rms": f"rms v({{r3.1}}) from={later:g} to={stop:g}", "lamp_resistance": LAMP, }, claims=[ Claim("f_o", f, within=0.005, unit="Hz", note="0.5% for the op amp's own phase shift, which grows with " "frequency, and for the lamp's small ripple"), Claim("settled", 1, within=0.002, note="the peak a fifth of the run apart: the amplitude has stopped moving"), Claim("lamp_resistance", "lamp_resistance", within=0.01, unit="Ohm", note="the gain-3 point, read back from the lamp's own voltage and current"), ], units={"t_first": "s", "t_last": "s", "peak_late": "V", "peak_end": "V", "lamp_rms": "V", "summing_rms": "V"}, note=note, )
BENCH = Bench( page=83, title="Wien Bridge Oscillator", runs=[ Run( "as_drawn", Transient(stop="0.6", step="5u"), cards=[".ic v({c_series.1})=0.1"], measure={ "e_high": "max v({e_out.1}) from=0.5 to=0.6", "e_low": "min v({e_out.1}) from=0.5 to=0.6", "t_first": "when v({e_out.1})=0 rise=1 td=0.5", "t_last": "when v({e_out.1})=0 rise=21 td=0.5", "f_o": "20 / (t_last - t_first)", "lamp_rms": "rms v({lamp.1}) from=0.5 to=0.6", "summing_rms": "rms v({r3.1}) from=0.5 to=0.6", "lamp_resistance": LAMP, }, claims=[ Claim("e_high", 13.5, within=0.01, unit="V", note="the output clips at the swing: the lamp has not warmed " "enough to bring the gain down to 3"), Claim("e_low", -13.5, within=0.01, unit="V"), ], units={"t_first": "s", "t_last": "s", "f_o": "Hz", "lamp_rms": "V", "summing_rms": "V", "lamp_resistance": "Ohm"}, note="The drawn circuit on the +/-13.5 V swing, tuned for 1 kHz. The " "lamp would have to reach 705 Ohm, and a clipped output drives it " "nowhere near: the gain stays far above 3, the output is a clipped " "wave, and its frequency is well below 1 / (2 pi R C).", ), _regulating( "regulating_1khz", 0.1, 1.2, "5u", "f_o", "The same circuit with a +/-60 V swing, R at 10 kOhm. The oscillation " "grows from a 0.1 V kick until the lamp warms to the gain-3 point.", ), _regulating( "regulating_6khz", 1 / 60, 1.0, "1u", 1 / (6.283185307179586 * 100e3 / 60 * 15.9e-9), "R turned down to 1/60 of its travel, 1.667 kOhm: the top of the " "page's range, 1 / (2 pi R C) = 6.006 kHz.", ), ],)The files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
C1 15.9 nF -C2 15.9 nF -GND1 Ground -LP1 Lamp -R1 1.8 kOhm -R2 220 Ohm -RV1 Potentiometer -RV2 Potentiometer -RV3 Potentiometer -TP1 Terminal -U1 OpAmp -Net-(C1-Pad1) C1.1 RV2.2 RV2.3Net-(C1-Pad2) C1.2 C2.1 RV3.1 U1.IN+Net-(C2-Pad2) C2.2 GND1.1 LP1.2 RV3.2 RV3.3Net-(LP1-Pad1) LP1.1 R2.2Net-(R1-Pad1) R1.1 RV1.3Net-(R1-Pad2) R1.2 R2.1 U1.IN-Net-(RV1-Pad1) RV1.1 RV1.2 RV2.1 TP1.1 U1.OUTEvery check that ran, and every one left undecided.
7 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
1 block 11 component 34 connection 7 constraint 4 decision 1 evidence 3 interface 24 pin 24 port 109 totalsnapshot sha256:8494709fc01d2bd30ae38b0ffb59c84ebbfdf6c5cd1a775e276231bde6247722All of it, including the KiCad netlist, is in
examples/ti_opamp_handbook/oscillators/wien_bridge_oscillator/out/. Rebuild it with:
fang build examples/ti_opamp_handbook/oscillators/wien_bridge_oscillator/wien_bridge_oscillator.py