Published: 2012-01-27 | Category: [»] Electronics.
Last Modified: 2014-07-22

For the average electronic enthusiast, building sine wave oscillators is some sort of black magic. It is a mysterious skill that only a few chosen one are able to master by relying on some obscure formula from an old dusty book. But the truth is that it is actually a very easy and reliable technology once you understand the theory behind it. It is also a trusted technology that allows you to get signals of up to 100 kHz with only a 0.1% distortion in it (check the AN263 report at the end of the page).

Here I would like to give a few advices on a special case of sine wave oscillators: RC phase-shift oscillators. They are the easiest to understand and have very low distortion. On the downside, their frequency cannot be tuned once they are built.

Since I'm going to show you a few maths here, I will first describe the basic principle of RC phase-shift oscillators so we can build something in top of it. Don't panic, the maths are quite easy and are based on process control theory.

RC Oscillators 101

The circuit we will be working on is given on Figure 1. You will find it in the AN263, it is quite popular and give very good results.

Figure 1

Basically, it consists of n low-pass RC stages whose final output is fed back into the first input with a gain K. Every RC stage is isolated from the other by a follower op-amp such that the low-pass effects cumulate without disturbing each others. We will come back on this later.

This circuit is able to generate a sine oscillation if n is greater or equal to 3. If you have read the AN263, you have probably seen that the most common versions are n=3 and n=4. The more you add RC stages, the less distortion you get but the more stages you add, the more op-amp you have to buy. The peak-to-peak output voltage is quite low (±300 mV during my tests) so it is often desirable to put a final op-amp to increase the output level.

Following process control theory, each RC stage can be written as a transfer function T(ω)=(1+jRCω)-1 with j2=-1 and ω is the angular frequency (the frequency in Hz, f, is given by ω/2π). The magnitude of T(ω) will tell you how much a sine wave signal of frequency ω will be attenuated (usually expressed in dB) and the phase of T(ω) will tell you how much phase shift occurs between the input sine wave and the output sine wave. If you try out with a few numbers, you will see that T(ω) is almost equal to 1 until it reaches a critical value ω0 were it begins to drop towards zero. This is why RC stages are called low-pass filter: they attenuate frequencies above a given threshold only.

The same theory field tell us that two consecutive blocks of transfer function T1(ω) and T2(ω) have a function transfer equal to T1(ω)*T2(ω). So n consecutives RC stages have a transfer function equal to T(ω)=(1+jRCω)-n. So far, it's only a few definitions that I ask you to trust as they are presented.

But our previous circuit is more than just a big low-pass filter because we are feeding the input with minus K times the output. Put differently:

Which can only holds for a precise value ω0:

A different way of viewing the problem is to think of the general definition of feedback loop of gain K such as presented on Figure 2.

Figure 2

Such a system have an output S(p) (think of it as p=jω) equal to A(p)(E(p)-KS(p)). After re-arranging the equations, we get S(p)=H(p)E(p) with H(p)=A(p)/(1+KA(p)). So the feedback system acts itself as some kind of filter with a frequency response described by H(p). The great deal happens for a particular frequency value where 1+KA(p)=0 because H(p) has then an infinite response. Put differently, any microscopic perturbation of a well-defined frequency will be amplified to infinity and, as a consequence, the system will output an almost perfect oscillation of that particular frequency (hence the very low distortion of such oscillators!). Because A(p) is our n-stages low-pass filter, we get exactly the same condition as above.

To solve the equality, we can break the equation in the magnitude/phase-shift system:

with T(ω)=(1+jRCω)-n.

First, we get the oscillation frequency using tangents laws:

And then we get the gain value to use:

In practice, we should take a gain slightly larger than that value but not too great neither to prevent distortion in the output signal.

Using less op-amps

Using the former equation sets, we can build any sine wave oscillator but at the expense of a lot of op-amps. I will propose here a method to reduce the number of amplifiers by removing the voltage follower which isolates every RC stage. Because the RC stages are now inter-dependent, we should first focus on how to compute the overall transfer function T(ω).

To achieve this, I will use the two-port networks transfer matrices theory.

Most systems, such as our RC filter, can be written as some kind of black box which take a voltage potential Vin and output a voltage potential Vout. The current drawn by the blackbox is named iin and the current outputted by the blackbox is named iout. The situation is represented on Figure 3.

Figure 3

Without telling much about the system M we can describe it by a matrix:

which is equivalent to:

We can say that, by definition,

For a series resistor, we have:

