Arduino Waveform Generator

29,212

154

75

Introduction: 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:

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

Teacher Notes

Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.

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.

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.

Electronics Tips & Tricks Challenge

Participated in the
Electronics Tips & Tricks Challenge

Be the First to Share

    Recommendations

    • LED Strip Speed Challenge

      LED Strip Speed Challenge
    • Sculpting Challenge

      Sculpting Challenge
    • Clocks Contest

      Clocks Contest

    75 Discussions

    0
    smallsol
    smallsol

    Question 25 days ago on Step 2

    Completed project with one problem. Turning the switch does not change waveforms, it only changes frequency. Pushing and turning does move the cursor where I want it to change frequency. I think I have done something wrong but can't find the problem. Please help, I am a complete noob with Arduino projects.
    Thank you,
    Steve

    0
    rgco
    rgco

    Answer 25 days ago

    Very strange! the cursor should move to the first letter of the waveform if you keep turning left with the button pushed. Then turning without pushing changes the waveform. From your description it doesn't seem to be a hardware problem, but if you dowloaded the code it can't be a software problem either!

    0
    smallsol
    smallsol

    Reply 25 days ago

    Project is now working! It was a flaky encoder switch. Replaced it with another and works perfect. Thank you for your prompt reply.
    Steve

    0
    smallsol
    smallsol

    25 days ago on Step 5

    I'm sorry, I left out that I am using a genuine UNO board. Also, the only waveform I can get is a SINE wave.
    Yhanks again,
    Steve

    0
    trexdd2
    trexdd2

    Question 2 months ago

    I've finished making the board, and when I used your code, this shows up. May I ask what the solution is for this problem?

    20200108_194833.jpg
    0
    MRMOIS
    MRMOIS

    Answer 7 weeks ago

    I had the same issue. The problem was a missing driver on my windows for the CH340 chip on the Arduino. Many chinese counterfeit Arduino boards come with this chip, instead of the one on original Arduino boards. Hence the IDE does not install the drivers for this chip. Just search for "CH340 Windows driver" and install it.

    0
    rgco
    rgco

    Answer 2 months ago

    Looks like a problem with uploading. I guess you choose for Board "Arduino Nano", the check Processor, there is "ATmega328P" and "ATmega328P (old bootloader)". I need to choose the latter to upload succesfully

    0
    mausi_mick
    mausi_mick

    2 months ago

    Now the two waveform generators are nearly finshed.

    I have overclocked the MapleMini (normal: 72 MHz, overclocked : 128 MHz).

    The signal a better at higher frequencies and also the signal at higher frequencys at the Osci is better.
    The video shows the signals generated with the LGT8F328p(32MHz) on the MapleMini Dual-Osci (I have here switched off the secon channel).

    https://youtu.be/qg0Yn3Hg5r4


    0
    mausi_mick
    mausi_mick

    3 months ago

    Hi, now I have 2 waveform generators,
    one with a LGT8F328p processor instead of ATMega328P and 1602 LCD running at 32 Mhz
    and one with a MapleMini (STM32F103 ) and ILI9341 2.8" Touch-Display, running with 72 Mhz.

    In the MapleMini I have integrate a Dual-Channel Oscilloscope with about 1 MSPS.
    I can choose over a touch-menu
    - normal WaveForm Generator
    - sweep WaveForm Generator or
    - Dual Channel Oscilloscope.

    You can see it here in action:


    P1000056.JPGP1000057.JPGP1000062.JPGP1000063.JPGP1000064.JPG
    0
    yurdumajans
    yurdumajans

    Question 3 months ago

    merhaba
    böyle birşey yaptım elektronik hakkında çok bilgim yok çıkışlardan sadece hışırtı alıyorum sanırım biryerde hata yaptım.
    ekranıda kodlayabilirsem 4 pinli bir ekran kullanıcam

    kontrol ederek hatamı yazarsanız çok mutlu olurum.
    teşekkürler.

    IMG_20191227_171616.jpgIMG_20191227_171630.jpgIMG_20191227_180320.jpg
    0
    yurdumajans
    yurdumajans

    3 months ago

    Merhaba

    4 Hz e kadar düşürmek mümkünmüdür ?

    0
    rgco
    rgco

    Reply 3 months ago

    4Hz is no problem. It goes down to 1mHz

    0
    yurdumajans
    yurdumajans

    Reply 3 months ago

    Teşekkür ederim

    bilgim az ama en kısa zamanda uygulayacağım.

    0
    mausi_mick
    mausi_mick

    Tip 3 months ago on Step 4

    Now I have tested it with a LGT8F328P, running at 32MHz.
    It has better results for sine upper 15 kHz. But I think 80 kHz(last picture) is to much :

    LGT8F328P_Wemos_XI_board.JPGLGT8f328P_24kHz.JPGLGT8F328P_24kHz_f.JPGLGT8F328P_41kHz.JPGLGT8F328P_41kHz_f.JPGLGT8F328P_80kHz.JPG
    1
    mausi_mick
    mausi_mick

    4 months ago

    The A-Star 328PB Micro (Pololu) was a litlle bit better or quicker than the original NANO with 16 MHz, but the Sinus-signal at about 20 kHz was not nice. Therefore I tried to port the software to
    STM32 MapleMini, running with 72 MHz.
    After some time I had succes:

    https://www.youtube.com/watch?v=tasBP7qpmMM&feature=youtu.be

    0
    rgco
    rgco

    Reply 4 months ago

    Wow, good work, that looks like a big jump in sample rate, even more than the factor 3.6 from the clock frequency, it looks like the sampling loop takes fewer cycles thanks to the intrinsic 32 bit calculations! The scope trace seems to have at least 30 points per cycle, so that would be 3Msps. You should really publish this as a separate instructable! I once tried but failed to program the STM32F103C8T6, and would be quite interested to see a reliable instructable on that.
    One follow-up idea I had but never came to was to make the function shape more flexible: fewer shapes, but each shape with additional parameters e.g. duty cycle, rise-time, fall time for block, amplitude, phase and frequency for double-sine.
    Also, if the STM has more RAM and it can search it fast, there is real gain from having a larger signal array (e.g. 4096samples) : square waves with very low duty cycles or pulse trains would then be possible.
    The triangle and sawtooth have a little bump halfway the range, it looks as if your DAC is not perfectly monotonic, you may want to test that at lower speeds.

    0
    mausi_mick
    mausi_mick

    Reply 4 months ago

    I had some problems to migrate Your code, to set the Port (like PORTD = ...) is different . Therefore I tried it at first with a special "digitalWriteFaster" to the eight Pins, but now I use the ODR command , it's for all 32 bits of the port ! It's quicker.

    I programmed it with the Arduino IDE (STM32DUINO from Roger Clark Melbourne).
    On the STM32F103CB (MapleMini / BluePill) is also more space for programming (128k) and nearly each pin you can use for ext. interrupts .

    But if i measure the frequency at 1 kHz , it is ok (because I have adjusted it there!).
    But if adjust my LCD to 100 kHz, the real frequency is about 3% higher.
    Because of the time for the ODR command in the loop.

    0
    mausi_mick
    mausi_mick

    Reply 4 months ago

    Hi,
    I have found the error in my curves, I had reversed some pins!
    Now the curves are o.k. and the frequencyon the display is near the frequency of the counter.
    I made some pictures f:
    Sinus at 100 kHz, Sinus at 200 kHz, Sinus at 300 kHz, Sinus at 400 kHz.
    At 400 kHz it looks like a triangle wave !

    MapleWaveGen_100kHz.JPGMapleWaveGen_200kHz.JPGMapleWaveGen_300kHz.JPGMapleWaveGen400kHz.JPG
    0
    rgco
    rgco

    Reply 4 months ago

    Hi, it says "Video unavailable. This video is private."
    A sweep is interesting. I did not consider it since I had no need for it and the update of the phase increment inside the core loop would slow things down, or is it not too bad?
    Your scope pictures seem to show wiggles that probably correspond to the sampling frequency, I would guess they come either from the amplification step or from the scope (which scope is that by the way, it looks like a budget scope but it has a much better bandwidth than my 1Msps DSO138). An RC filter could be used to filter out the frequencies above the sampling rate. The output impedance of the DAC already provides the R, so it would just be a matter of adding a capacitor to ground at the exit of the R2R DAC. The value of C should be or order 1/(fR)=1/(1MHz*10kOhm)=100pF.