Sine Wave Generator

Sine Wave Generator

Introduction

In the Music with a Piezo Element project, we created a music player that was able to play a song via a piezoelectric buzzer. We were able to play music by generating a square wave with the chipKIT™ board and piezo buzzer, all by toggling a digital output pin at the desired frequency. For this project, we will be expanding on that concept by creating a sine wave generator. Instead of rapidly toggling a digital pin high or low to generate an output signal, we will be using an external digital to analog converter (DAC for short) for more control over the signal. Additionally, we will be incorporating potentiometers for volume and frequency control of the sine wave.

Before you begin, you should:
  • Understand how a piezoelectric buzzer can be used to play a square wave.
  • Understand how a potentiometer can be used as a variable resistor.
  • Know how to read analog signals with the chipKIT board.
  • Know how to use the Serial object.
After you're done, you should:
  • Know how to use Serial Peripheral Interface (SPI) to communicate data to external ICs.
  • Know how to use external DACs.
  • Understand what a “wave table” is and how to create one.
  • Understand how to use Core Timer Service.
  • Gain an understanding of “sample rate,” “bit depth,” and how a complex waveform is constructed.

Inventory:

Qty Description Typical Image Schematic Symbol Breadboard Image
1 Piezoelectric buzzer
2 10 Ω to 10 KΩ potentiometers
1 MCP4902 Digital to Analog converter

Sampling and Basic Theory

In the Music With a Piezo Element project, we created a square waveform by toggling a digital pin on and off at the desired frequency. For that project, we did not take into consideration bit depth or sample rate because we were changing the amplitude of the waveform at exact points.

Figure 1 shows an example of that method, illustrating the waveform that would be generated by the following code. The line numbers of the code relate directly to labels in the figure.

  digitalWrite(speakerPin,HIGH);
  delay(1);
  digitalWrite(speakerPin,LOW);
  delay(1);
  digitalWrite(speakerPin,HIGH);
  delay(1);
  digitalWrite(speakerPin,LOW);
  delay(1);

Figure 1. Square wave example.

Complex waveforms (like sine waves, and especially audio from your mp3 player) have to use a more sophisticated way of reconstructing sound that involves sampling. Sampling works by updating the amplitude of an output signal at a fixed interval of time that is much faster than the signal you are trying to produce. This interval is the sample rate, which is defined as the number of samples taken per second used to reconstruct a waveform. You could think of this method as if plotting points on a graph of a waveform; with enough points you get an accurate picture of the waveform.

Figure 2 shows the sampling method of recreating a waveform (the red dots in the graph are sample points). You can see that with enough points, the same square wave is reproduced. While this may seem a bit excessive for just a square wave, you may begin to see how any waveform can be created in this manner.

Figure 2. Square wave example (sampling).

Bit depth is defined as the number of bits used to represent the amplitude at each sample point. Most often, amplitude values in digital systems are stored as integers (mainly due to hardware limitations), which means there is a discrete number of values which can be used to represent the signal. Using the graph plotting example, you can think of bit depth as the number of vertical divisions on the graph. Figure 3 shows an example of bit depth. In this example, the same continuous time waveform is represented by three signals of different bit depths. By using enough bits, a reasonable approximation of the original waveform can be achieved.

Figure 3. Bit depth example.

As a point of reference, CD quality audio is reproduced at a sample rate of 44.1 kHz with a bit depth of 16. For our project, we will be using a sample rate of 44.1 kHz but only a bit depth of 8 (which is still reasonable enough to represent our waveform).

SPI and Communicating with the DAC

The digital to analog converter (the MCP4902) for this project will communicate with our chipKIT board via the serial peripheral interface bus (or SPI for short). Essentially, when the DAC receives a data word from SPI it will set its output pin to a voltage between 0V and 5V accordingly. This will be how we create our sample points as described above. SPI is an industry standard much like UART (which is used by the serial object) for communicating between digital devices, and the chipKIT board has a library to handle SPI communication. Follow the link to the right for a more in-depth explanation of how SPI functions to transmit data.

SPI normally requires four signal pins, but for this project we will only be using three. Those signal pins are: Master out Slave in (MOSI), Slave Clock (SCK), and Chip Select (CS) (Master in Slave out (MISO) will not be used for this project). With the exception of the CS signal pin, SPI pins are hardwired on chipKIT boards and are shared with the normal digital I/O pins. For the Uno32™, MOSI is pin 11, SCK is pin 13, and MISO is pin 12. (For the other chipKIT boards, the pinout for SPI is listed in the reference manual, which can be found on the digilent website).

