Arduino Controlled AD9833 Function Generator With Variable Gain

by kevinjpower in Circuits > Arduino

9969 Views, 2 Favorites, 0 Comments

Arduino Controlled AD9833 Function Generator With Variable Gain

CoverPicture.png

Use the AD9833 and Arduino to make a versatile function generator. As a added bonus the combination of a MCP601 op amp and an MCP4131 digital pot allow for variable gain controlled from the Arduino

Supplies

  1. Arduino Uno
  2. AD9833 DDS Programmable Function Generator Module
  3. MCP601 Operational Amplifier
  4. MCP4131 SPI Digital POT
  5. 1 kOhm resistor
  6. Breadboard and wires

Back Story

Ever wondered how to create a simple and low cost function generator using an Arduino? Such a circuit would be very useful for many electronic experiments. It can be done by using an AD9833 function generator IC connected to an Arduino

The function generator described in this project also has a digitally controlled gain, allowing the Arduino to control the amplitude of the output waveforms

Lets make some waves!

AD9833 DDS Function Generator Module

IMG_3704.JPG
IMG_3705.JPG

The AD9833 is a low power, micro controller programmable function generator capable of producing sine, triangular, and square wave outputs.

The output frequency and phase are software programmable, allowing for easy tuning.

The specific board used in this project can be purchased on eBay, AliExpress, Amazon and other sites. Prices range from $5 to $10 excluding shipping.

Picture shows the specific version used in this project. It comes with a header which is not attached to the board. As a second step in this project, solder the header into the board to achieve the completed result as shown

A few more items to note regarding the AD9833:

  • The max amplitude output when generating a Sine or Triangle wave is 650 mV with a minimum of 38 mV. More on this limitation later. The square wave output is rail to rail
  • The max Vdd is 5 V. This suits the 5V power supply available from the Arduino
  • The AD9833 uses a SPI communication protocol to write to the data register that controls the output. Later in this project, the Arduino sketch will demonstrate how this works

For full details of how the AD9833 works, see the AD datasheet

https://www.analog.com/en/products/ad9833.html

MCP4131 Digitally Controlled Potentiometer

MCPBlockDiagram.png

The MCP41XX family of integrated circuits are manufactured by Microchip and represent a range of digitally controlled potentiometers. The family of devices support 7-bit and 8-bit resistor networks using an SPI interface to control resistance.

A block diagram from the MCP data sheet included

The device can be used as two terminal variable resistor or a three terminal potentiomenter.

Control of the wiper position and hence the resistance is determined by the three digital inputs:

  • Device is selected by taking CS low.
  • Using a SPI serial interface (10 MHz, modes 0,0 & 1,1), high-speed Read/Writes are written to potentiometer registers. SCK synchronizes the clock between devices
  • The wiper moves to the position determined by the value written to the register.
  • The wiper has 128 (in the case of MCP4131) possible positions (0 – 127), allowing for incremental steps of about 0.8% of the total resistance.
  • The current wiper position can be read from the MCP4131 via the SPI interface

Complete specifications for the MCP4131 can be found at the Microchip website.

https://www.microchip.com/en-us/product/MCP4131

https://ww1.microchip.com/downloads/en/DeviceDoc/22060b.pdf

Downloads

Non Inverting Amplifier

opamp-opamp61.gif

As explained earlier, the AD9833 produces a max output of 650 mV, which cannot be varied.

This limitation could be restrictive for applications or experiments, so it would be useful to amplify this voltage to larger values. This project uses an non-inverting op amp circuit to achieve this. A standard non-inverting op amp configuration as shown.

The gain of this configuration is G = 1 + Rf/Rin. There are numerous articles explaining how the configuration works and derivations of the gain function for those who need more background.

https://www.electronics-tutorials.ws/opamp/opamp_3.html


If Rf is made variable, then the gain can be varied to produce a output voltage with a required value that suits the application or experiment. This project inserts the MCP4131 103 as the feedback resistor (Rf) in a two terminal variable resistor configuration. It is controlled by the Arduino to give the required variable output.

For the specific configuration used in this project, max Rf = 6 kOhm , Rin = 1 kOhm to give a gain of 7.

This gain gives an output voltage of 7 x 650 mV = 4.55 V. This will not saturate the output of op amp.

The Circuit

CircuitDiagram.png

See the schematic of the circuit used in this project:

