Nov 21, 2013:
Revised: v3.0

ZL2PD DDS Audio Function Generator

This powerful low-distortion audio function generator covers from 1 Hz to above 65 kHz. It produces four different waveforms at low distortion with output levels adjustable from a few millivolts to 5 volts p-p. It's all done with a cheap ATtiny2313 microprocessor and a cheap CMOS 4015 IC.


I have been experimenting recently with digital filters. Narrow bandpass audio filters, to be precise. When it came to testing these filters, it quickly became obvious that I needed a sine wave generator with low distortion, fine tuning, and possessing an accurate and repeatable tone frequency.                                Figure 1 : The function generator uses a
                                                                                                           rotary encoder for frequency selection

My existing test equipment had some major limitations. I have a simple CMOS sine wave generator which is described elsewhere on this website, but it only covered a limited range of frequencies. Some initial tests also showed that its distortion, although fine for general testing, was just too great for these tests. I’m guessing it might be as much as high as 5%. In addition, it could not be tuned accurately and repeatably to a specific frequency, particularly within a few Hz.

My other, somewhat older, low distortion opamp-based Wien bridge audio oscillator was also starting to show its age. It had no fine tuning control, and the variable resistor used for tuning the oscillator was noisy, and badly in need of replacement.

I was clearly in need of a good, accurate, sine wave generator. As I considered the options, I decided that it was as probably going to be about as easy to build a full function generator, with sine, square and a few other waveforms, as it was to build just a basic low distortion sinewave generator. So that was the direction I headed.

The next question was whether to design my own unit, build an existing design, or purchase a function generator. I rapidly disposed of the ‘purchase’ idea. Instruments within my price range had limited features, and those that I liked were way beyond my budget. Such is life.

So, my next question: Should I build an existing design, or design one from scratch? Scanning my magazines and looking around the Web, function generator designs seemed to fall into three categories. Older designs seemed to use either a truckload of discrete components and a bucketful of opamps, or they used a specialised chip, usually the Exar XR2206 or the Maxim MAX038. The XR2206 can be hard to find and has some limitations in common with my existing test equipment, while the MAX038 can be very expensive. Newer designs mostly use Direct Digital Synthesis (DDS), either based on an Analog Design DDS chip such as the AD9835 which is driven by a microprocessor, or they simply a fast microprocessor running DDS software.

The microprocessor-based DDS software designs appealed to me, since I have a few skills in this area. Most of the designs I found used the fast Atmel AVR processor family. Yet, despite finding a number of potential designs, most, if not all, had problems.

For example, one design I came across was a really nice looking function generator kit from Altronics published in the June 2009 issue of Silicon Chip. But it was limited to a maximum frequency of 30 kHz, and it had a surprisingly high distortion of 4%. The method used to control the device was something of an issue too. Up/Down pushbuttons were used to select the required frequency. This is not very convenient to use in practice, and to make matters worse, it had some unusual tuning steps. For example, the generator is only able to tune in 500 Hz steps at frequencies above 1kHz. If I wanted an output of 1250 Hz, that Altronics/Silicon Chip design would simply not do the job.

So, ‘design my own’ was the decision, and this design is the result.

Functional Description

This generator will be used for a variety of audio test work on my workbench, as well as for the specific digital audio filter testing which kicked off this project originally. With this in mind, my aim was to produce a function generator with low distortion, ideally less than 0.5%, covering at least 1 Hz to 50 kHz, with 1 Hz resolution. I also wanted the generator to have a reasonable range of output levels to allow testing of microphone inputs, which requires low mV levels, as well as filters where several volts of output level is often necessary.

I also wanted a function generator that was reasonably compact, battery powered, and low in weight. The frequency shown on the display also had to accurately report the generator’s output frequency. Some microprocessor-based designs have relied on the micro’s internal RC clock rather than a more stable and accurate external crystal, and this can lead to some significant errors in the generated tone frequency.

The features of my function generator design are listed in Table 1:

Table 1 : Function Generator Specifications

All of the major design objectives and specifications were met or exceeded, some by a considerable margin.

