Arduino Sinewave Generator

This project describes how to use an I2C DAC and an Arduino Uno to make a sinewave.

Project description

Overview:

I was reading through the Arduino Forum one day and found a young man who wanted to know how you could use the Arduino to make a sine wave generator. Apparently, he was an engineering student and this was his assignment He was told by some it was not possible, others said it was, but only as fast as 50 Hz. Since our company makes an I2C DAC dev board for the Arduino I thought it would be educational (for me) to make a sine wave generator using the DAC board.

About this Project

A signal generator usually has various signals that is can generate, such as Sine, Square and triangle. Others have a sweep function and an arbitrary waveform. These are useful tools in the workshop. They can be used to test out audio circuits, op amp circuits and testing circuit response. Most modern function generators can easily put out frequencies up to 1 Mhz.

So, while I did not expect an Arduino based sine wave generator to replace my desktop function generator, I thought it would be interesting to see how to go about designing one and how it would perform.

DAC PCB, Arduino and Solderless breadboard

Lookup table

I decided the easiest way to start would be to create a lookup table of values to be used in my sinewave output. The more values you have in the table the better the output will approximate a true sinewave. It is also very convenient to use powers of 2 when creating a lookup table that you are going to cycle through repeatedly. So, 8 values were not going to work and 128 was probably going to tax the ability of the MCU. I decided that 32 values would be a good place to start.

Next, I needed to decide how much resolution to provide. The SF-5 is based on the MCP4725 DAC which is a 12-bit device. (Note: You can purchase the SF-5 on Tindie, or you can buy the DIP package of the device and put it directly onto your solderless breadboard). So, 12 bits it was. I opened an Excel spreadsheet and wrote down the numbers from 0 to 31. The next column I needed was the angle in radians. That is just the index times 2 times Pi divided by 32. The next column is the normalized amplitude of the signal. I simply took the sine of the angle in the previous column. This created a signal that was 2 units peak to peak and centered on zero. Its maximum was +1 and minimum was -1.

Having a 0 to 5V output range I would need a signal that was centered on 2.5V and had an amplitude of ± 2.5V. Next column just multiplied the signal by 2.5. Then we offset it by 2.5V. The signal showed a maximum of 5V and a minimum of 0V. Perfect!

Now we just needed to convert that into a 12-bit number to put in our table. Since we wanted the value to be 4095 when the voltage was 5V, we multiply by 4095 and divide by 5. To check, look at line 8. The voltage output is 5V and the bit count is 4095.

The table

32 2-byte numbers is not a lot of data and could easily be stored in RAM, but this was a learning experience and I wanted to learn how to store the table in FLASH and read it as needed. The advantage is that one day I will have too much data to put in RAM and will need to use the FLASH, so this is a good time to learn. The command is PROGMEM and the statement that stored the data is:

const PROGMEM unsigned int mysine [] = {TABLE}

The code

I wrote a simple looping program that looked for user input on the serial line. In my case when the number 6 was seen, it would jump into the code that dumped the table to the DAC over the I2C bus. Having never used FLASH directly before I first thought I could just index my array and read the data. RTFM – Read the Fine Manual. When reading from FLASH you use a different command –

temp = pgm_read_word_near(mysine + i);

This reads a 16-bit word, or one entry, from the array. I start with i=0 and increase until it is 31 and then set it back to zero again (i = I & 0x1F;). It is nice that we can read from the FLASH in words, but we can only write to the DAC in bytes. We need to do some editing on our data.

The MSB is sent first. The top two bits are the speed, the next two bits are the power down select mode, the next 4 bits are the data. That gets sent off fist using the Wire.write command. Next, we send the lower 8 bits of data, also using the Wire.write command.

How about frequency?

When you go into a loop and just continuously send out the 32 table entries, this is the fastest you can go and the highest frequency you will get. The frequency I get is 92Hz. I then wanted to adjust the frequency and I decided to add a delay () after each table entry. That will be 32ms (roughly) per cycle. So, subsequent frequencies are 23Hz, 13Hz, 9Hz. You can go up to a delay of 256. I think that gave me a period of 8 seconds.

Raw output from DAC

What a terrible output!

Looking at the picture of the raw output you might thing, “What a terrible looking output.” It has a stairstep pattern. That is because those are the 32 voltages we are putting out of the DAC. We could add more entries to the table to make the stairstep pattern become smoother, but that will decrease the maximum frequency. The other thing we can do is to filter out the high frequencies that are making those sharp corners in the waveform when we change voltage levels. I used a simple RC low pass filter; R = 10K, C = 0.1uF for a cutoff frequency of about 160Hz. As you can see it looks much better and is a better approximation of a true sinewave.

