Search Microcontrollers

Thursday, November 1, 2012

MSP430G2 - ADC / 2 - sampling an audio signal

I thought it would have been fun to sample an audio signal using the launchpad.
Before starting : The MSP430G2 is not designed with audio sampling in mind, so if you want to develop some good digital recorder or audio sampling device, you should probably look for more suitable devices.
... still, it can be done.

One issue is the limited amount of RAM in the mcu, which would prevent us to buffer and process the audio data internally (i.e. with an FFT).
The only hope we have is to get the data from the ADC and send it as fast as possible to the serial port, then use it in the pc itself.
You can achieve the same task, much easily and better using the internal sound card of your pc, but that would not be fun at all.

First thing I prepared a stereo cable to get the audio signal out of my computers headphone connection.
I used an old internal CD cable to which I soldered a stereo jack.


Then I checked I could get signals from both channels, using my scope (yup, to play with audio signals, the scope is quite handy).


Since we are going to feed this signal to our ADC converter, we need to ensure that its values are within the limits defined by Vref- and Vref+, meaning the two reference voltages used by the converter to define the minimum and maximum values to be sampled.
Most ADCs, including the MSP430G2 ADC10, can sample only positive signals (Vref- >+0) with a maximum value normally equal to their Vcc (3.3V for the launchpad).

Audio signals are obviously AC signals and typically they have their offset at 0V (you need to check that, it was the case for me, check the image above).
[The offset voltage is the voltage you obtain when you have the jack connected and no sound is played. When sound is played the offset can be eyeballed as the median voltage level]
This means that typically half of your audio signal has negative values, which would not work with the ADC.
We need to add a positive offset in a way that the incoming signal is never negative.

The other important thing is the amplitude of the signal (which you change with the volume settings).
I saw that most of the audio coming out from a youtube video had an amplitude of about 800mV, but pumping up the volume and playing some loud music showed a higher amplitude, reaching about 2V.

The ADC10 can use different Vref values, to make things easier we will use Vref- = 0V (GND) and we need to chose from 1.5V, 2.5V, Vcc (3.3V) for Vref+.

The signal amplitude we can take, assuming we can offset it exactly in the middle of our range is equal to Vref+ - Vref- , so having Vref- = 0V, the amplitude is equal to Vref+.
I would go for 2.5V because it is a bit higher than input signal amplitude, but not too much.

As a general rule, you normally want to amplify (or attenuate) the signal to make sure it fits nicely in your ADC band.

The reason why you want to  match your reference as close as possible is that, when sampling digitally, you divide your reference in a finite number of levels (1024 for a 10 bit ADC).
If the incoming signal has an amplitude which is covering just a small portion of your sampling band, then you are wasting sampling resolution.

Audio signal are pretty much symmetric in respect of their offset, so it's in general a good idea to move the offset in the middle of the sampling range, in our case to 2.5 / 2 = 1.25 V.
The typical way to achieve this is via a simple circuit based on an op-amp, but for this example (remember, this is an experiment, don't design devices in this way, it's probably not  a good idea!) I decided I wanted something simpler.

I just needed something capable to deliver a clean 1.25V and stick it in series with my signal.
That sounds like a fully charged rechargeable AA battery to me :)


A battery holder and a couple of crocodile contacts helped to put the battery in series with the signal before entering the scope probe (easier than dealing with op-amps, right?).
I checked with the scope setting the cursors to my Vref values (0 and 2.5V), this is what I obtained :


Eureka! This looks definitely a signal that could be handled by the ADC10.

Let the fun begin

It's time to start preparing our application.
The idea is to sample both channels (stereo!!) and eventually transfer the sampled data via UART.
The msp4302553 has 8 external Analog inputs mapped on port P1 (P1.0 to P1.7), we need to select two suitable channels in a way that they do not overlap with other functions we might need.
I will use the serial port, most likely UART, so I will steer clear from the pins used by that functionality (even if for this test I am planning to use the onboard FTDI chip that sends the uart data to the usb connection).
The 2553 datasheet has it all : our A3 / A4 channels (P1.3 , P1.4) look like good candidates.

Also in the datasheet we find  that there is no real need to set the multiplex values (P1SEL, P1SEL2) or the P1DIR register when using pins as ADC channels, because the ADC10 will set them for us (everybody say with me : "Thanks mister ADC10!") using the  ADC10AE0 register.


The ADC10 can do some interesting things when sampling multiple channels :
It can automatically sample a sequence of channels (that is actually quite cool for our application).