Table 2 compares my design with the original Altronics product in Silicon Chip magazine:

Table 2 : DDS Function Generator Performance Comparison

My design outperforms the Silicon Chip/Altronics generator in practically every single specification. I think my generator looks nicer too!

Circuit Description

The core of this design is a microprocessor, the low cost Atmel ATtiny2313 from the Atmel AVR family. This microprocessor is capable of high speed operation, vital for digital waveform synthesis. It is clocked by a 16.934MHz external crystal. This crystal might appear to be an unusual frequency, but these are a commonly encountered crystal, and often used in CD players. Using a crystal improves the accuracy and stability of the function generator considerably over the alternative internal microprocessor oscillator seen in some other designs.

Figure 2 : DDS Function Generator Schematic
           (Right-click on the diagram to see it full size)

There are several ways to generate the desired waveform. One way is to use pulse-width modulation (PWM). In this case, the pulse width of a high speed square wave, say about 1MHz or more, is varied using software. If the pulse width changes are done at a rate equal to the desired audio frequency, and then passed through a low pass filter, then the resulting waveform will be just the audio frequency. The 1 MHz square wave will have been filtered out. The low pass filter typically cuts off around 100 kHz. The audio output can include sine, square or other waveforms.

The advantage of this approach is that it only requires the use of a single pin on the microprocessor. Many microprocessors have PWM functionality built into the chip these days. I spent some time working on this approach before discarding it as impractical for this application. Despite some extensive additional filtering, this method could not deliver the performance I was seeking. (This work was not wasted – It spun off into a miniature square wave oscillator with PWM, sweep and fixed sine wave outputs which I also built for use with SMPS design work)

The alternate method is more widely used. This uses an 8-bit port on the chip to drive a passive resistor digital to analog (D/A) converter. This simplifies the software to some extent but requires eight times the pin resources of the PWM method. Since this delivered the required performance, that was the method used in this design.

Such an approach does not require the use of 1% resistors, although these are commonly found in this type of A/D converter. The only requirement is for a precise 2:1 resistor ratio. These resistor values must come from the E24 resistor range. Since E24 parts are typically only available from suppliers in 1% high stability component ranges, the impression has been wrongly given that this level of precision is actually required in D/A R/2R ladders. Not so. If output distortion of, say, 1% is acceptable, then both 2:1 ratio and 1% accuracy requirements can be waived. However, 1% distortion was too high for this design, so 1% E24 2:1 ratio resistors are required for this design.

Rotary Encoder
The function generator’s frequency is selected using a rotary encoder. I’ve recovered quite a few of these from old car radios and defunct stereos. If you need to purchase one of these, they are available from the web, from RF Candy, for example, for around $5 each plus shipping. (See and look under Switches – General)

Figure 3 : A typical rotary encoder with integral switch

These rotary encoders produce a pair of phase shifted square waves, with the lead or lag in phase indicating the direction of rotation of the knob. These encoders almost all come with an integrated switch, activated by pressing down on the knob, and this is used in this design to select the required tuning step rate. 1, 10 or 100 Hz tuning steps can be selected anywhere within the generator’s range.

If you are unable to find one of these encoders, it is possible to make one from an old (really old!) computer mouse. A Google search for "make your own rotary encoder from a computer mouse" will produce a number of links. Similarly, if you cannot find an encoder with an integrated switch, a separate pushbutton can be used, similar to that used in this design to select the desired output waveform.

LCD Display and Interface
The LCD display is a standard 2 line by 8 character alphanumeric display. Most function generator designs use the larger 16 character per line display, and then leave most of the display blank and unused. I was aiming for a compact unit, so this smaller display was ideal, although there is next to no cost savings over the larger 16 character type.

Interfacing this LCD requires at least six pins on the micro, but I’d run out of pins since I needed eight pins for the D/A. With insufficient pins available on the ATtiny2313, I could simply have used a larger AVR microprocessor. However, I found the price for these larger chips rose quite quickly. It turned out to be much cheaper to use available components, in this case a CMOS 4015 shift register, to resolve the problem.

