Introduction: Arduino Waveform Generator
Feb. 2021 update: check out the new version with 300x the sampling rate, based on the Raspberry Pi Pico.
In the lab, one often needs a repetitive signal of a certain frequency, shape and amplitude. It may be to test an amplifier, check out a circuit, a component or an actuator. Powerful waveform generators are available commercially, but it is relatively easily to make a useful one yourself with an Arduino Uno or Arduino Nano, see for example:
https://www.instructables.com/id/Arduino-Waveform-...
https://www.instructables.com/id/10-Resister-Ardui...
Here is the description of another one with the following features:
* Accurate waveforms: 8-bit output using R2R DAC, 256-sample shape
* Fast: 381 kHz sampling rate
* Precise: 1mHz steps frequency range. As accurate as the Arduino crystal.
* Easy operation: waveform and frequency settable with single rotary encoder
* Wide range of amplitudes: millivolts to 20V
* 20 pre-defined waveforms. Straightforward to add more.
* Easy to make: Arduino Uno or Nano plus standard components
Step 1: Technical Considerations
Making an analog signal
One shortcoming of the Arduino Uno and Nano is that it does not have a digital-to-analog (DAC) converter, so it is not possible to make it output an analog voltage directly on the pins. One solution is the R2R ladder: 8 digital pins are connected to a resistor network so that 256 levels of output can be reached. Through direct port access, the Arduino can set 8 pins simultaneously with a single command. For the resistor network, 9 resistors with value R are needed and 8 with value 2R. I used 10kOhm as a value for R, that keeps the current from the pins to 0.5mA or less. I guess R=1kOhm could work as well, since the Arduino can easily deliver 5mA per pin, 40mA per port. It is important that the ratio between the R and the 2R resistors is really 2. That is most easily achieved by putting 2 resistors of value R in series, for a total of 25 resistors.
Phase accumulator
Generating a waveform then comes down to repetitively sending a sequence of 8-bit numbers to the Arduino pins. The waveform is stored in an array of 256 bytes and this array is sampled and sent to the pins. The frequency of the output signal is determined by how fast one advances through the array. A robust, precise and elegant way to do that is with a phase accumulator: a 32-bit number gets incremented at regular intervals, and we use the 8 most significant bits as the index of the array.
Fast sampling
Interrupts allow to sample at well-defined times, but the overhead of interrupts limit the sampling frequency to ~100kHz. An infinite loop to update the phase, sample the waveform and set the pins takes 42 clock cycles, thus achieving a sampling rate of 16MHz/42=381kHz. Rotating or pushing the rotary encoder causes a pin change and an interrupt that gets out of the loop to change the setting (waveform or frequency). At this stage the 256 numbers in the array are recalculated so that no actual calculations of the waveform need to be performed in the main loop. The absolute maximum frequency that can be generated is 190kHz (half of the sampling rate) but then there are only two samples per period, so not much control of the shape. The interface thus doesn't allow to set the frequency above 100kHz. At 50kHz, there are 7-8 samples per period and at 1.5 kHz and below all 256 numbers stored in the array get sampled each period. For waveforms where the signal changes smoothly, for example the sine wave, skipping samples is no problem. But for waveforms with narrow spikes, for example a square wave with a small duty cycle, there is the danger that for frequencies above 1.5 kHz missing a single sample can result in a the waveform not behaving as expected
Accuracy of the frequency
The number by which the phase is incremented at each sample is proportional to the frequency. The frequency can thus be set to an accuracy of 381kHz/2^32=0.089mHz. In practice such accuracy is hardly ever needed, so the interface limits to set the frequency in steps of 1mHz. The absolute precision of the frequency is determined by the precision of the Arduino clock frequency. This depends on the Arduino type but most specify a frequency of 16.000MHz, so a precision of ~10^-4. The code allows to modify the ratio of the frequency and the phase increment to correct for small deviations of the 16MHz assumption.
Buffering and amplification
The resistor network has a high output impedance, so its output voltage quickly drops if a load is attached. That can be solved by buffering or amplifying the output. Here, the buffering and amplification is done with an opamp. I used the LM358 because I had some. It is a slow opamp (slew rate 0.5V per microsecond) so at high frequency and high amplitude the signal gets distorted. A good thing is that it can handle voltages very close to 0V. The output voltage is however limited to ~2V below the rail, so using +5V power limits the output voltage to 3V. Step-up modules are compact and cheap. Feeding +20V to the opamp, it can generate signals with voltage up to 18V. (NB, the schematic says LTC3105 because that was the only step-up I found in Fritzing. In reality I used an MT3608 module, see pictures in the next steps). I choose to apply a variable attenuation to the output of the R2R DAC then use one of the opamps to buffer the signal without amplification and the other to amplify by 5.7, so that the signal can reach a maximum output of about 20V. The output current is rather limited, ~10mA, so a stronger amplifier may be needed if the signal is to drive a large speaker or electromagnet.
Step 2: Required Components
For the core waveform generator
Arduino Uno or Nano
16x2 LCD display + 20kOhm trimmer and 100Ohm series resistor for backlight
5-pin rotary encoder (with integrated pushbutton)
25 resistors of 10kOhm
For the buffer/amplifier
LM358 or other dual opamp
step-up module based on the MT3608
50kOhm variable resistor
10kOhm resistor
47kOhm resistor
1muF capacitor
Step 3: Construction
I soldered everything on a 7x9cm prototype board, as shown in the picture. Since it got a bit messy with all the wires I tried to colour the leads that carry positive voltage red and those that carry ground black.
The encoder I used has 5 pins, 3 on one side, 2 on the other side. The side with 3 pins is the actual encoder, the side with 2 pins is the integrated pushbutton. On the 3-pin side, the central pin should be connected to ground, the other two pins to D10 and D11. On the 2-pin side, one pin should be connected to ground and the other to D12.
It's the ugliest thing I've ever made but it works. It'd be nice to put in an enclosure, but for now the extra work and cost doesn't really justify it. The Nano and the display are attached with pin-headers. I wouldn't do that again if I'd build a new one. I did not put connectors on the board to pick up the signals. Instead, I pick them up with crocodile leads from protruding pieces of copper wire, labelled as follows:
R - raw signal from the R2R DAC
B - buffered signal
A - amplified signal
T - timer signal from pin 9
G - ground
+ - positive 'high' voltage from the step-up module
Step 4: The Code
The code, an Arduino sketch, is attached and should be uploaded to the Arduino.
20 waveforms have been pre-defined. It should be straightforward to add any other wave. Note that the random waves fill up the 256-value array with random values, but the same pattern gets repeated every period. True random signals sound like noise, but this waveform sounds much more like a whistle.
The code sets a 1kHz signal on pin D9 with TIMER1. This is useful to check the timing of the analog signal. That is how I figured out that the number of clock cycles is 42: If I assume either 41 or 43, and generate a 1kHz signal, it clearly has a different frequency from the signal on pin D9. With the value 42 they match perfectly.
Normally, the Arduino interrupts every millisecond to keep track of time with the millis() function. This would disturb the accurate signal generation, so the particular interrupt is disabled.
The compiler says: "Sketch uses 7254 bytes (23%) of program storage space. Maximum is 30720 bytes. Global variables use 483 bytes (23%) of dynamic memory, leaving 1565 bytes for local variables. Maximum is 2048 bytes." So there is ample space for more sophisticated code. Beware that you may have to choose "ATmega328P (old bootloader)" to upload successfully to the Nano.
Attachments
Step 5: Usage
The signal generator can be powered simply through the mini-USB cable of the Arduino Nano. It is best done with a power bank, so that there is no accidental ground loop with the apparatus that it may be connected with.
When switched on it will generate a 100Hz sine wave. By rotating the knob, one of the other 20 wave types can be chosen. By rotating while pushed, the cursor can be set to any of the digits of the frequency, which can then be changed to the desired value.
The amplitude can be regulated with the potentiometer and either the buffered or the amplified signal can be used.
It is really helpful to use an oscilloscope to check the signal amplitude, in particular when the signal supplies current to another device. If too much current is drawn, the signal will clip and the signal is heavily distorted
For very low frequencies, the output can be visualised with an LED in series with a 10kOhm resistor. Audio frequencies can be heard with a speaker. Make sure to set the signal very small ~0.5V, otherwise the current gets too high and the signal starts clipping.