Basically, the serial communication with SPI is coded in MPIDE very similarly to the Serial object you have seen from other projects. For SPI you simply have to set the following two lines of code in the setup portion of your sketch to initialize communication.

SPI.begin();
SPI.setBitOrder(MSBFIRST);

The begin() method simply initializes the SPI hardware, and setBitOrder() tells the chipKIT board to either send the least or most significant bit of transmitting data first. When a byte of information is viewed in binary, the least significant bit is the least value bit (or the bit furthest to the right), and hence the most significant bit is the highest value (or furthest to the left). The MCP4902 DAC requires that data be sent with the most significant bit first.

Additionally, the MCP4902 requires that a header be sent along with the data. A header is a small amount of information preceding the transmitted data that tells the chip how to use the information. Figure 4 shows the required data format for the MCP4902. The following list describes the function of each bit field.

Figure 4. Data format for MCP4902.
  • $ \overline{A}B $: This bit signifies if data is being sent to the left or right stereo channel (for this project we will only use one channel so this bit will be permanently set to 0.)

  • $BUF:$ Signifies if the DAC will use input buffering for the Vref pin, since this does not affect our application we will leave it to the default value of 0.

  • $ \overline{GA} $: Selects between unity voltage gain and a 2x gain in the DAC. Since voltage gain is unneeded it will be set to 1.

  • $ \overline{SHDN} $: Selects if the output channel is in use or deactivated. We will set it to 1 to activate the channel.

  • Bits 11 through 4 are data bits.

  • Bits 3 through 0 are not used.

To accomplish a transmission in our sketch via SPI, we will use the following function block of code:

    
void transmitPacket(byte val){
  
  byte upper = 0b00000000;
  byte lower = 0b00000000;
  
  upper = ((val >> 4) | 0b00110000);
  lower = (val << 4);
  digitalWrite(CS,LOW);
  SPI.transfer(upper);
  SPI.transfer(lower);
  digitalWrite(CS,HIGH);
   
}

Our 16 bit data word first needs to be split into two separate bytes. This is done because the SPI.transfer() function only transmits a byte at a time, so two function calls will be needed. The data word values are split into upper and lower bytes with the >> and << operators. These operators will shift the bits in the input variable (variable to the left of the operator) by the number of places specified by the number to the right of the operator. The upper byte will contain the information for bits 15 through 8 of Fig. 4, and the lower byte will contain information for bits 7 through 0.

After the data has been shifted to the correct place, the upper byte is bitwise “ORed” with the header mask (the $ \overline{A}/B$, $\text{BUF}$,$ \overline{\text{GA}} $, and $ \overline{\text{SHDN}} $ bits).

Once the data is prepared, the CS signal is pulled low to initialize the transmission (follow the SPI link above if you want to know more about why this has to be done to initiate a transmission). Once the CS pin is low, the method SPI.transfer() is called to start transmitting data. The DAC requires that bit 15 of Fig. 4 to be received first. After both upper and lower bytes have been transmitted, the CS signal is pulled back high to acknowledge the transmission is done.

Digital to Analog Converter

The 4902 DAC is probably one of the more complicated ICs we have worked with before, so a brief description of the IC's pin layout may be necessary before constructing the circuit.

Figure 5. MCP4902 Pinout.
  • VDD and VSS: VDD is simply the positive voltage supply for our output signal (with a range from 2.7V to 5.5V), and VSS is the ground pin for the chip. For this project we will be connecting VDD to 5V rail, and VSS to the GND pins of our chipKIT board.

  • Vref (A or B): This pin sets the reference voltage for the output to either channel A or B accordingly. The voltage applied to these pins essentially sets the max voltage for each channel. For this project both pins will be tied to the 5V rail.

  • CS: Chipselect pin for SPI.

  • SDI: MOSI pin for SPI.

  • SCK: The serial clock (SCK) pin for SPI.

  • SHDN: This is a hardwired shutdown pin for the DAC. When pulled low, both output channels will always be turned off.

  • LDAC: Latch DAC input. When pulled low, a data word will set the data output pin with the corresponding value. Keeping this pin pulled low will cause the output pin of both channels to be updated simultaneously when the CS signal goes back high after a transmission is complete (which is acceptable for this project).

  • Vout (A or B): Analog output channel.

  • NC: Not connected.

Step 1: Setting up the Circuit

Now we are able to construct the circuit.

