Introduction: Arbitrary Waveform Generator, for ~20$

About: I publish my failures and my successes, as my teachers have done before me. I am a member of Foulab, an independent, nonprofit research and engineering group in Montreal. Check out our webpage at www.foul…

An arbitrary waveform generator (AWG) is a useful but often expensive piece of test equipment (ebay it for laughs). Use it to determine component frequency response, generate carrier signals, as an LCR meter if you have a scope, tune resonant circuits, play sounds, or just draw cool graphics on your scope. It has many other uses as well, both benign and sinister, use your imagination (at your own risk)!

This project will describe how to make an AWG that can produce decent sine waves up to about 2Mhz, and of course all kinds of other waveforms, for around 20$ (assuming you own an stk500 or equivalent programmer).

This project assumes the builder is familiar with assembly language, atmel microcontrollers and their programmers, oscilloscope use, and basic electronics. All novel ideas and schematics are released under the GPL, all non-schematic images are released under a Creative Commons license.

Parts:
2x 10 pF capacitors
1x crystal, preferably 16Mhz, I used 14Mhz
1x 5v voltage regulator
2x 9v battery
7x 50kohm resistors, 1%
10x 100kohm resistors, 1%
2x 4.7kohm resistors
1x 100kohm potentiometer
1x 10kohm potentiometer
1x OPA2132 op-amp, or any op-amp you're familiar with
2x 220uF electrolytic capacitors, rated 18v or higher

Finally, you will need the datasheets for the atmega16-16pu, and your opamp of choice. In the amplifier circuit, I labeled the pins by function and not by number, the datasheet will show you which pins are which (I used the same naming scheme as the datasheet).

The original html version of this project is available at http://legionlabs.nullnode.com/

The photo demonstrates a 1Mhz sine wave generated by the device.

Step 1: First Circuit.

This circuit contains the microcontroller and the digital to analog converter (called an R/2R network) which generates the waveform. The waveform generated is between 0 and +0.2v, the 100kohm resistor and potentiometer act as a bias to make it between -0.1v and +0.1v.

VERY IMPORTANT: Unless you want to include switches that change the waveform type/frequency... I didn't because it involves a performance tradeoff... you will be reprogramming this microcontroller frequently. Either be fancy and include ISP, or do what I did: solder an IC socket to the circuit board, and also lodge the microcontroller into another IC socket... the electrical contact between the two IC sockets is just fine, and an IC removal tool lets you pull it out with minimum force.

Alternatively, spend an extra few dollars and get a ZIF socket. If I were to redo this project, this is what I would do.

When this stage is complete, you have a functioning waveform generator... which you should proceed to test with your scope (test the bias!). A later step will include a link to a useful site that has assembly code compatible with this microcontroller that will generate various waveforms.

Next, we will ad an amplifier stage to increase the signal voltage to useful levels.

Step 2: Amplifier and Amp Power Supply

Now, we build an amplifier, and a power supply for it. Some of you may recognize this circuit as a bastardized cmoy amplifier.

The gain is controlled by a 10kohm potentiometer, which was hooked up in a rather foolish way but still works fine. Having three leads, the resistance between the ones on the ends is always 10k, and the resistance between one end and the middle changes depending on the dial position... in this way we use the potentiometer as two resistors instead of just one. If you look at the datasheet for the OPA 2132, look at the formula to determine gain... you will see why this is suboptimal, but still works. You may fix this problem by using two 10k potentiometers for the gain-determining resistors shown on the datasheet.

The power supply gives our amp +9, -9 and 0 volt rails. Without the two 9v batteries, the amp behaves strangely, and may "cut off" the higher and lower parts of waveforms, which would be sad. With the dual rail power supply, waveforms can be amplified to +/- 1.5 volts, YMMV. Additionally, it helps compensate for the differential drain on the batteries... one has to run a microcontroller and an amp, and the other runs the bias and amp.

With these circuits done, you're ready to make some waves.

EDIT: I originally referred to the power supply circuit as a virtual ground circuit (the photo still does). This is an artifact of an older design for the AWG. I'm not entirely sure whether it is a true virtual ground or not, so I've renamed it. I thank the readers for their informative comments on the matter.

Step 3: Sine Wave, 1.790Mhz