Filtered Output

Improving user interface

I allow the delay to be modified by the user from their keyboard by increasing the delay by one unit when a ‘+’ character is received and decreasing the delay when a ‘-‘ character is received.

I also don’t want to lock the user into an endless loop, so I check to see if the user sends down an ‘e’ or an ‘E’ (because nothing is more annoying than having to worry about case sensitivity). When I get that, I treat it as an exit request and jump back to the main loop. I do so a bit inelegantly, without regard for where I leave the voltage output. But remember, we are just having fun here. If this were a product, I would want to leave the output in a known state, probably 0 volts.

This is the main Menu (Numbers should be sequential but didn’t copy well)

Improvements

I used a simple user interface that anyone could write on the Arduino. I am sure you can use Python or something on your PC to make a really cool interface. I also did not have a specific reason for doing this, so if you have a project with real requirements, you will likely change the code to suit.

Demo

I made a demo video that includes all the options in the menu including the Sinewave output (6) and put it on Youtube. If you skip to the end you can see the demo of the sinewave:

Code

SF-5 DAC Example Arduino Code

arduino

This code will output the sinewave discussed, but it also reads and writes to the registers of the MSP4725.

// Written by Celtic Engineering Solutions LLC 2021

// SF5.ino  is free software: you can redistribute it and/or modify
// it under the terms  of the GNU General Public License as published by
// the Free Software Foundation,  either version 3 of the License, or
// any later version.

// SF5.ino is  distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY;  without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR  PURPOSE.  See the
// GNU General Public License for more details.

// You  should have received a copy of the GNU General Public License
// along with SF5.ino.  If not, see http://www.gnu.org/licenses/.

// Title: SF5.ino
// Author:  The Celtic Engineer
// Description: This is example code of a polled interface  for the
// SF-5 Digital to Analog Converter board
// Revision History: (Created  Monday June 28 13:08 2021)

#include<Wire.h>
#include<avr/pgmspace.h>

//  Definitions
#define addr 0b1100011  // Default device address (pin is pulled  up to VDD). YOU WILL HAVE TO CHANGE THIS IF YOU CHANGE THE JUMPER.

// REGISTERS
#define  Conver_reg      0X00 // Conversion Register (read only)
#define Config_reg      0x01  // Configuration Register
#define Low_lim_reg     0X02 // Low Threshold Register
#define  High_lim_reg    0X03 // High Threshold Register

// COMMAND TYPE
#define  FM      0x00      // Fast Mode
#define LD      0x02      // Load DAC
#define  DaEP    0x03      // Load DAC and EEPROM

//Variables
unsigned char incomingByte  = 0;           // Byte from user
unsigned char DataH = 0;                  //  High DAC byte
unsigned char DataL = 0;                  // Low DAC byte
unsigned  char Cmd = 0x00;                 // Command Byte default is normal
unsigned char  PDM = 0;                    // Power Down Mode
unsigned char Array[6] = {0, 0,  0, 0, 0}; // Input value
unsigned char i = 0;                      // Counter
unsigned  char val = 0;                    // 8 bit character scratchpad
unsigned int temp  = 0;                    // 16 bit integer scratchpad
unsigned char wait = 0;                   // Trap
unsigned char x = 0;                      // delays  for sine wave
const PROGMEM unsigned int mysine[] = {
  // Save this to FLASH.  It is not really necessary because it is a small amount
  // of data, but an  interesting exercise in case you want to use larger data blocks.
  2048,
  2447,
  2831,
  3185,
  3495,
  3750,
  3939,
  4056,
  4095,
  4056,
  3939,
  3750,
  3495,
  3185,
  2831,
  2447,
  2048,
  1648,
  1264,
  910,
  600,
  345,
  156,
  39,
  0,
  39,
  156,
  345,
  600,
  910,
  1264,
  1648
};

void  setup() {
  // This is your initialization code
  Wire.begin(); // Start I2C  Bus as Master
  Serial.begin(9600);
  banner();     // This is part of your  user interface
}