Figure 6. Circuit.
  1. Connect the 5V pin of the chipKIT board to a bus strip on the breadboard. This bus strip will be referenced as the 5V bus strip. Connect the GND pin of the chipKIT board to the bus strip next to the 5V bust strip. This bus strip will now be referenced as the Ground bus strip.
  2. Place a potentiometer in the breadboard, as displayed in Fig. 6. This potentiometer will be referred to as the volume potentiometer. It will allow you to scale the voltage driving the piezoelectric speaker, hence controlling the volume of the speaker. Connect the bottom terminal of this potentiometer to the ground bus strip.
  3. Place the 4902 DAC on the breadboard. Make sure that the IC is oriented as in Fig. 6. Connect the pins on the 4902 to the following locations (reference Fig. 5 for pin numbers).
    • Pin 1: 5V bus strip.
    • Pin 2: leave disconnected.
    • Pin 3: Connect to pin 10 of the chipKIT board.
    • Pin 4: Connect to pin 13 of the chipKIT board.
    • Pin 5: Connect to pin 11 of the chipKIT board.
    • Pins 6,7: leave disconnected.
    • Pin 14: Connect to the top terminal of the volume potentiometer.
    • Pin 13: 5V bus strip.
    • Pin 12: Ground bus strip.
    • Pin 11: VDD bus strip.
    • Pin 10: Leave disconnected.
    • Pin 9: 5V bus strip.
    • Pin 8: 5V bus strip.
  4. Place a second potentiometer in the breadboard, as displayed in Fig. 6. This will be the frequency potentiometer, allowing us to select what tone the chipKIT board will play. Connect the top terminal of this potentiometer to the 5V bus strip, and the bottom terminal to the Ground bus strip. Then connect the central terminal to pin A0 of the chipKIT board.
  5. Connect the positive terminal of the piezoelectric buzzer to the center terminal of the volume potentiometer. Then connect the negative terminal to the Ground bus strip.
Figure 4. Circuit schematic.

PDF Schematic

Before we start writing the code for this project, the concepts of wave tables and core timer services need to be discussed slightly.

Core Timer Services

A Core Timer Service is a function that will periodically execute at a nearly exact interval of time. In most microcontrollers, this type of function is called a timer interrupt. If you are unfamiliar with interrupts, they are a special type of function that is only called when an external condition (such as an input signal on an I/O pin) is met. For instance, an external interrupt could be setup to be called every time it detects a LOW to HIGH transition on an I/O pin. In the case of the Core Timer Service, the interrupt is called every time a counter on the PIC®32™ microcontroller (the processor chip on your chipKIT board) reaches a certain value. It is very important to note that when the timer service (or interrupt) is scheduled to execute, it will “pause” what the microcontroller is doing (i.e., any tasks that are running in the main loop) and run the function that the timer service was told to execute. Once that function is completed, it will return to whatever it was doing previously.

A core timer service (and timer interrupts in general) is used whenever there is a need for exact timing in a program (such as outputting sample points to our DAC). While the same task could be accomplished in the main loop of our program, it would be very impractical. We would have to balance precisely the time it would take to transmit data to the DAC (and any other tasks that need to be completed), with the correct amount of delay. A timer service is more practical, as it will execute exactly at the correct periodicity, regardless of the amount of code in the main loop. This becomes convenient if not necessary when program sketches are quite large and timing is necessary.

In order to instantiate a Core Timer Service, the following code needs to be placed in the setup function of your sketch.

attachCoreTimerService(TimerServiceFunction);

Where “TimerServiceFunction” is the name of the function, the Core Timer Service is going to call.

The function that is called by the timer service must also take an unsigned int as a parameter, as well as return one. The notation uint32_t is used to represent a 32-bit unsigned integer (since chipKIT is a 32-bit microcontroller, all “int”s are 32 bits). This is a common notation for low level programming. While not used often in MPIDE, the Core Timer Service requires it. MPIDE will throw an error if, for instance, you used an unsigned int instead of uint32_t.

uint32_t TimerServiceFunction(uint32_t currentTime) {
  // Timer interupt code

  return (currentTime + TickRate);
}

The returned value (currentTime + TickRate) is how the Core Timer Service determines when to call its function again. The “TickRate” is the interval of time between calls, so when currentTime + TickRate is returned, it represents a value of time in the future when the Core Timer Service will be called again. A Tick is equal to 25 ns (nano = $10^{-9}$). Thus, if you wanted the Core Timer Service to trigger every 50 μs, you would just need to return (currentTime + 2000), since $ \frac{50\mu s}{25 ns} = 2000 $.

