Arbitrary Waveform Generator, for ~20$




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

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.

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

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:

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:

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:


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

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
out PORTB,r19
out PORTB,r20
out PORTB,r19
out PORTB,r18
out PORTB,r17 ;One period of sine wave @ 1Mhz if you use a 16Mhz clock speed
out PORTB,r16 ;This is the lazy way.
out PORTB,r17 ;The next example will demonstrate the better way.
rjmp 1Mhz sine0

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:
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:
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 ""
ldi r18,0xFF
out DDRD,r18

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.



    • Creative Misuse Contest

      Creative Misuse Contest
    • Water Contest

      Water Contest
    • Tiny Home Contest

      Tiny Home Contest

    36 Discussions

    you can use the arduino IDE to program the AVR easily. Also you can use the PWM to replace the R-2R DAC.

    5 replies

    Yes, that would work too, but my personal preference is assembly language. Also, avoiding the arduino means I can intergrate this into more permanent projects at a lower cost. To each their own!

    PWM also normally gives you a square wave, but you could filter it.

    You could also do all of this with opamps, which would be a lot of un!

    Well, there are several ways. Let's discuss 2: the simplest and most reasonable, and the most absurd.

    First (and most reasonably), you could build various tunable oscillators using 1-4 opamps. A Wein Bridge circuit would be simplest (, but the awesomely named "Bubba oscillator" is more precise. You *could* tune with a variable cap, but a trimpot or potentiometer is a better choice. Amplify the output via LM386 to provide relatively low impedance compatible output.

    The most absurd method would be to build a CPU and RAM entirely out of opamps, and emulate the AVR chip I used... if you do this, my hat is off to you.

    If I were to redo this project, I'd use magnetoresistive memory and a counter to generate waveforms through an R/2R. You could reach higher frequencies (maybe 5x)!

    thanks legion ,
    i just need a sine wave , the frequency does not matter
    its more of the output voltage and current ,

    I guess the most logical questions are "what voltage/current", and "how accurate of a sine wave does it have to be"?

    Heck, if up to 20khz, ~2v, and 15-25mA is fine, then just use an MP3 player and make your sine wave in Audacity (open source sound editor).

    love it thats all a Dlink is good for a case lol.

    This illustration shows (or implies) that there are two 9 volt batteries or voltage sources. The common terminal as shown would be the ground (or more appropriately called circuit common). If you used a single 18V source and the circuit shown, then the common terminal created by the voltage divider would be referred to as a virtual ground (or circuit common).

    2 replies

    Indeed, there were two 9v batteries in this design. You've got a good eye, my friend! These days I try to save batteries by using an 18v wall adapter, but many of these use switching voltage regulators and I've had some problems with power supply noise in very sensitive designs (for example, circuits with gain over 15 million). For this design, an 18v wall adapter source would work just fine. Makes the device less mobile though, that's for sure.

    Switchers in sensitive circuits are always an issue. Special filtering is required, or a switcher designed for noise free DC (which just means the filtering is in the switcher) more $$. ;-)

    can you hack up an AM transmitter to it and put it on the frequency your transmitter is so your transmitter can send further?

    6 replies

    I should warn you that being caught transmitting on most radio frequencies without a license means an eventual visit from secret services in my country. If you push your existing transmitter past the FCC regulations (or the regulations in your area), you're asking for trouble. The answer to your question is twofold. Firstly, what you describe is possible. Secondly, for what you are describing, it may not be the most "useful" approach. It is possible in the sense that you could use this device to generate a carrier frequency for AM or FM transmission. You could even set the frequency output quite accurately if you choose the correct crystal. However, there are important limitations. If you tear up an AM transmitter, you may find that there is some "fancy" part of the device that makes it hard to shove a different carrier frequency in there. It would be easier to just build the amplitude modulation yourself, as a simple implementation would require about 2 pieces... a transistor, and a resistor resistant enough to put your transistor into linear mode. Secondly, making a signal go "further" in radio is not a simple matter. Different frequencies are absorbed differently by the environment. Furthermore, range does not increase as drastically as you might think by using more power: a better antenna often produces better results, and is much more energy efficient. A simple dipole shouldn't require frightening amounts of math... the other types sometimes can however. Lastly, it sounds like what you want to do (more or less) is increase the strength of the existing carrier signal. The simplest way to do this is to use an operational amplifier designed to work at that frequency (expect a cost of less than 10$ for the amp circuit I used, it is OK up to a few Mhz)... or possibly just a transistor in linear mode (cost: ~0.07$ for an N2222). Overall I think it would be simpler to increase the amplitude of the existing carrier frequency rather than build a device to generate a new one. In summary, if you absolutely want to accomplish this using microcontrollers, with sufficient patience it should work. I encourage you to use whatever design that you consider the most beautiful (and within radio transmission regulations). If you have any specific questions, feel free to send me them as private messages to avoid clutter.

    Forgive my ignorance, but what about an amplifier where the antenna was, then a different antenna on the ouput of that?

    Should work. The difficult question is: What type of amplifier and what type of antenna? Worse: How do you debug it if it doesn't work? Op-amps have rather limited power output (unless you have lots of $$$), and cost also increases with frequency. You will need a nice oscilloscope for testing/debugging the amplifier, these are expensive at high frequencies. For the antenna, you'll either have to get a pre-built one, or carefully design one (a yagi is a nice design). If you want to test it, you need a spectrum analyzer, and this is very expensive. To summarize: You're right, but the devil's in the details. Analog amps and homebrew antennas are not too complex but do require tuning, and this is difficult without a little knowledge and access to equipment. It is still possible without equipment... just difficult. Of course if it's for wifi @2.4Ghz, just build yourself a cantenna or yagi using a guide and you're set. I know it worked for me ;)

    Yes, devils in the details.... A guide as in, a how-to manual, or a radio lingo guide? Would the aforementioned circuit work for 1MHz? (or close thereto?)

    This is a nice guide for cantennas:

    It will only work well for microwave frequencies (hundreds to thousands of Mhz).

    For 1Mhz, nice antennas tend to be large-ish. Antenna designs have dimensions based on how far a photon can travel in once cycle, so when you use lower frequencies, even a 1/2 dipole can be an inconvenient size. It may be possible to make a decent antenna anyway, however my limited knowledge of antenna design is not sufficient to help you out.

    I don't like super-high frequency stuff that requires waveguides... the physics makes my head hurt! But I respect those who understand stuff like this....

    Are you sure that the dip is caused by the loop function I would have expected the dip to occur every cycle, not every 10 cycles. Is there a watchdog that has to be reset? This is a great, simple project - well done!

    1 reply

    Pretty certain. If the RJMP statement was used every cycle there would be a dip each cycle... but if I recall correctly I stuck enough instructions in there to do several repetitions without needing to loop. It's a memory-time tradeoff. I used no watchdog timer.

    what is that white goop u used to glue down the wires?