Participated in the
Electronics Tips & Tricks Challenge
100 Comments
Question 1 year ago on Step 4
Hello again! Can you talk a little more about pin 9 and its purpose? I don't see a wire connection in the schematic, nor the PCB Board. You spoke about a disturbance to the signal generation in Step 4, is that the reason you aren't actively connecting it?
When would you use D9 and how would you connect it?
Answer 1 year ago
D9 is what I'd call a debug-pin. It is not connected to anything, apart from a little copper bridge to make it easier to connect a scope probe to it. The idea is that if the circuit doesn't work, you can check if there is a 1kHz square wave on pin D9. That way you know that the device is powered well, that the code is uploaded and that the frequency is reliable. Moreover, any glitches would result in loss of phase between the analog signal and the square wave. Since I observed that D9 and the analog signal remained in perfect phase for minutes, I could confirm that this AWG can rely for its timing on a fixed number of clock cycles in the loop and has no need to be synchronised by a hardware timer.
Reply 1 year ago
Thank you! That was really helpful.
Question 1 year ago
Question, so I noticed in the circuit you built, there is a resistor added to the 'A' connection of the LCD display, but this is not on the drawn-out schematic. Is this a necessary component? If so what is the resistance? It is hard to make it out in the image.
Answer 1 year ago
You are right! Apparently I forgot to draw the connections to the anode (A) and cathode (K) of the LCD display. It will work without, but the display will not be illuminated. So in the actual device, I connected K to ground and A to 5V, through a 100Ohm current-limiting resistor. Again, all this does is light the LED inside the LCD display.
Question 1 year ago on Introduction
Hello! I am learning more about these devices and Arduinos, and I am very interested in your work.
I am at a loss trying to identify a component in your video. I have circled it in a snapshot below.
Answer 1 year ago
Glad you like it! The blue/white component is the 50kOhm potentiometer, R27 in the schematic. They come in many different shapes. The one in the picture is the trimmer-type, usually meant for adjusting once and then leave alone. Instead, you can also put a proper potentiometer with a shaft.
Question 1 year ago
Dear RGCO,
Thank you for the very simple, straightforward and instructive project.
I am reproducing it part by part and it is interesting.
I would like to make a video about the project (mainly for undergraduate students at a physics course here) and I'd like to know if you allow me to share your ideas in the video. You'll be credited, of course.
also, I will test with the ESP32, maybe we can have a better sampling rate.
As someone said here, the Arduino for this project is very attractive for simplicity, cost, availability, at least here in Brazil.
Regards, Fabricio
Answer 1 year ago
It's open source, licence allows copying, sharing, modifying, etc, as long as the result is also open source and as long as it is credited, so please go head! - if you can post a link here once it's ready, that'd be awesome.
I've never tried the ESP32, I've understood it's powerful. It was a bit expensive when it came out (better now) and I had no need for Wifi, bluetooth etc. There is a new version however linked at the beginning of this instructable using the very cheap raspberry pi pico, which samples faster by 2 orders of magnitude, but no stand-alone interface or amplifier yet, also slightly less accurate frequency setting.
2 years ago
As a novice here I like to ask some questions on this project.
The Fritzing schematic is not too clear, an old-fashioned schematic would be better understandable....
Could you please explain the usage of the MC3608? What output voltage setting is used here?
According the datasheet the LM358 runs at 3 - 32 volt...
Reply 2 years ago
Agreed, Fritzing is aweful (and dead by now indeed). The step-up is adjustable, best is to set it 2V higher than the maximum output voltage, anything above that is just dissipated in the opamp. Really, it makes not much sense to rebuild this project literally, by now it's better to remake in steps and improve on them.
For the output stage, please use a better opamp, NE5532 or TL072. For the stepup, there are nowadays +- supplies to make true AC signals:
https://www.aliexpress.com/item/33035241680.html?s...
and the Raspberry pi pico is so much better for this than the Arduino, here for >30kHz the waveforms start to suffer badly from the low sampling rate.
Reply 2 years ago
Thanks for your advise on step up converter and more.
Since I am more familiar with Arduino I will continue to use it.
Using the NE5532 as a better amplifier is a good idea.
Frequencies above 30 kHz are not required for my current project, I will consider to use Raspberry in a later stage, if the high frequencies are required.
Reply 2 years ago
Okay! indeed if 384ksps are sufficient, this may be good solution. Also the limited RAM (and the use of bytes for indexing) limits the complexity of the waveform to 256 samples. ample for sine, triangle etc but very-low duty cycle pulses, or complex shapes won't work. Beware that the NE5332 doesn't do to negative rail. Good luck!
Reply 2 years ago
Do you have an idea which values should be used in the resistor ladder??
Reply 2 years ago
Here I used R=10k. With the PICO I used 2R=2k. Since R=1k worked for the PICO, I'd be pretty confident it also works for the Arduino, which is known for it's relatively high output current capabilities. On the other hand there may not be an advantage in going below R=10k if you're below 1MHz and have an amplifier with a high input impedance. Overall I'd stick with 10k unless or whatever you have available in the 1k-20k range. In the instructable for the pico I show a technique to get a matched set of resistors.
Question 2 years ago on Introduction
Hello!
Wonderful and interesting generator.
However, I can't find the schematics.
Could you provide it or indicate where is it?
Thank you very much
Answer 2 years ago
It's in step 3. Sorry these are poor Fritzing 'schematics'. I'm learning KiCad now ;-)
Reply 2 years ago
Did you manage to create a PCB layout in KiCad?
If so, please share it with us
Reply 2 years ago
Sorry, I didn't redo the schematics nor PCB layout of this project. The updated project with the Raspberry pi pico (300x faster) is in KiCad.
Reply 2 years ago
Thank you very much.
If you send me the schematics with component values, in Fritzing or even drawn by hand, I can draw it with KiCad.
Regards
Horacio