Here are some features of the circuit:

  • A MCP601 single rail power supply operational amplifier is used in the circuit. This can be substituted with other similar IC’s
  • The op amp is set up as a non inverting amplifier; input signal from the AD9833 is connected to the non inverting input (pin 3). It also acts as a buffer between the AD9833 and whatever load is attached, providing a low impedance output.
  • Rin is a fixed 1 kOhm resistor connected between inverting input and ground
  • Rf is the MCP4131 configured as a two terminal variable resistor. The resistance can be digitally varied by the Arduino to give resistances between 1 kOhm and 6 kOhm. This corresponds to a gain of 2 to 7
  • SPI communication with the MCP4131 uses Arduino pin 13(SCK), pin 11 (SDI), and pin 10 (CS). This is configured in the Arduino sketch that contols the circuit
  • SPI communication with the AD9833 uses Arduino pin 4 (DATA), pin 5 (CLK) and pin 6 (FSYNC). This is also configured in the Arduino sketch
  • The circuit can be powered from the Arduino 5V supply

Building the Circuit

CircuitPicture.png

Using a breadboard and connectors, build the circuit as shown in the picture

Load the Code

SerialMenu.png
LibraryAD9833.png

Before compiling the sketch, you need to install the AD9833 library. In the Arduino IDE, click on the library manager icon and search for AD9833. The library required is MD_AD9833 by majicDesigns (this is for IDE version 2.0.3. Screenshot will be different for previous IDE versions).

The library provides access to the AD9833 chip features. More info on the library along with documentation can be found at:

https://github.com/MajicDesigns/MD_AD9833

The sketch used in this project is based on an example included with the library

Compile and upload the sketch to the Arduino. Once the sketch is loaded and the program initializes, a menu is displayed on the serial monitor

Commands are entered in the format :XXXXX; for example, to set the frequency of channel 0 to 2000 Hz, use the following command - :f12000;

To set the gain to 4 enter following command - :ga4;

It is important to include the : and ; with all commands.

/*
Program to control AD9833 and MCP4131
Based on example provided with AD9833 library
AD9833 connected to pins 5,6,7
MCP4131 connected to pins 10,11,13
Menu for controlling AD9833 and gain available on Serial Monitor
*/

#include <SPI.h>
#include <MD_AD9833.h>
#include <MCP4131.h>


#define DATA 6 ///< SPI Data pin number
#define CLK 7 ///< SPI Clock pin number
#define FSYNC 5 ///< SPI Load pin number (FNC on AD9833 breakout)


long frequency;
const int chipSelect = 10; // Define chipselect pin for MCP4131
unsigned int wiperValue; // variable to hold wipervalue for MCP4131


MD_AD9833 AD(DATA, CLK, FSYNC); // Arbitrary SPI pins
MCP4131 Potentiometer(chipSelect);


// Character constants for commands
const char CMD_HELP = '?';
const char BLANK = ' ';
const char PACKET_START = ':';
const char PACKET_END = ';';
const char CMD_FREQ = 'F';
const char CMD_PHASE = 'P';
const char CMD_OUTPUT = 'O';
const char CMD_GAIN = 'G';
const char OPT_FREQ = 'F';
const char OPT_PHASE = 'P';
const char OPT_SIGNAL = 'S';
const char OPT_1 = '1';
const char OPT_2 = '2';
const char OPT_GAIN = 'A';
const uint8_t PACKET_SIZE = 20;


void setup()
{
Serial.begin(57600);
AD.begin();
usage();
wiperValue = 64;
Potentiometer.writeWiper(wiperValue); // Set MCP4131 to mid position
}


// Set up menu that displays on Serial Monitor
void usage(void)
{
Serial.print(F("\n[MD_AD9833_Tester]"));
Serial.print(F("\n?\thelp - Type ?"));
Serial.print(F("\n\n:<cmd><opt> <param>;"));
Serial.print(F("\n:f1n;\tset frequency 1 to n Hz"));
Serial.print(F("\n:f2n;\tset frequency 2 to n Hz"));
Serial.print(F("\n:p1n;\tset phase 1 to n in tenths of a degree (1201 is 120.1 deg)"));
Serial.print(F("\n:p2n;\tset phase 2 to n in tenths of a degree (1201 is 120.1 deg)"));
Serial.print(F("\n:ofn;\toutput frequency n [n=1/2]"));
Serial.print(F("\n:opn;\toutput phase n [n=1/2]"));
Serial.print(F("\n:osn;\toutput signal type n [n=(o)ff/(s)ine/(t)riangle/s(q)uare]"));
Serial.print(F("\n:gan;\tset gain n 1 to 6"));
}


// Convert to ASCII
uint8_t htoi(char c)
{
c = toupper(c);


if (c >= '0' && c <= '9')
return(c - '0');
else if (c >= 'A' && c <= 'F')
return(c - 'A' + 10);
else
return(0);
}


char nextChar(void)
// Read the next character from the serial input stream
// Blocking wait
{
while (!Serial.available())
; /* do nothing */
return(toupper(Serial.read()));
}