void loop() {
  // This is the main loop of the program

  if (Serial.available() > 0)           // Wait for a command from user to do something.
  {
    // Read the incoming byte from User:
    incomingByte = Serial.read();

    //====================
    if (incomingByte == '1')            // Read DAC  Registers
    {
      incomingByte = 0;                 // Clear incomming  byte

      Wire.requestFrom(addr, 5, 1);
      i = 0;
      while (Wire.available())          // Keep doing this as long as there is data avialable
      {
        i++;                            // Incrament the counter
        Array[i] = Wire.read();         // Get a byte
      }
      Serial.print("\
 This is the Command  Byte: "); Serial.print(Array[1], HEX);
      Serial.print("\
 DAC Reg MSB  Byte: "); Serial.print(Array[2], HEX);
      Serial.print("\
 DAC REG LSB  Byte: "); Serial.print(Array[3], HEX);
      Serial.print("\
 EEPROM Power  down + MSB Byte: "); Serial.print(Array[4], HEX);
      Serial.print("\
 EEPROM  LSB Byte: "); Serial.println(Array[5], HEX);
      banner();
    }

    //====================
    if (incomingByte == '2')            // Power Down  Select
    {
      incomingByte = 0;                 // Clear incomming byte

      // Sub-Menu Power Down Mode Select
      Serial.print("\
Select the power  down mode\
");
      Serial.print("1 - Normal Mode (0)\
");
      Serial.print("2  - 1K Resistor to GND (1)\
");
      Serial.print("3 - 100K Resistor to GND  (2)\
");
      Serial.print("4 - 500K Resistor to GND (3)\
");
      wait  = 1;   // Set up trap
      PDM = 0;    // Clear out old power down mode
      while (wait) {    // Hang out until user responds
        if (Serial.available()  > 0)      // Wait for a command from user to do something.
        {
          //  Read the incoming byte from User:
          incomingByte = Serial.read();
          if ((incomingByte > 47) && (incomingByte < 53)) {// Make sure it is a  number
            wait = 0;
          }
        }
      }
      switch  (incomingByte) {// Set variable based on user selection
        case 49:
          PDM = 0x00;
          break;
        case 50:
          PDM =  0x01;
          break;
        case 51:
          PDM = 0x02;
          break;
        case 52:
          PDM = 0x03;
          break;
      }
      incomingByte  = 0;
      Serial.print("\
PDM mode: ");
      Serial.print(PDM);
      Serial.print("\
");
      banner();
    }

    //====================
    if (incomingByte  == '3')            // Command Mode Select
    {
      incomingByte = 0;                 //  Clear incomming byte

      // Sub-Menu Command Mode Select
      Serial.print("\
Select  the Command Mode\
");
      Serial.print("1 - Fast (0)\
");
      Serial.print("2  - Write to DAC (2)\
");
      Serial.print("3 - Write to DAC and EEPROM (3)\
");

      wait = 1;                       // Set up trap
      Cmd = 0;                        //  Clear out old power down mode
      while (wait) {                  // Hang out  until user responds
        if (Serial.available() > 0)   // Wait for a command  from user to do something.
        {
          // Read the incoming byte from  User:
          incomingByte = Serial.read();
          if ((incomingByte  > 48) && (incomingByte < 52)) {// Only accept 1 , 2 or 3
            wait = 0;
          }
        }
      }
      switch (incomingByte) {// Set variable  base on user selection
        case 49:
          Cmd = 0x00;
          break;
        case 50:
          Cmd = 0x02;
          break;
        case 51:
          Cmd = 0x03;
          break;
      }
      Serial.print("\
CMD  mode: ");
      Serial.print(Cmd);
      Serial.print("\
");
      banner();
    }

    //====================
    if (incomingByte == '4')        //  Get value to write
    {
      incomingByte = 0;             // Clear incomming  byte

      // Get value to write
      Serial.print("\
Enter a value  between 0 and 4095\
");
      Serial.print("You must use leading zeros\
");
      Serial.print("Value: ");

      wait = 1;
      i = 0;
      while  (wait) {
        if (Serial.available() > 0)      // Wait for a command from  user to do something.
        {
          // Read the incoming byte from User:
          incomingByte = Serial.read();
          if ((incomingByte) != 10) {
            i++;
            Array[i] = (incomingByte - 48);
            if  (i >= 4) {
              wait = 0;
            }
          }
        }
      }

      // Assemble value
      temp = 0;
      temp = 1000 *  Array[1] + 100 * Array[2] + 10 * Array[3] + Array[4];
      if (temp > 4095)  {
        Serial.print("\
Error: Value too large -> ");
        Serial.print(temp);
      }
      else {  // Format data for a Normal write
        DataL = ((temp  << 4) & 0x00F0);   // Only want the bottom 4 bits, but left justified
        DataH  = ((temp >> 4) & 0x00FF);   // Top 8 bits of integer moved to a character
      }
      Serial.print("\
");
      Serial.print(temp);
      Serial.print("\
");
      banner();
    }

    //====================
    if (incomingByte  == '5')            // Execute a write
    {
      incomingByte = 0;                 //  Clear incomming byte

      if (Cmd & 0x06)   // Reg or Reg and EEPROM
      {
        // Normal data 3 bytes
        val = (((Cmd << 5) & 0xE0)  | ((PDM < 1) & 0x06)); // C2 C1 C0 X X PD1 PD0 X
        Wire.beginTransmission(addr);
        Wire.write(val);                // Command and power down
        Wire.write(DataH);              // MSB Data out
        Wire.write(DataL);              // LSB  Data out
        Wire.endTransmission();

        Serial.print("\
 MSB:  "); Serial.print(DataH, HEX);
        Serial.print("\
 LSB: "); Serial.println(DataL,  HEX);
      }

      else              // Fast Mode
      {
        //  Compressed data to 2 bytes
        val = (((PDM << 4) & 0x30) | (( DataH >> 4)  & 0x0F));  // 0 0 PD1 PD0 D11 D10 D9 D8
        Wire.beginTransmission(addr);
        Wire.write(val);
        val = (((DataH << 4) & 0xF0) | ((DataL >> 4)  & 0x0F)); // Low 8 bits of data
        Wire.write(val);
        Wire.endTransmission();
        Serial.print("\
Fast MSB: "); Serial.print(DataH, HEX);
        Serial.print("\
  LSB: "); Serial.println(DataL, HEX);
      }
      banner();
    }

    //====================
    if (incomingByte == '6')        // Sine wave example
    {
      /*  This outputs a sine wave based on the Table of 32 steps saved  to FLASH
       *   It is best to look at on an o-scope.
       *   The output  is a bit ragged, but you can improve the look by adding an RC
       *   fileter.  Place a resistor (10K - 1.5M) and a Cap (0.1uF) depending
       *   on your  frequency. A 0-Delay will output a sine wave of 92Hz, while 
       *   a 255-Delay  will output a sine wave of 0.122Hz (8.2 second period).
       */
      incomingByte  = 0;             // Clear incomming byte
      Serial.print("\
Outputs a sine  wave\
");

      Serial.print("Enter the 'e' key to exit\
");
      Serial.print("+  to increase period and - to decrease period\
");
      Serial.print("Delay  time will be printed to Serial Monitor \
");
      Serial.print("Outputing  wave now\
");

      wait = 1; // Set trap
      i = 0;

      while  (wait)
      {
        if (Serial.available() > 0)
        {
          //  Read the incoming byte from User: Should we leave or should we stay?
          incomingByte  = Serial.read();
          if ((incomingByte == 'e') | (incomingByte == 'E'))  {
            wait = 0;
          }
          if (incomingByte == 43) {  // increment
            x++;
            Serial.println(x);
          }
          else if (incomingByte == 45) {  // decrement
            x--;
            Serial.println(x);
          }
          incomingByte = 0;             // Clear incomming byte
        }
        // Crude way of waiting. Assumes you don't want to do anything  else
        delay(x);

        // Get next voltage to send out
        temp  = pgm_read_word_near(mysine + i);
        //temp = (mysine[i]);
        DataL  = ((temp << 4) & 0x00F0);   // Only want the bottom 4 bits, but left justified
        DataH = ((temp >> 4) & 0x00FF);   // Top 8 bits of integer moved to a character

        val = (((PDM << 4) & 0x30) | (( DataH >> 4) & 0x0F));  // 0 0 PD1 PD0 D11  D10 D9 D8
        Wire.beginTransmission(addr);
        Wire.write(val);
        val = (((DataH << 4) & 0xF0) | ((DataL >> 4) & 0x0F)); // Low 8 bits of  data
        Wire.write(val);
        Wire.endTransmission();

        //  Update counter
        i++;                    // incrament couter
        i  = i & 0x1F;           // count to 31 then go back to zero
      }
      banner();
    }

    //====================
    if ((incomingByte == 'H') | (incomingByte  == 'h')) // Print Banner
    {
      incomingByte = 0;             // Clear  incomming byte

      Serial.print("\
");
      banner();                     //  This is your User Interface - Menu
    }
  }
}

void banner() {
  // This is your User Interface - Menu
  Serial.print ("\
Digital to Analog  Converter SF-5: Main Menu\
\
");
  Serial.print ("1. Read DAC Registers\
");
  Serial.print ("2. Power Down Select\
");
  Serial.print ("3. Command Mode  Select\
");
  Serial.print ("4. Get value to write\
");
  Serial.print  ("5. Execute a write\
");
  Serial.print ("6. Sine wave output\
");
  Serial.print ("H. Help Menu\
");
}

Downloadable files

Schematic Diagram

This shows how to connect the SF-5 to the Arduino Uno

https://projects.arduinocontent.cc/f4b79efa-173e-42eb-8c33-f08b9615ef7e.pdf

Similar Posts

Leave a Reply