And for a shunt capacitor, we have:

RC systems can be seen as the product of the two previous transfer matrices:

We recognize the top-left term as the inverse of the transmittance of a RC filter. More interestingly, we can add the effect of several RC stages without follower op-amps by multiplying their respective transfer matrices. As a consequence, the transfer function of n stages of RC filters is equal to:

Once we have identified T(ω) we can use the two equations to find the oscillation frequency and the required gain K.

Solving T(ω)

However, solving T(ω) is going to be a real pain in the foot because its expression can be rather complex, especially when n=4. As a consequence, I have decided to solve it numerically on the computer.

We can easily write a function that returns T(ω) as a complex number for any ω and RC components values. We also know that the phase-shift of T(ω) will be of π on the oscillation frequency so we can test successive values of ω until we find one that is close enough to π and take it as oscillation frequency. Once we have identified the oscillation frequency, we can find the gain easily.

Still, this does not tell which values or resistors and capacitances to select for a wanted oscillation frequency target. Once again, the idea is to test all the possible combination of available components. Since I have about 35 different resistors and 10 different capacitors in my lab, a quick calculation will tell that there are about 10 billion possible combinations of these components for n=4. We could try to compute all of them (this is fairly possible with enough computational power) but the overall behaviour is not that chaotic and if a solution is clearly far away from our target frequency, there are very low probability that changing only one resistor or one capacitor will do the trick.

I took this into account and wrote a Genetic Algorithm program to browse the solution space. It has the advantage of finding a solution relatively quickly but it never guarantee to have found the best solution possible. Also, the longer you run the algorithm and the better the estimates usually gets.

In Genetic Algorithms, we create a population of m entities which all have their own set of properties, chosen randomly over the whole possible sets of value. For each entity, we compute a fitness function that tells how good a given entity behaves when faced to the problem that we would like to solve. Then, we take the entities who got the best score and create a new population with children that mixes the property of pairs of parent. To make more room for development of interesting individual, we also mutate some properties randomly to prevent our population from being stuck into a less optimal solution. In the Darwinian theory of life, these mutations play an essential role so it's important to use them as well!

In the case of our oscillator problem, the population parameters are the different R and C components of a given transfer function. Each entity then has a specific oscillation frequency and gain and we try to breed an entity that gives the frequency closest to the oscillation target. After a few generations, we should get a candidate that is close enough to our target frequency.

Finally, there might be an issue with the tolerance of the components. Because we are using elements that are defined within a range and not with a precise value, we may have problems with the real circuit. To prevent this, each solution candidate error is computed from the component tolerance and the error. You can use this error for the score function too if you want to avoid solution whose frequency or gain might change too much depending on the tolerance of the components. To test the probable error associated to component tolerance, simply make copies of the original solution and alter every parameter randomly within its tolerance range. By computing the spreading of frequencies and gains, you can have an idea on the kind of error you will get.

Some results

The final circuit is given on Figure 4 and built from TL084 op-amp. As a side-note, I have found that adding a series resistor R0 in front of the RC stages often allows a better match with the target frequency.

Figure 4

The circuit was tested with a target frequency of 2600 Hz and the program returned the following components value for n=4 after 10,000 generations: R0=4.7 kΩ, R1=6.8 kΩ, R2=5.6 kΩ, R3=39 kΩ, R4=56 kΩ, C1, C3, C4=2.2 nF and C2=10 nF. The program predicted as oscillation frequency of 2597 Hz and a gain K=13.95. The Monte-Carlo check for the tolerance predicted a frequency within 5% and a required gain up to 14.45. As a consequence, I have chosen a gain of 15 for the test.

Technically speaking, the second op-amp (voltage follower) can be omitted by choosing resistors value high enough for the gain feedback but I preferred including it here.

The circuit did produce a sine wave of 2.65 kHz (tested with a cheap multimeter) with a peak-to-peak voltage of about 560 mV RMS and a dc offset of 10 mV. I was not able to check the distortion because I didn't have the tool required for that...

An interesting thing is that the circuit was checked with the SPICE3 program which computed an oscillation frequency of 2605 Hz for a gain K=24. This over-estimation of the gain is probably due to an over-damping of the SPICE3 integrator which uses a different method than the theoretical background I have highlighted here.

So, is this still black magic? ;-)

Resources

Here is the Application Report:

[∞] Download the AN263 PDF

Also, if you are playing with complex numbers this code might help you a bit:

complex.h #ifndef __COMPLEX_H__ #define __COMPLEX_H__ #include <math.h> class Complex { public: Complex(void) { this->m_fRe = 0; this->m_fIm = 0; } Complex(double re, double im) { this->m_fRe = re; this->m_fIm = im; } Complex(const Complex& rComplex) { this->operator=(rComplex); } double intensity(void) const { return this->m_fRe * this->m_fRe + this->m_fIm * this->m_fIm; } double amplitude(void) const { return sqrt(this->m_fRe * this->m_fRe + this->m_fIm * this->m_fIm); } double phase(void) const { return atan2(this->m_fIm, this->m_fRe); } double re(void) const { return this->m_fRe; } double im(void) const { return this->m_fIm; } Complex conjugate(void) const { return Complex(this->m_fRe, -this->m_fIm); } Complex& operator=(const Complex& rComplex) { this->m_fRe = rComplex.m_fRe; this->m_fIm = rComplex.m_fIm; return *this; } Complex& operator*=(const Complex& rComplex) { double fRe = this->m_fRe * rComplex.m_fRe - this->m_fIm * rComplex.m_fIm; double fIm = this->m_fRe * rComplex.m_fIm + this->m_fIm * rComplex.m_fRe; this->m_fRe = fRe; this->m_fIm = fIm; return *this; } Complex& operator/=(const Complex& rComplex) { double fIntensity = rComplex.intensity(); double fRe = this->m_fRe * rComplex.m_fRe + this->m_fIm * rComplex.m_fIm; double fIm = this->m_fIm * rComplex.m_fRe - this->m_fRe * rComplex.m_fIm; this->m_fRe = fRe / fIntensity; this->m_fIm = fIm / fIntensity; return *this; } Complex& operator+=(const Complex& rComplex) { this->m_fRe += rComplex.m_fRe; this->m_fIm += rComplex.m_fIm; return *this; } Complex& operator-=(const Complex& rComplex) { this->m_fRe -= rComplex.m_fRe; this->m_fIm -= rComplex.m_fIm; return *this; } private: double m_fRe, m_fIm; }; Complex operator+(const Complex& rA, const Complex& rB); Complex operator+(const Complex& rComplex, double fReal); Complex operator+(double fReal, const Complex& rComplex); Complex operator-(const Complex& rA, const Complex& rB); Complex operator-(const Complex& rComplex, double fReal); Complex operator-(double fReal, const Complex& rComplex); Complex operator*(const Complex& rA, const Complex& rB); Complex operator*(const Complex& rComplex, double fReal); Complex operator*(double fReal, const Complex& rComplex); Complex operator/(const Complex& rA, const Complex& rB); Complex operator/(const Complex& rComplex, double fReal); Complex operator/(double fReal, const Complex& rComplex); Complex operator~(const Complex& rA); #endif
complex.cpp #include "complex.h" Complex operator+(const Complex& rA, const Complex& rB) { Complex ret = rA; ret += rB; return ret; } Complex operator+(const Complex& rComplex, double fReal) { Complex ret = rComplex; ret += Complex(fReal, 0); return ret; } Complex operator+(double fReal, const Complex& rComplex) { Complex ret = Complex(fReal, 0); ret += rComplex; return ret; } Complex operator-(const Complex& rA, const Complex& rB) { Complex ret = rA; ret -= rB; return ret; } Complex operator-(const Complex& rComplex, double fReal) { Complex ret = rComplex; ret -= Complex(fReal, 0); return ret; } Complex operator-(double fReal, const Complex& rComplex) { Complex ret = Complex(fReal, 0); ret -= rComplex; return ret; } Complex operator*(const Complex& rA, const Complex& rB) { Complex ret = rA; ret *= rB; return ret; } Complex operator*(const Complex& rComplex, double fReal) { Complex ret = rComplex; ret *= Complex(fReal, 0); return ret; } Complex operator*(double fReal, const Complex& rComplex) { Complex ret = Complex(fReal, 0); ret *= rComplex; return ret; } Complex operator/(const Complex& rA, const Complex& rB) { Complex ret = rA; ret /= rB; return ret; } Complex operator/(const Complex& rComplex, double fReal) { Complex ret = rComplex; ret /= Complex(fReal, 0); return ret; } Complex operator/(double fReal, const Complex& rComplex) { Complex ret = Complex(fReal, 0); ret /= rComplex; return ret; } Complex operator~(const Complex& rA) { return rA.conjugate(); }
[⇈] Top of Page