char *readPacket(void)
// read a packet and return the contents
{
static enum { S_IDLE, S_READ_CMD, S_READ_MOD, S_READ_PKT } state = S_IDLE;
static char cBuf[PACKET_SIZE + 1];
static char *cp;
char c;


switch (state)
{
case S_IDLE: // waiting for packet start
c = nextChar();
if (c == CMD_HELP)
{
usage();
break;
}
if (c == PACKET_START)
{
cp = cBuf;
state = S_READ_CMD;
}
break;


case S_READ_CMD: // waiting for command char
c = nextChar();
if (c == CMD_FREQ || c == CMD_PHASE || c == CMD_OUTPUT || c == CMD_GAIN)
{
*cp++ = c;
state = S_READ_MOD;
}
else
state = S_IDLE;
break;


case S_READ_MOD: // Waiting for command modifier
c = nextChar();
if (c == OPT_FREQ || c == OPT_PHASE || c == OPT_SIGNAL ||
c == OPT_1 || c == OPT_2 || c == OPT_GAIN)
{
*cp++ = c;
state = S_READ_PKT;
}
else
state = S_IDLE;
break;


case S_READ_PKT: // Reading parameter until packet end
c = nextChar();
if (c == PACKET_END)
{
*cp = '\0';
state = S_IDLE;
return(cBuf);
}
*cp++ = c;
break;


default:
state = S_IDLE;
break;
}


return(NULL);
}


void processPacket(char *cp)
// Assume we have a correctly formed packet from the parsing in readPacket()
// Depending on input from Serial Monitor, send commands to AD9833 and MCP4131
{
uint32_t ul; // Frequency Value
uint32_t wV; // Gain Value
MD_AD9833::channel_t chan;
MD_AD9833::mode_t mode;
switch (*cp++)
{
case CMD_FREQ:
switch (*cp++)
{
case OPT_1: chan = MD_AD9833::CHAN_0; break;
case OPT_2: chan = MD_AD9833::CHAN_1; break;
case OPT_GAIN: break;
}


ul = strtoul(cp, NULL, 10);
// Serial.println(ul); for debug purposes
AD.setFrequency(chan, ul);
break;


case CMD_PHASE:
switch (*cp++)
{
case OPT_1: chan = MD_AD9833::CHAN_0; break;
case OPT_2: chan = MD_AD9833::CHAN_1; break;
}


ul = strtoul(cp, NULL, 10);
AD.setPhase(chan, (uint16_t)ul);
break;


case CMD_GAIN:
*cp++;
wV = strtoul(cp, NULL, 10);
// Serial.println(wV); for bebug purposes
wV = map(wV, 1, 5, 12, 65);
// Serial.println(wV); for debug purposes
Potentiometer.writeWiper(wV);
break;


case CMD_OUTPUT:
switch (*cp++)
{
case OPT_FREQ:
switch (*cp)
{
case OPT_1: chan = MD_AD9833::CHAN_0; break;
case OPT_2: chan = MD_AD9833::CHAN_1; break;
}
AD.setActiveFrequency(chan);
break;


case OPT_PHASE:
switch (*cp)
{
case OPT_1: chan = MD_AD9833::CHAN_0; break;
case OPT_2: chan = MD_AD9833::CHAN_1; break;
}
AD.setActivePhase(chan);
break;


case OPT_SIGNAL:
switch (*cp)
{
case 'O': mode = MD_AD9833::MODE_OFF; break;
case 'S': mode = MD_AD9833::MODE_SINE; break;
case 'T': mode = MD_AD9833::MODE_TRIANGLE; break;
case 'Q': mode = MD_AD9833::MODE_SQUARE1; break;
}
AD.setMode(mode);
break;

}
break;
}


return;
}

void loop()
{
char *cp;


if ((cp = readPacket()) != NULL)
processPacket(cp);
}


Downloads

Results

Gain5Freq2.bmp
Gain2Freq2.bmp

Here are two screenshots captured from the output.

Frequency = 2 kHz and gain = 5

Frequency = 2000 Hz and Gain = 2

All op amps have a reduced open loop gain as frequency increases. This is captured in a parameter called “Gain Bandwidth Product” which is quoted on all op amp spec sheets.

Refer to the following Wikipedia article for a full explanation

https://en.wikipedia.org/wiki/Gain%E2%80%93bandwidth_product

Because of this limitation, the MCP601 op amp used in this circuit will have a reduced gain as the frequency gets higher and it will be impossible to output a waveform at 4.55 V with the max gain of 7.

The gain bandwidth product of the MCP601 is quoted as 2.4 MHz. This means that a gain of 7 is possible up to 2400/7 = 342 kHz. At frequencies above this gain will decrease until no more output is possible.

All things considered, for a total cost of approx $20, a very good result and useful function generator.