This is a sine wave generated at 1.790Mhz. Why this frequency? I used a 14.3mhz crystal... and the sine wave is generated by producing a sequence of 8 values repeatedly (ie: sin(pi/4,pi/2,3pi/4...). Our program conceptually looks like this:

Reset:
r1=255*sin(0)
r2=255*sin(pi/4)
r3=255*sin(pi/2)
r4=255*sin(3pi/4)
r5=255*sin(pi)
r6=255*sin(5pi/4)
r7=255*sin(3pi/2)
Loop:
output portN,r1
output portN,r2
output portN,r3
output portN,r4
output portN,r5
output portN,r6
output portN,r7
rjmp loop

The little irregular "dip" in the waveform is caused by the rjmp statement which takes 2 clock cycles to process. To get around this, you copy/paste the sequence in the loop function many times back to back, producing many periods of the waveform for each loop. This photo is of a sequence of 10 periods per loop, the atmega16-16pu has enough memory for ten times that easily.

To make other frequencies, you need to be creative:
- change the resolution (pi/n), as long as you keep in mind higher values of n require more registers.
- use the nop statement (it does nothing and takes a clock cycle to do it)
- use timers
- use a sine table in EEPROM

- weird tricks: notice how the rjmp artifact brings the voltage below the zero value of the waveform... this is because it represents the value 0 existing for 3 clock cycles, and whatever test leads you use will have a certain capacitance and inductance which resists changes in current and voltage. You could make your program produce an asymmetrical waveform by replacing r1 with a nonzero positive integer so that the voltage decays exactly to the "zero point" of the rest of the waveform over 2 clock cycles. If you can do this, then my hat is off to you.

Step 4: Case

It's very useful to put this project in a case. At the very least it provides a grip when you pull out the microcontroller to program it.

Besides, I had this nice case... which was unfortunately filled with a dlink router. I provide the current example as evidence that this company's products are good for something after all.

Let it be known that dlink manufactures excellent but expensive cases, which unfortunately come with free wireless routers.

The two switches connect the batteries, the dials are for bias/gain. The output is via an RC jack. BNC or coax jacks would have also been good.

The photo with the scope shows a 2.5khz sawtooth wave. If you connect the outputs to a small speaker, you can hear it too!

N.B.: The scope photos are mainly of the waveforms I photographed before I built the amplifier. I could not see any distortion produced by the amplifier, which demonstrated surprisingly even gain for the frequency ranges produced by this device.

Finally, here is a reference I would have found useful if I had found it before building this:
http://www.avr-asm-tutorial.net/avr_en/AVR_DAC.html

Step 5: Programming the AWG

Here's a guide to programming this device. I will start with the program used to generate the 1.7 Mhz sine wave:

START:

.include "m8515def.inc" ;This is a definition file, a very useful thing to use. If you need a copy, google the filename

REGISTERS0:
ldi r16,0x00
ldi r17,0x25
ldi r18,0x7F
ldi r19,0xD9 ;Load registers first, that way later your code can produce ~1 output per clock cycle
ldi r20,0xFF ;These values were determined by 127*sin(x)(pi/4), for positive integer values of x.
out DDRB,r20

2Mhz sine0:
out PORTB,r18
out PORTB,r19
out PORTB,r20
out PORTB,r19
out PORTB,r18
out PORTB,r17
out PORTB,r16
out PORTB,r17 ;One period of sine wave @ 2Mhz if you use a 16Mhz clock speed
rjmp 2Mhz sine0

This following are examples of 1Mhz sine waves, generated two different ways.

1Mhz sine0:
out PORTB,r18
nop
out PORTB,r19
nop
out PORTB,r20
nop
out PORTB,r19
nop
out PORTB,r18
nop
out PORTB,r17 ;One period of sine wave @ 1Mhz if you use a 16Mhz clock speed
nop
out PORTB,r16 ;This is the lazy way.
nop
out PORTB,r17 ;The next example will demonstrate the better way.
rjmp 1Mhz sine0

REGISTERS1:
ldi r16,0x7F
ldi r17,0xAB
ldi r18,0xD1
ldi r19,0xF6
ldi r20,0xFE
ldi r21,0x53 ;Notice we've loaded 9 registers to memory! Note how many registers you have, and
ldi r22,0x2D ;make good use of them. Where 127*sin(x)(pi/n), n can be any number of registers
ldi r23,0x08 ;where number of registers plus 1 divided by 2...unless I'm wrong!
ldi r24,0x00
1Mhz sine1:
out PORTB,r16
out PORTB,r17
out PORTB,r18
out PORTB,r19
out PORTB,r20
out PORTB,r19
out PORTB,r18
out PORTB,r17
out PORTB,r16
out PORTB,r21
out PORTB,r22
out PORTB,r23
out PORTB,r24
out PORTB,r23
out PORTB,r22
out PORTB,r21
rjmp 1Mhz sine1

The above is a nice example of the tradeoff between resolution and frequency. By halving the resolution, you can double the frequency. A perceptive reader
will have noticed that both waveforms use 0x7F (127) as a zero point regardless of the order the registers are loaded... You may determine that a different
zero point is more useful for certain waveforms... but for symmetrical ones like you're most likely to use, 0x7F is optimal.

Now, we move to a more complicated topic... how do we generate a 1.5Mhz waveform? Consider:
sine(x)(pi/6)
which would be the correct resolution to use... but, since this resolution divides evenly into 2pi, but not into p/2... our waveform will look strange,
because at no point is the output equal to the minimum or the maximum of the function, which is to say something near to 0x00 or 0xFF! For high
frequencies, the waveform may be approximately correct anyway, because of the natural capacitance and inductance in any circuit. This resists any change
in current or voltage, so at higher frequencies, if you output 0x00 ten times, then 0xFF twice... the second 0xFF will give you a somewhat higher value
than the first one. Try it and see, it may or may not work depending on variables that are too complex to discuss here.

The point is that it's difficult or impossible to generate frequencies that are not binary fractions of the clock speed... At very high
frequencies you might be able to "cheat" using parasitic capacitance and inductance... and certainly at lower frequencies the issue becomes irrelevant
as we'll see in the next example... but certainly there are some frequencies that cannot be generated.

A clever engineer (ie: not me) will install a socket to hold the crystal oscillator used in this device... that way, s/he can trivially change the
fundamental frequency of the device and obtain essentially any frequency they want within the specifications of the microcontroller
(I've seen cheap...2$... atmels that work up to 20Mhz clock speeds).

Now, here's some code for a decidedly lower frequency waveform. It's basically code from the website I listed as a reference:
http://www.avr-asm-tutorial.net/avr_en/AVR_DAC.html
The waveform is a sawtooth wave. Go check out the website as it is very useful and the code there is really good for low-medium frequency waveforms.

.include "m8515def.inc"
START:
ldi r18,0xFF
out DDRD,r18

SAWTOOTH:
out PORTD,r18
inc r18
rjmp sawtooth

This generates a waveform of about 2.5 kilohertz. You could increase/decrease frequency by adding pauses (nop) or timers, or you could increase frequency
by decreasing the resolution...instead of inc (increment) simple add a number to r18. If you add 2, the frequency would double. If you add 3 and a pause
(nop), the frequency will increase by 1.5 times.

To make a triangle wave, add a cpi statement to test if r18 is equal to 0xFF, and if so, branch to a similar function that decrements or subtracts from r18. That function must of course test if r18=0x00, and if so branch back to the first function.

I'll end this tutorial with a few clues on how to cleverly use this device:
-Use proper timer functions to accurately create low frequency waveforms. It's harder than you think to keep track of clock cycles of programs in your head.
-If timer functions scare you (they scare me), count clock cycles in your head, and then test it on your scope to make sure it's correct.
-A decimal to hex converter is a very useful thing when determining what the values of registers should be.
-Don't hook this device to an antenna and use it for wireless communications unless you have a license and know what you're doing.
-You can probably program up to a 4Mhz square wave with this device... use it as a variable clock source, or to inject serial communications into a circuit.
-8 of these together with a common clock would make a really cool programmable parallel logic source.
-Generate neuron action potentials with it, and no doubt save your biology lab a lot of money.
-Make a piano with it.
-This device leaves many inputs on the atmega unused. If you want the device to be more convenient but have restricted functions, you could build an
interface for it and a clever program so you can generate a range of waveforms and frequencies without reprogramming.
-Remember that rjmp takes clock cycles and creates an artifact! Get around this by including many periods in your program before looping. Make good use
of all that memory on the atmegas!

Outdated (Legion Labs is a new, nonprofit, no-degrees-required research effort currently located in Montreal. We are not affiliated with any other organizations.
It currently has one member, since I've only very recently considered expanding the scope of this operation.)

Current: Legion Labs is a member of a montreal-based nonprofit research/engineering effort with a number of other people, who rent out an industrial workshop as a place to tinker.

Step 6: Improvements Underway

Previously mentioned possible improvements include:

- Use heat shrink tubing to protect resistors in the R/2R from shorting.
- Use ZIF sockets, at least on your STK500, if you can afford it.
- Don't be as lazy as I was: build a better amplifier to give yourself a larger voltage range.
- Include more signal bias range if you like.
- Add ISP+USB interface!

Now, there was some talk about adding a variable clock source. I tried attaching the outputs of a hex inverter to it's inputs, but the internal resistance/capacitance of the device limited frequency to a maximum of about 4.5 Mhz.

The theory is that you attach a resistor and capacitor such that the capacitor charges at a certain rate, and causes the hex inverter to...err.... invert logic. This then causes the charge in the capacitor to deplete, causing it to invert again, and so forth. A variable resistor allows you to control frequency.

Nonetheless, this technique may be useful in a future project where lower frequencies are fine, and I need to control them precisely.

I invite the readers to suggest alternate methods, or a hex inverter that will produce variable clock output up to 16Mhz, or even 20 Mhz.