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.


9 comments:

Kiran K said...

Can you tell me the pins used to provide analog input to the MSP ?? And how exactly did you observe the output using the code described earlier?

Francesco Agosti said...

Hello Kiran,
you can chose different pins and they are configured setting the ADC register values

// INCH_4 : highest channel we are going to sample
ADC10CTL1 = ADC10SSEL_0 + ADC10DIV_0 + INCH_4 + CONSEQ_1;

ADC10AE0 |= BIT3 + BIT4; // adc option for channels 3 and 4

ADC10DTC1 = 0x02; // number of channels to be sampled

When using channel sequences you need to specify the highest channel to be sampled.
In my case I used 2 channels, so passing channel 4 (INCH_4), channels 3 and 4 will be sampled.

Checking the datasheet of the msp430g2553 I found that those analog channels (A3 and A4) are mapped to the pins P1.3 and P1.4.

To monitor the signal I used a digital oscilloscope in parallel to the ADC (one connection to ground, the other to p1.3).

I hope it makes things a bit more clear.

Kiran K said...

Hey ,can you tell me how to copy the value of ADC10MEM to a variable . .
Since the value of ADC10MEM is a 12bit value, how can i obtain it at the output of MSP430G2231 ?

Francesco Agosti said...

Hello Kiran, that's correct, you need 12 bit of space to store a sample.

An unsigned int is 16 bit , so it would do for us.

In my example I was sampling two channels, therefore I defined a buffer (array) of 2 ints

unsigned int buffer[2];

Then I instructed the Data Transfer Coordinator (DTC) to copy the results in my buffer :

ADC10SA = (unsigned int)&buffer;

Technically I wrote the pointer of my buffer variable to the ADC10SA register.

Finally I can "collect" the sampled values for my two channels in buffer[0] and buffer[1]

Francesco Agosti said...

Just an additional note : when using this technique, it appears you have to reset the ADC10SA pointer at each sampling sequence, since it is incremented automatically.

You can let it increment eventually if you want to store several samples, just make sure in that case that the buffer you allocated is big enough and that you do not overrun it.

88470000-8b39-11e2-aa78-000bcdcb2996 said...

Hello,
So i was reading your blog and you mentioned something about transferring the data to a pc.
I was wondering on how you would do that since i have a mic circuit connected on my chip and i used your program to sample only 1 channel (INCH4) and now i would like to display my wave form on matlab.
Do you have any idea on how i would be able to do that?

Thanks a lot!

Asma

88470000-8b39-11e2-aa78-000bcdcb2996 said...

Hello,
How would you alter your code to send your sample waveform to a file on the pc?
Thanks
Asma

Francesco Agosti said...

Hello, I am currently travelling so I can only give you a short answer (might develop further when I am back home, tomorrow).
You need to send data via serial interface (easiest way is to use the ftdi interface - servial over usb).
On your PC you need to set up a propgram to capture the serial stream.
I.e. some time ago I created a component for the Talend ETL (Open source) to capture it :
http://www.talendforge.org/exchange/index.php?eid=455&product=tos&action=view&nav=1,1,1

Francesco Agosti said...

In order to send the data to a pc, you should configure the serial port (probably you want it set to 115200 baud or a higher -non standard- speed if you can match it on the PC side) and then send data at each sampling.
To set up the port you can check my post here : http://fortytwoandnow.blogspot.ch/2012/06/msp430g2-serial-communication-1.html

Make sure you are not using the same pins as ADC input channels and as UART RX/TX.
Also pay attention to the clock settings, if you use SMCLK both for ADC and UART operations make sure to find a speed (divider) that fits both your needs.

Then inside the ADC interrupt function

#pragma vector=ADC10_VECTOR
__interrupt void ADC10_ISR(void)
{
UART_SendChar((buffer[0]>>8)&0xff);
UART_SendChar(buffer[0]&0xff); UART_SendChar((buffer[1]>>8)&0xff);
UART_SendChar(buffer[1]&0xff);
sampling = 0;
}

where the sendchar function is something like this

void UART_SendChar(unsigned char txChar)
{
while (!(IFG2&UCA0TXIFG)); // USCI_A0 TX buffer ready?
UCA0TXBUF=txChar;
}

In the ADC interrupt I assumed you are sampling 2 channels and you want to send the 10 bit sampled value ove the serial port sending first the high part, then the low part, one channel after the other.
In a real application you might want to set up some kind of smarter protocol to avoid to waste 6 bits every channel in the transmission (you send two bytes = 16 bits while you need just 10) and some kind of synchronization to establish when the packet starts.
Also you may want to bring the 10 bit sampled value down to 8 bit and send one single bye per channel/sample :

unsigned char sendThis = (buffer[0]>>2)&0xff;