Special thanks must go to Roman Black for the idea behind the 2-pin drive to the LCD used in this design. You can find his website here. Actually, Roman used a 74HC595, driven by just one pin. Inspired, I tried this same approach using a cheap CD4015 CMOS shift register I had in my parts bin, plus my own AVR code, rather than Roman’s C-language code which he uses for his PIC microprocessor based designs. It all worked perfectly.

Figure 4 :
LCD interface with CD4015 shift register

Roman’s clever idea was to use two different timed pulses to latch in either a 0 or a 1 bit value into a shift register. In Figure 4, the logic level present on the D-input to the 4015 is clocked in on the rising edge of each of these timed pulses. A short 1uS pulse is too brief to change the high logic level present on C8 (2n2) and so a ‘1’ is clocked in by the rising edge of this pulse. However, the longer 15uS pulse is sufficient to discharge C8, resulting in a “0” being clocked into the 4015 by this pulse's rising edge. The following 30uS high logic level “tail” restores C8 to a high logic level.

As a result, up to eight bits can be shifted into the 4015. This can take up to 400uS, but that’s nothing in the scheme of things. The LCD display is much slower at responding to events than this.

The fifth bit sent to the 4015 is used to drive the R/S pin of the LCD display. This sets the LCD display mode. The LCD is enabled with a separate pin from the microprocessor. The software waits for the data on the 4015 outputs to settle before enabling the LCD with this connection, and this allows the new data at the 4015 outputs to be read by the LCD.

A small jumper on the board
(marked SW5 on the schematic) was used to turn on and off the backlight LED. LED backlighting consumes a lot of current from the battery. I originally thought it would be essential to have backlighting, but after a while I turned it off and I’ve not used it since. The display has adequate contract without the extra help from backlighting.

