Waveform generator

Basic waveform generator using ATmega328p (Arduino Nano/Uno)

Preamle

While I was testing some logic IC, I needed a clock. As manually putting in/out a wire obviously did not work probably due to bouncing effects, I started using a microcontroller for generating a basic rectangular wave alike the simple blinking Arduino tutorial project. This is not very convenient and does not easily allow creating advanced waveforms, however.

There are ready-to-use waveform generators available on Ebay, but they seem to need higher voltages than I use for all other devices which is the USB bus power of 5V. Furthermore, they are like expensive starting at about 12$ up to 20$ for the more advanced ones.

Professional equipment is even more expensive and I decided that I better spend my money on other stuff.

Idea

So I decided that building a simple waveform generator cannot be that difficult and started this little project. Searching the internet reveals that there are several ‘Arduino’-waveform generators available – but there is a problem: They depend on an Arduino Due which already has a hardware DAC (digital-analog-converter).

Thus, I at first tried using a PWM signal on my development board (mega2560). Smoothing the signal with a simple RC filter does not work very well. Although you get quite nice signals for simple waves like a rect, e.g. a sine will particularly look distorted. One may use specifically defined RC elements for good smoothing results, but this does not allow a wide range of frequencies.

So I needed to create my own DAC. Furthermore, I needed to create a waveform at user-defined sampling rates with accurate timing. Having all this, letting the user choose different amplitudes, frequencies and waves should be possible and also some output is needed.

Basic Operation

The simplest idea is using a certain waveform function (rect, triangle, whatever), setting the amplitude, a sample-rate and frequency and putting this into a main loop with different delays. This easy setup has several disadvantages:

  • You cannot use arbitrary frequencies, but you depend on your minimum delay (this is typically constrained to microsecond [microseconds()]) and your sampling rate. This gets even worse considering higher sampling rates and a fixed delay in betweeen.
  • Your maximum frequency is very constrained due to the speed of the microcontroller and ad-hoc waveform function evaluation.

I have tested a lot with this approach – but it just did not work out acceptable. Therefore, I started using a timer and precomputed waves.

DAC

An easy way for creating a scalable digital-to-analog converter is the so called R2R ladder resistance network. Here, we only need one resistor type and may put them together in a way such that each input is weighted in a binary way. This fits our needs very good as we are dealing with binary representations anyways.

I have set up an 8bit DAC, whereas due to lack of an available port on the Arduino nano having 8 external pins, I have put the least significant bit to GND. Of course, you may also just implement a 7bit DAC – the maximum output voltage will be a little smaller.

You can see the R2R network structure in this picture:

R2R ladder - resistance network

R2R ladder – resistance network

Precomputed waves

Ad-hoc computation of the waves is a simple solution – but not very clever in any way. The data does not changes and the needed memory is ridiculous.

Precomputing the waves is a straight forward task: Set function, number of samples, amplitude, allocate memory, compute:

Now we have all values set up for just getting them from the ram for writing them directly. No more need for ad-hoc computations.

Timing

As already mentioned, the simple loop with delays is not a good idea. I decided using Timer1 (on both, the mega2560 for development and later on the 328p for deployment) which counts 16bits. An output compare interrupt gives you all freedom we need for all different frequencies the microcontroller is capable of.

As I have developed the project on my mega2560, I had more than enough digital output pins. The Arduino digitalWrite function works quite well, but is by far too slow. For fast operation, we need to use the PORT register directly. I decided using PORTC which is connected to PINs 30..37 on the mega board (see e.g. this PIN map). Luckily, this seems to be the best choice for the nano as well.

Writing PORTC is quite easy (just assign a variable to the PORTC macro), but before we have to set the data direction register to output DDRC = 0xff (this may also be done by using the Arduino pinMode function for all 8 pins).

The timer 1 has to be initialised and we must the several options:

This enables time1 couting up at full 16MHz. The CTC value defines the target value to which the timer value is being compared. In case the timer value reaches this target value, the interrupt for the timer 1 compare channel A is being fired.

We will use the timer1 output compare A interrupt service routine for writing the actual waveform values. Our setup so far enables us writing output to PORTC at a very accurate and fast timing. A ood thing: Due to the register having 16 bit, we are able to map a big variety of sampling frequencies:

  • The biggest frequency is: F_{sampling} <\frac{F_{CPU \cdot samples}}{prescaler \cdot CTC_{min}}=16MHz / 89 \approx 179.742kHz \cdot samples
  • The lowest frequency is: F_{sampling} >\frac{F_{CPU \cdot samples}}{prescaler \cdot CTC_{min}}=16MHz / 65535 \approx 244Hz \cdot samples

The actual resulting wave frequency also depends on the number of samples. Assuming we want to use our 8bit DAC in its full detail, for a sine wave, we want to map all possible 256 different DAC values to the full sine range (-1 to 1; DAC values 128..255 for 0..pi/2, 255..128 for pi/2..pi, 127..0 for 2pi..3/2pi and 0..127 for 3/2pi to 2pi). Simply said, taking a look into the sine function, we see that more than 2*256 samples do not make a difference anymore. Nevertheless, we always want to use rather more than lesser samples in general.

