Arduino Waveform Generator




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:

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 half of this, 190kHz, 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 and 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 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 should be connected to ground, the other two 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.

Step 5: Usage

The apparatus 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.



    • First Time Author

      First Time Author
    • PCB Contest

      PCB Contest
    • Make it Glow Contest 2018

      Make it Glow Contest 2018

    36 Discussions


    13 days ago

    This presents an oversimplification of the R2R network requirements for accuracy. The pin outputs need to be well matched, which is
    best done a light loads, so stick with the 10K values. To achieve 8-bit DAC accuracy and be monotonic (google it), the resistors' should be accurate to 1/512 or about 0.2%, so that makes them expensive.
    However, typical 1% or even 2% resistors have a resistor-to-resistor matching within a
    single batch that can be much better than their absolute accuracy. It’s also possible to “hand
    select” resistors using a DVM. Note that DVM accuracy is not as important as
    its repeatability and resolution, since it’s their ratios that is important here,
    but ALL resistors should be within 0.2% of each other! See: "resistor ladder" in Wikipedia. DACs need to be to monotonic to accurately reproduce signals without distortion.


    Question 16 days ago

    Could you please share the fritzing file? I want to create a PCB board using your model and it would be less error prone to get this right using the original design.

    1 answer

    Answer 16 days ago

    Done! beware that there may be mistakes in it! I drew it up after making the device and did not do any tests or simulations (if that is at all possible with Fritzing...)


    4 weeks ago

    Just a quick update on my purchase of a DSO138 oscilloscope. The eBay seller, cinnamon 1956-5, refunded me the cost of the case (look out for that misleading practice where the Selection box has more than one price, although it didn't when I placed my order, it did later, I'm taking screen prints from now on). The agreement was that I would buy the o'scope from him/her again (at a higher price for certain), which I've done. So I'm now waiting for another 16 days for the scope to get here (USA). I guess the case will serve as compensation for the time lost, but all is well.


    Question 7 weeks ago

    Thanks, rgco. I didn't spend too much time reading it because I am not really interested in building a waveform generator at this time BUT I am really interested in the oscilloscope off to the right. Did you build it or did you buy it already built? Any info on that would be greatly appreciated. You could probably do an Instructable on that. TIA, -- Ron

    9 answers

    Reply 7 weeks ago

    Thank you, BuddyMartn! I'm going to order one.


    Reply 5 weeks ago

    Ron, No way! I’m so sorry, I feel horrible, worse than horrible! Was it the same ad? I looked again and I’m sure you have a case with PayPal because this describes the o-scope, NOT the case. Listen, I feel so horrible, I’d like to pay you back for the money you spent on my recommendation. Please email me at (all run together and no quotes) buds iPad at (gmail). com and let me know where to PayPal the funds. I don’t see how they could have written that description and sent thrust the cover. I profoundly apologize. Buddy


    Reply 5 weeks ago

    @BuddyMartin Buddy, please don't feel responsible for this seller's misrepresentation. I knew that existed with other eBay sellers before. That is why I clicked on the Selection box but, at that time, it did not have any other option but the fully assembled o'scope, in an acrylic plastic case. It has since been updated to show 2 options (the case and the board). I am currently giving the seller a chance to redeem himself. He has offered to refund 40% of the price; I countered with 100% refund and I will buy the board from him. Still waiting for a reply. If he doesn't do right, eBay will get a chance; if eBay fails, my credit card has purchase guarantees. Thanks again for your message, Buddy, please don't feel that you caused the problem, you didn't.


    Reply 5 weeks ago

    It finally got here (16 days later) BUT it was the case not the oscilloscope! It looks like beautifully laser cut pieces of clear plastic with the tape still on the surfaces. I am in the process of trying to get my money back. I wish I had ordered my DSO138 from someone other than cinnamon 1956-5 on eBay. And now I've lost 2+ weeks waiting.


    Answer 6 weeks ago

    I've been shopping for one of these, too. A couple of things I've found in my research:

    There's a successor to the model in this Instructable, called the DSO150 (the one above is the DSO138). It's the same electronic design, but the mechanical design is very different. It's case-friendly (and often comes bundled with a case), so you can actually carry it around as a "pocket" scope.

    I've seen a couple of user comments that it's also easier to assemble.

    And I've seen at least one warning that it's a little fussy about the power supply: it says "9V", and it means it. If you go much lower, the onboard voltage regulator drops out. If you go much higher (like, say, with a 3S LiPo battery), the regulator may "release its magic smoke".


    Answer 7 weeks ago

    Hi, yes, I confirm the scope was bought as shown, I paid 13EUR online. There are kits as well but after watching a video of how to assemble that, I figured it was worth to spend a few euros more for the assembled scope.
    Actually I do have an instructable for how to use an Arduino as a scope ( ) which has a plus that it's multichannel. However the sampling rate is lower (77ks/s vs 1000ks/s) and it's a bit of a hassle to use the laptop screen as a scope display


    Answer 7 weeks ago

    The video really did make a great display of that little oscillocscope and it convinced me to buy one. Now they have them completely assembled for less than $26 USD at amazon ( Amazing.


    Question 5 weeks ago

    Thanks for this. I've built it, but not having much luck with the LM358 connections - I'm just getting noise - is it because I've mis-understood the wiring? The schematic doesn't tie-up with the breadboard exactly, so inferred the final connections. I get about 50mv from point 5/6 (raw signal)but doesn't drive my scope (same as yours) am I doing something silly?

    1 answer

    Answer 5 weeks ago

    The schematic and breadboard should by construction match up (Fritzing makes both from the same input) but I agree they are not super clear!
    From what you say I suggest to debug in steps: if you disconnect the R2R from the rest, does it give a waveform with minima-maxima of 0-5V? If not, the issue is likely the R2R bridge. If yes, reconnect the attenuator. Depending on the setting the max should now be in the range 0-4V. Then the unamplified (buffered) output, then the amplified output.


    6 weeks ago

    Nice job! I have a need for something portable that generates more-complex waveforms at low frequencies, and this may well be the basis for it.

    One suggestion: you can buy R2R ladders as SIPs or DIPs, and will probably get a closer match by using one. Plus you save a bunch of board space and soldering. I found the Bourns 4610X-R2R-103LF (10K SIP) for a buck at Mouser, and it's probably available at about the same price from Digi-Key, Farnell, etc.

    Thanks for sharing this.

    1 reply

    Reply 6 weeks ago

    Thanks for the tip! I must say I was surprised how well the R2R DAC works even without any special measures: I had started to make a list of resistor values to make matched pairs, but then got bored and thought I could adjust afterwards if needed. Instead there is no sign of any steps in going e.g. from 127 to 128. Also the soldering wasn't too bad since the schematics almost matches the pin layout, so there is no need for bridges, and resistors are easy anyway since they are symmetric and temperature-resistant. But I'll definitively consider for future projects!

    If you don't need the highest frequencies, the waveform array can indeed be extended to e.g. 1024 bytes using a 16-bit integer as index. The number of clock cycles will need to be redetermined (easiest by trial and error) but I suppose it won't add more than 10 cycles.


    Question 7 weeks ago on Step 5

    Is 1muF capacitor 1 microF or nanoFarad. IF it's micro, can I use an electrolytic and if so which way should the connections be?
    Also any help with encoder connections??

    1 answer

    Answer 7 weeks ago

    It is 1 microfarad. It is not essential but helps to smooth the power to the opamp. I would recommend to put the largest ceramic capacitor that you have. Most sets go up to 100nF and that is probably fine. I once bought a bag of 100 1microfarad ceramic capacitors for less than 1 euro and they come in great everytime an IC is used in a project. I would avoid electrolitic capactors here because their capacitance is very much reduced at high frequencies.