When sampling a sequence of channels, we need to specify the highest channel  we want to sample and the mcu will start a loop in which it will sample all the channels down to A0,
from the user guide :
"A sequence of channels is sampled and converted once. The sequence begins with the channel selected by INCHx and decrements to channel A0. Each ADC result is written to ADC10MEM. The sequence stops
after conversion of channel A0."

Fortunately, despite what stated in the user guide, it seems it is possible to set how many channels must be sampled, the DTC (a sort of dma controller, dedicated to the ADC10) can transfer the values to a defined memory area.


Time to write some code.


#include <msp430g2553.h>

unsigned int sampling;
unsigned int buffer[2];

void clockConfig()
{
 // configure the CPU clock (MCLK)
 // to run from DCO @ 16MHz and SMCLK = DCO / 4
 BCSCTL1 = CALBC1_16MHZ; // Set DCO
 DCOCTL = CALDCO_16MHZ;
 BCSCTL2= DIVS_2 + DIVM_0; // divider=4 for SMCLK and 1 for MCLK
}

void adcConfig()
{

  // ADC10SSEL_0 :  ADC10OSC
  // ADC10DIV_0 : ADC10 Clock Divider Select 0. Roughly 5MHz
  // INCH_4 : highest channel we are going to sample
  // CONSEQ_1 :  Sequence of channels
  ADC10CTL1 = ADC10SSEL_0 + ADC10DIV_0 + INCH_4 +  CONSEQ_1; //
  // SREF_1 : VR+ = VREF+ and VR- = AVSS
  // ADC10SHT_0 : Sample & Hold = 4 x ADC10CLKs
  // REF2_5V : ADC10 Ref 0:1.5V / 1:2.5V;
  ADC10CTL0 =  SREF_1 + REF2_5V + REFON + ADC10SHT_0 + MSC + ADC10ON + ADC10IE;
  ADC10AE0 |= BIT3 + BIT4;  // adc option for channels 3 and 4
  ADC10DTC1 = 0x02; // number of channels to be sampled
  __delay_cycles(1000);  // Wait for ADC Ref to settle
}

// temporary interrupt routine to check that the ADC sequence is complete
#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void)
{
  sampling = 0;
}

void main(void)
{
 WDTCTL = WDTPW + WDTHOLD;
 clockConfig();
 adcConfig();
 __bis_SR_register(GIE);  // Enable interrupts
 while (1)
 {
  ADC10CTL0 &= ~ENC;
  while (ADC10CTL1 & BUSY);               // Wait if ADC10 core is active
  sampling = 1;
  ADC10SA = (unsigned int)&buffer;        // Data buffer start
  ADC10CTL0 |= ENC + ADC10SC;             // Sampling and conversion start
  while(sampling>0); // wait until the sampling sequence is complete
 }
}


At this point you should refer to the previous post in which I introduced the ADC10, if you did not read it before.

Basically I opted to use the internal ADC oscillator at its maximum frequency.
For Vref I used VRef+ = internal reference and VRef- = GND
The internal reference is configured at 2.5V (default is 1.5V) and the sample & hold time is the shortest possible (4 cycle of ADC10CLK).
The MSC bit is set, that tells the ADC to keep converting data once the encoding request is triggered, then DTC controller will post it to the buffer starting at the address of the buffer variable (ADC10SA =(unsigned int)&buffer; )
It seems that the buffer start address must be set each time before starting a new conversion.

There are four operating modes selected with the CONSEQ constants :

#define CONSEQ_0               (0*2u)         /* Single channel single conversion */
#define CONSEQ_1               (1*2u)         /* Sequence of channels */
#define CONSEQ_2               (2*2u)         /* Repeat single channel */
#define CONSEQ_3               (3*2u)         /* Repeat sequence of channels */

I chose to sample a single sequence of channels, the DTC will drop the data in my two 16 bit integer buffer.

I also use a sampling variable to check if the sequence is complete or not, this is a simple temporary solution, in future implementations we might simply use the interrupt routine to send the data via uart.

That's about it for today, I debugged the program and -surprise surprise- my buffer is actually filled with values that vary at each loop, indicating that the DTC is sending it samples.
I also notice that with no audio incoming my channels they are at values around 510, which matches pretty closely my ideal offset (1024/2 = 512).

I might develop this experiment further and actually send some data to the pc... but that's going to be another post.  

Another thing to look into is to test if we can get away without the interrupt and just poll the ADC BUSY bit, it might actually be interesting to maximize the data transfer to the pc.