For this project, we want the sample rate to be as close as possible to 44100 Hz (CD quality audio). To do this, we will need to call our timer function every 22.675 μs ($ \frac{1}{44100 \ Hz} = 22.675 \mu s $). Thus, we can calculate the number of ticks in a similar manner: $ \frac{22.675 \mu s}{25 ns} = 907 $. Setting the core timer service to interrupt every 907 ticks will give us that sample rate. The frequency has some error due to being restricted to using integer values $( \frac{1}{907 \ * \ 25 ns } = 44101.43 \ Hz ) $ (this error is called frequency quantization error), but the value is close enough for our purposes.

Wave Table

We will be pre-calculating the values of a sine wave and storing them in an array instead of calculating those values on the fly. Certain math functions (mainly involving floats or doubles) can be expensive time-wise, so an array of values is often more efficient. This array of sine wave values is called a wave table. The following lines of code will set up a wave table for the note middle “F” (or F4, this is a reference to the central F key on a piano).

int waveF [127];

for (i = 0; i < 127; i++){

  waveF[i] = 127*sin(sum);

  sum = sum + .04986655;  
}

The array length and the incrementing values can be determined by a small amount of math. Since we know that the core tick rate is 25 ns, and that the timer service function will be called every 907 ticks, the timer function’s period is 22.675 μs ($ 907 \ * \ 25 \ ns = 22.675 \mu s$) (i.e., the time between calls, or the sampling rate).

$Frequency = \frac{1}{Period}$, and we know that iterating through every value in a wave table will give us one cycle of a waveform.

It follows that:

$Frequency = \frac{1}{ Array \ length \ * \ 22.675 \ \mu s}$

Since a wave table is incremented by the Core Timer Service (every 22.675 μs), the period of the waveform is just the length of the table times 22.675 μs. Once we know the period, the frequency is found by inverting the period.

Middle F is 349.23 Hz. By rearranging the equation above, we can determine the length of the array for F4's wave table.

$Array \ length = \frac{1}{Frequency \ * \ 22.675 \mu s}$

Note again that there is a small amount of error (called frequency quantization) due to the use of integer values. ($ \frac{1}{349.23 Hz * 22.675 us} = 126.2819 $, using the rounded array length of 126, the actual frequency we will get is 350 Hz).

We now know how large a wave table should be, but we need know what to input into the sin() function for each table entry. The sine function (sin()), like all trigonometric functions, is periodic and its input parameter must be in radians (a unit of angle measurement like degrees). To convert to radians from degrees, use this equation: $ 360^{\circ} = 2\pi rad $. A full cycle can be seen by inputting values from 0 to $2\pi$ (i.e., $0^{\circ}$ to $360^{\circ}$). Since a sine function is periodic, any numbers beyond this range repeats its value. You can think of this like turning in a circle; if you turn 90 degrees to your left, or 450 degrees (i.e., making a full rotation before turning left again), you will still end up facing the same direction. Now that the array length is known, we can determine the radian incrementing value for the sin() function by simply dividing 2π by the array length.

$ \text{increment value} = \frac{2\pi}{\text{Array Length}} = 2\pi \text{Frequency} 22.675\mu s $