Assuming we want a precise output of a sine, we should use 512 samples. The maximum frequency for this number of samples is 179.742Hz/ 512 \approx 351Hz. This already shows that our ‘fast’ 16MHz microcontroller is no wonder-kid. In case we want to get higher frequencies, we MUST decrease the number of samples for the wave – so it is somehow a trade-off between quality and speed.

The interrupt service routine looks quite simple as follows:

Sampling rate/speed

The important stuff is ready. But how do we actually set our values for a certain desired frequency?

I have already talking about maximum sampling frequencies and the number of samples for a wave. The formula with a fixed frequency getting the right CTC (ticks for the timer until is fires) is the following: CTC=F_{CPU} / samples . Sounds quite easy, however as already mentioned, there are restrictions on the CTC (in my case ≥ 89 if the compiler optimises agressively O3!). This ‘high’ delay basically is due to a lot of code for entering the ISR (store & restore current program context). In order to sample a desired frequency, we must decrease the number of samples.

This needs some optimisation which may be done naively as follows:

The presented method works and time is not an issue, of course one may use some more sophisticated integer optimisation technique.

Here, we encounter some discretisation issues: The desired frequency may not be reached exactly. You can compute the actual frequency which results from your samples and the found CTC as follows:

F=\frac{F_{CPU}}{samples \cdot CTC}

LCD output

The waveform generation is ready. However, the user does not know what is going on except for looking into the resulting signal via an oscilloscope. Therefore, I have used an LCD display (2×16 characters) which displays the current waveform name, actual frequency, sampling rate and amplitude (in percent):

Menu

Now as we have finished the waveform generation and displaying information to the user, we may want to add a menu for setting different values. I have tried using potentiometers, this however does not allow setting exact values. Therefore, I decided doing it all digital.

We generally do not want to disturb the waveform generation from its operation as is may be firing at the maximum possible frequency. Therefore, I have set up an external interrupt on pin 2 (external interrupt #0); This is an important point for deploying the project to thr Nano – we would have 8bits for the DAC if we used PORTB, but external interrupts  are only possible on the same port. The interrupt need sdebouncing. Furthermore, as ISRs are to be kept small, I have put the menu logic into the main loop which is entered after a flag has been set in the ISR.

If the program enters the menu, I disabled the timer and hence, interrupt the waveform generation. After everything is set up, I compute the new wave values and target CTC. Before restarting the timer, I print the wave information.

User interaction and such menus are quite complex if you want to make it user-friendly. In my setup, I used three buttons: A button attached to the interrupt for entering the menu. This button also is responsible for the next menu screen and acknowledgement. Two other buttons are used for navigation and changing values.

The main loop enters the menu branch as long as the flag is set. Here I check if any button is pressed at first. Later on, the menu relies on a simple automaton which determines which function we want to do next. Here we may also do transitions by setting a state variable to another state. If then the main loop is executed again, the next function is displayed.

Menu automaton strcuture

Menu automaton strcuture

As the menu code spans over 300 lines of code, I will not include it here. Feel free to take a look into the actual source code.

EEProm

Having default values is a nice thing. But do you really always want to tune the settings again and again? – I wanted to setup a certain wave once and it should be re-loaded even after turning the device off.

A simple solution it using the internal eeprom memory chip. Here I store and load the wavetype, amplitude and frequency as follows:

 

Examples

Sine wave at 363Hz with 363 samples

Sine wave at 363Hz with 363 samples, amplitude at 87%

Triangle wave at 24844Hz with only 6 samples

Triangle wave at 24844Hz with only 6 samples, amplitude at ~100% (I do not remember o_O)

Source code

You may download it here: wavegen source code – released to public domain. Have fun.

Todo

In case you need high output current, you probably want to add an amplifier. This however is not part of this project.

Furthermore, we may increase accurary by adding some lowpass filter – for me, a simple 1n Cap on the output works.

2 Comments

  • 1) I think more than 256 samples is useless for 8 bit R2R DAC, you can not output more details than 2^8;
    2) Do not affraid to put signal reference tables to atmega, you just need to put them into flash, not RAM. Use PROGMEM

    const uint8_t sin_wave[256] PROGMEM = {
    0x80,0x83,0x86,0x89,0x8c,0x8f,0x92,0x95,0x98,0x9c,0x9f,0xa2,0xa5,0xa8,0xab,0xae,
    0xb0,0xb3,0xb6,0xb9,0xbc,0xbf,0xc1,0xc4,0xc7,0xc9,0xcc,0xce,0xd1,0xd3,0xd5,0xd8,
    0xda,………..(any wave form 256 samples with values 0x00 to 0xff)
    };

    and access data from flash with pgm_read_byte like

    uint16_t counter = 0;
    while(1)
    {
    counter += sk; //you should calculate step value for frequency required
    PORTA = pgm_read_byte(&sin_wave[(counter>>8)]);
    }
    }

    and google for FAST AVR DDS, it is good tutorial online and sugestions how to speed up with ASM code

    • 1) Of course you are right, the example however shows a complete sine wave – the tricky part is that the output voltage is restricted to 2^8 differentfernt values for the 8bit DAC. Sampling a complete sine is very reasonable at least at 256 values for each of the four sine quarters.
      2) Digging deeper into asm coding may speed up things. The limiting factor here just seems to be the ISR context switch and using the timer. I will give it a look someday, thanks for the hint.

Leave a Reply