Also, while I used a 56 ohm resistor for the internal LCD display backlighting LED, and this value is shown in the circuit diagram
(It's marked R3 on the schematic), I found R3 could be increased to 150 ohms without a really noticeable change in backlight level. However, LCD displays and backlighting will vary. If you need backlighting, you may need to try different values. I do not recommend using anything lower in value than 56 ohms for R3 however.

VR2 (10k) sets the LCD display contrast. When you first turn the unit on, this will have to be adjusted so you can see the display clearly. Try setting it around mid-position initially.

Function Generator Output

The output opamp circuitry is quite standard, delivering an in-phase and an inverted pair of output signals. It does avoid the need for any large value electrolytic capacitors, and for that reason, take care. The outputs are not DC isolated(!!) Most inputs on equipment I test already have DC isolation, so I don’t usually have to worry about that, but you may need to consider this.

If necessary, add a large value capacitor in series with each output. A 22uF 16VDC electrolytic capacitor is suitable, although the output level will roll off somewhat below 10 Hz, but larger capacitors are not desirable.

The first (in-phase) generator output is controlled continuously from the variable front panel “Level” control, while the second (inverted) output has an additional three step attenuator to allow lower output levels to be reached. I use this output for testing more sensitive circuitry, such as microphone inputs on transmitters. This output also inverts the waveforms, but this is only of significance with the sawtooth wave. With this waveform selected, one output has a rising ramp and sharp fall while the second output has a falling ramp followed by a sharp rise. The useful thing about this output design was that it saved me having to code in both sawtooth waveforms into ROM.

I used a TL072 opamp here. It has better specifications than the more common LM358 and it gave much better performance at the upper end of the frequency range. Other modern opamps could be used here also.

Power Supply
The function generator is powered from a standard 9V battery. The current is relatively high for this type of battery, so an alkaline type should definitely be used. To date, this battery has proven to be satisfactory, although the notes above about the backlight should be borne in mind. Using the backlight will run down the battery fairly quickly unless a 150 ohm resistor (or higher value) is  used at R3.

To improve battery life still further, a low volt drop regulator (LP2951) has been used. This allows the battery voltage to drop to about 5.5V before I have to toss it away. You could use a 7805 or 78L05 type, but the battery life will be reduced because those regulators require a couple of volts to be dropped across them.

Software Description

All of the software has been written in assembler to deliver optimal speed. There are four main sections of code:

    Initialisation  – Sets up the instrument at power-up

    Input code – Manages the rotary encoder and switches

    Tone Generation – A tiny code segment that does the bulk of the work

    Display code – Code to drive the LCD and the 2-wire LCD interface via the 4015 shift register

The initial code starts up the generator with a brief two-line scrolling sign-on message, and then the function generator outputs a 1000Hz sinewave. The generator is initialized with a 1Hz tuning step size.

For most of the time, the microprocessor simply runs the short tone generation code segment. Interrupts are used to handle the rotary encoder and switches to allow the microprocessor to rapidly manage any user-required changes, such as a new frequency or waveform. Diodes D1 and D2 detect switch closures and initiate the required interrupt routine, while the rotary encoder activity is detected directly with a second interrupt input.

The use of interrupts does cause a brief flicker on the output waveform when a change is made to the generator’s setting. The processor is very fast, but not swift enough to manage to read the new switch settings, update the LCD and simultaneously keep the digitally generated waveform going perfectly. This is typical of all of these designs, and of little concern when the generator is actually being used.

Most of the LCD code is quite routine since most alphanumeric displays use either the Hitachi HD44780 or a compatible variation of this interface chip. My display used a compatible Chinese-made Sunplus SPLC780D chip.

These chips or their compatible equivalents have been used for a number of years, and rarely give any problems. However, my standard core LCD code allows for a fairly wide variation in display LCD timing. This is a problem with some of the less expensive displays from some suppliers, and this has given rise to problems in other designs. My software should be compatible with all of these cheap LCDs.

Some minor additions to this core LCD driver code provides the necessary support for the unusual CMOS 4015 serial to parallel interface used. The impact on the code size is minimal.

Each waveform is stored in 256 bytes of the ROM. These waveform “lookup tables” must be located at specific 256 byte address boundaries. As a result, it is a tight squeeze to get all of the code along with as many waveforms as possible. I managed to get all of the code and the four different waveforms into the small 2048 bytes (“2k”) of ROM space, with just 2 bytes to spare!


I built the unit shown on prototyping board rather than designing a specific PCB for the unit. This was a quick to build but this required more time than usual to be spent on the enclosure. The resulting circuitry didn’t fit in any standard off the shelf box.

The prototype used a 5mm thick composite wood base measuring about 100 x 85mm and a matching
1.5mm thick aluminium front panel of identical size. Four 30mm long nylon hex spacers are used to hold the front panel in place and parallel with the back wooden base panel. This front panel was covered by the laser printed panel artwork (Available in the Download area at the bottom of this webpage). In turn, this was covered in self-adhesive clear plastic to protect the panel during use.

The sides of the box were made from 3mm thick mahogany strips. These are made in 100mm x 1m long panels, and they are available from model shops. I cut out a 40mm wide strip which was then cut into the four pieces needed for the sides of the box, and glued to the base. The required slots for the various switches were cut into two of the side panels. One side was screwed into place to allow the 9V battery to be quickly and easily replaced.

All told, my prototype measures 105(W) x 90(H) x 40(D) mm, excluding knobs, or 55 mm (D) including knobs, and weighs in at 235 grams. That's just over 8 oz for those still using those odd units. A plastic case would reduce this weight by 30%, I suspect.

Using the Function Generator

Turn the function generator on. The sign-on message will scroll across the LCD. The display will then switch to the normal display of frequency and waveform settings. The initial setting on power-up is a 1000 Hz sinewave. The tuning rate is also set to 1 Hz at power-up, so rotating the Frequency knob should increase or decrease the output frequency in 1 Hz steps.

Pressing down on the Frequency control activates the integral Step switch. Pushing this briefly allows the next tuning step size to be selected, from 1 to 10 to 100 Hz, and then back to 1Hz steps again. This allows for faster or slower tuning as required.

Pressing the Wave button will select the required sine, square, triangle or sawtooth waveform.

The output level can be set using the combination of the continuously adjustable Level control and the Attenuator switch. The Level control provides continuous adjustment of the output levels up to the maximum selected with the Attenuator switch (50mV, 500mV or 5Vpp)

The two outputs can deliver levels ranging from low millivolts to more than 5V peak to peak. The maximum output depends on battery voltage. Take care when using the higher output levels. It is very easy to drive systems into overload with such large voltage swings.

Warning: The audio outputs are NOT DC isolated. While most inputs on filters and equipment which might be tested with this generator are equipped with isolating capacitors, that's not always the case. If you need outputs free from DC voltage, add a suitable capacitor in series with the 560 ohm output resistor.

The output waveform distortion is low on all of the waveforms. Sinewave distortion was measured at less than 0.2% on the prototype. Figure 5 shows the output as measured using a PC soundcard and SpectraPLUS© demo software. The figure illustrates the clean output of the function generator, although the distortion reported of 0.15% is probably better than the actual case because of the sharp filters in the sound card which cut off any input signals into the card which are above 11kHz.

Figure 5 :
Performance of a 1 kHz sinewave was measured using PC soundcard-based software and illustrates the low distortion output of the generator

Other measurements across the range of output levels and frequencies (within the measurement limitations of the PC and soundcard used) suggests that distortion performance was typically less than 0.2%, and in line with expectations.

Square wave outputs are excellent up to 35 kHz, although some distortion and leading edge ringing can be visible above this frequency depending on output amplifier wiring and construction, and the specific op-amp chosen. I found that C11 took care of this problem in the prototype, but values of C11 ranging from 2p2 to 8p2 were required depending on the op-amp used, and my layout.

Some Final Thoughts

I could have addressed a couple of other minor items if I had just had another hundred bytes of ROM available. But I faced the choice of either writing code to support four different waveforms (or five, if you count the inverted ramp waveform which results from the main and inverted op-amp output ports) or to resolve a few minor ‘might be nice to have’ features. Given the choice, I went with coding the four/five waveforms.

Also, I've now been using this generator for almost four years, and that experience has demonstrated, firstly, that it does meet all of the goals I set for it at the outset, it's easy to use, and perhaps most importantly, the 9V battery seems to last for many months of use. But still, there are a few things I'd like to address if I was doing it all over again.

So what are these minor issues?

Firstly, it’s not obvious which tuning increment (1, 10 or 100 Hz tuning steps) is currently selected just by glancing at the LCD display. Turning the tuning knob makes it obvious, but I would like to have added an underline character to the frequency display or to use the three free output pins on the 4015 to drive LEDs to indicate the tuning step size.

Secondly, the function generator runs up to 65,535 Hz before rolling over through zero to restart at 1 Hz again. That’s fine, but it would have been nice to get up to 100 kHz, say, or even 200 kHz. But that would have required a longer tuning word, a few more calculations, and, yes, more ROM. So, for now, 65535 Hz is the upper limit of this generator.

Thirdly, when you tune, there is a momentary glitch on the waveform as the DDS software updates the required output frequency. Almost every test I do is at a specific frequency, so I tune to the frequency and make my measurement. With a quick sweep of frequencies, say from 500Hz to 3 kHz, the series of glitches that occur during the tuning are barely noticeable. These momentary disturbances in the output waveform are due to the rotary encoder detection and DDS update interrupt software. This is possible to fix if I had enough code space left, but I don't. I have since done it on another design which needed that glitch-free performance, but it confirmed I needed a lot of additional code space to achieve it.

Despite these minor issues, and as I mentioned above, the objectives I had in mind when I started to design this function generator have all been met. I have designed and built a very compact, low cost, lightweight and accurate function generator with wide range and low distortion which has proved ideal for testing a wide variety of equipment. And I’ve found it a genuine delight to use on my workbench.

I hope others will build it and enjoy it just as much as me.


Source code :  (11kB)
Hex code :  (3kB)

Front panel artwork : panel.gif (29kB)

AVR Fuse Bit settings : ddsFcnGen_FuseBits.txt (2kB)

(Sorry – No PCB available)

Want to go back to the main page? Click here to return directly.