The sine function will return a value between -1 and 1, so it will be scaled by 127 (so the overall value stored will be from -127 to 127, a total span of 255 (i.e., 127 + 127 + 1 = 255, remember to count zero). Just before we transmit a value, the wave table will be offset by 127 so that the value ranges from 0 to 254 (as well as cast to a byte data type).

Step 2: Setting up the Code

The following is the code to implement the sine wave generator.

#include ‹SPI.h›


const int sampleTickRate = 907;

int waveF [127];//F4
int waveG [113];//G4
int waveA [101];//A5
int waveB [90]; //B5
int waveC [85]; //C5
int waveD [76]; //D5
int waveE [68]; //E5

unsigned int t;
int freq;
float sum;
int i;


void setup() {
  pinMode(9, OUTPUT);

	
  digitalWrite(9,HIGH); 
	
  i = 0;
  t = 0;
	
  sum = 0;
  freq = 0;
	

  // F4
  for (i = 0; i < 127; i++){
    waveF[i] = 127 * sin(sum);
    sum = sum + .04986655;  
  }
		
		
  // G4
  for (i = 0; i < 113; i++){
    waveG[i] = 127 * sin(sum);
    sum = sum + .0560998;  
  } 
		
		
  // A5 
  for (i = 0; i < 101; i++){
    waveA[i] = 127 * sin(sum);
    sum = sum + .062831853;
  }
		
		
  // B5
  for (i = 0; i < 90; i++){
    waveB[i] = 127 * sin(sum);
    sum = sum + .0708975877;  
  } 
		
  // C5
  for (i = 0; i < 85; i++){
    waveC[i] = 127 * sin(sum);
    sum = sum + .0747998;  
  } 
		 
  // D5
  for (i = 0; i < 76; i++){
    waveD[i] = 127 * sin(sum);
    sum = sum + .0837758;  
  } 
		 
  // E5
  for (i = 0; i < 68; i++){
    waveE[i] = 127 * sin(sum);
    sum = sum + .093778885;  
  } 

  SPI.begin() ;
  SPI.setBitOrder(MSBFIRST);
	
  attachCoreTimerService(Timer);
}


void transmittPacket(byte val){
  byte upper = 0b00000000;
  byte lower = 0b00000000;
  
  upper = ((val >> 4) | 0b00110000);
  lower = (val << 4);
  digitalWrite(9,LOW);
  SPI.transfer(upper);
  SPI.transfer(lower);
  digitalWrite(9,HIGH);  
}


void loop() {
  /* Reads the Frequency potentiometer and then selects one of 7
  * frequencies accordingly.
  */
  
  int temp = analogRead(0);
  
  /*The map(input, old low range, old high range, new low range, new high range) function will scale a input variable from its original range to a new range.
  * This allows the chipKIT board to scale the analog input (which has  a range from 0 to 1024) to a range of 0 to 6
  * (to select one of the seven available frequencies).
  */
  
  freq = map(temp ,0 ,1000,0,6);
  
  delay(100);
}



uint32_t Timer(uint32_t currentTime) {
  byte tempByte;
  int tempVal;
  
  /* Case statement selects which frequency to output. The freq variable is determined in the main loop by reading the value
  *  from the frequency potentiometer, and then mapping it to one of 7 values.
  */
  
  switch (freq){
	
	
	/*All of the wave tables are incremented by (t % array length). If you are unfamiliar with the modulus operator,
	* it simply outputs the remainder of a division operation. This is done so that no array can ever be overrun.
	*/
	
    case 0:
      tempVal =  (waveF[(t % 126)] + 127);
      break;
		    
    case 1:
      tempVal =  (waveG[(t % 112)] + 127);
      break;
		    
    case 2:
      tempVal =  (waveA[(t % 100)] + 127);
      break;
		    
    case 3:
      tempVal =  (waveB[(t % 89)] + 127);
      break;
		    
    case 4:
      tempVal =  (waveC[(t % 84)] + 127);
      break;
		    
    case 5:
      tempVal =  (waveD[(t % 75)] + 127);
      break;
      
    default:
      tempVal =  (waveE[(t % 67)] + 127);    
      break;
		    
  }
  
  //constrain(input, low range, high range) sets a max and min bound for the input variable.
  
  tempVal = constrain(tempVal,0, 255);
    
  tempByte = byte(tempVal);
    
  transmittPacket(tempByte); 
    
  t++;
  
  return (currentTime + sampleTickRate);
}

Once uploaded to the chipKIT board, the DAC should start playing a single note depending on the position of the frequency potentiometer (if you do not hear anything, adjust the volume potentiometer). The sound produced from the piezoelectric buzzer won't be very loud (this is because the DAC is limited in the amount of power it can deliver to the buzzer, which is a topic best left for another project), but if it is working correctly, then turning the frequency potentiometer should change and select one of the seven frequencies available.

Test Your Knowledge!

Now that you've completed this project, you should be able to:

  • Calculate the wave table for F5.
  • Shift the frequency up or down an octave (remember two notes an octave apart are only doubled/halved in frequency).
  • Set up a stereo sine wave generator (given that you have another buzzer available).
  • Given superposition (all waves can be summed together to form more complex sounds), change the code so that the sine wave generator can play a triad chord. A triad chord in music refers to three notes played at the same time; the root note (the first), then a note two notes higher (the third), and then a note four notes higher (the fifth). An example of an A major triad would be A5, C#5, and E5.

It is important when taking the sum of waves that the values can be both positive and negative. Waves naturally reinforce and diminish themselves when combined (without negative values the waves would only reinforce). The offset used in this project is mainly because of the DAC, as it can only accept values from 0 to 255.

Given that any sound that you hear is really a combination of many simple waveforms, can you start to see the leap between playing sine waves and playing a complex waveform like a clip of audio? In this project, we used a wave table to recreate the points in our audio waveform, wouldn't it be feasible to simple play back pre-recorded points from a clip of audio in the same fashion?


  • Other product and company names mentioned herein are trademarks or trade names of their respective companies. © 2014 Digilent Inc. All rights reserved.
  • Circuit and breadboard images were created using Fritzing.