Using the ADC [tutorial part 5]

Hello dear readers, today I will write(a lot, this is a pretty long tutorial today) about the internal ADC of our micro-controller, but first a little introduction to what is an ADC.
ADC stands for analogic to digital converter and permits us to convert analogic voltage levels to a digital representation, this permits us to read things like the output of a potentiometer, LDR’s, temperature sensors(like the LM35), humidity sensors, accelerometers and gyroscopes that have an analogic voltage output, pressure sensors and much more things.

Our atmega328p(and all of the atmega micro-controllers from atmel) as a built in analogic to digital converter that is ready to use(some older micro-controllers like the 16F family from Microchip and the BasicStamp they didn’t have a built in ADC) and doesn’t need any additional hardware, this enables us to just connect an analogic sensor a pot for testing and just do the software part and we are ready to go! The internal ADC of our micro-controller as a resolution of 10 bits, this means that the input analogic voltage will be converted to a numeric value between 0 and 1023.

Although the atmega328p as 6 pins that are capable of being used as analogic input pins, there is only one ADC in the micro-controller, but between the ADC and the pins there is a thing called analogic multiplexer, this permits us to choose which pin is connected to the ADC, this means that although we can use all the pins we can only read the value of each one at a time, for almost applications this is more than enough, but in some limited cases that need high speed ADC readings we might need to use external ADC’s, but for all our projects the internal one is good enough. In the case of the atmega328p the pins that can be used an analogic input are all the pins from PORT C.

As our chip is being feed with 5v we can plug any signal that we want to the ADC input pins if the signal is inside the 0-5v range, above or below that and the ADC circuitry will be damaged for ever and you might even destroy the whole chip. We can also change(given that its always bellow the Vcc of the atmega chip) the maximum voltage that the ADC uses, for that you can put your desired max voltage(its called the reference voltage and its against this voltage that all analogic to digital conversions are done) in the Aref pin, that is pin 21 of the DIP package and then configure the ADC in software to use that voltage reference, I will not explain this in this tutorial, but if you have doubts just shoot a question.

Now you are thinking, why would I want to reduce the maximum voltage of my ADC? Well this as a lot of uses, but lets explain some things first. Using the default reference voltage of 5v and this voltage is converted to a 10 bits value, the resolution of each bit is 5v/1023 = 4.88mV for each consecutive value, but imagine that you have an analogic accelerometer, those are almost always 3.3v part, so if you used the 5v reference you would have 5 – 3.3 = 1.7v of unused values and also a reduced resolution, now lets see what is the resolution if you used an 3.3v reference voltage 3.3/1023 = 3.22mV for each consecutive value, so its a gain in resolution and you would be able to used the full range of the ADC.

The internal ADC can also be used in an 8bits mode, where only the most significant 8 bits of the full 10bits resolution are used, this might be useful when working in noisy environments and you only need 8 bits resolution, using this mode is a plus because you don’t need to spend extra time shifting the full 10 bits result into an 8 bits one. The ADC can also be configured to do one conversion and stop or can be configured to work in a free running mode, the first option is the best one when we want to read different pins, and the latter is better when we only need to sample one pin this can save some time between conversions.

We also need to take care of the maximum working frequency of the ADC, this value is specified in the data-sheet and its 200Khz, this is the value of the internal clock of the ADC circuitry and is generated by dividing the main atmega clock, that in my/your case is 16Mhz, this dividing of the clock is done using pre-scalers and unfortunately there is only a very limited range of values for then so the maximum frequency that we can use and still be inside the maximum working frequency is 125Khz, we can over-clock our ADC, but we will loose precision, this means that if we exceed the maximum 200Khz the ADC is still fully function that it will no longer guarantee its 10 bits precision, but I can guarantee that if you use the next possible frequency that the atmega328p can offer when clocked at 16Mhz that is 250Khz you still have at least 8 bits, maybe 9, much more than 250Khz and the returned converted value is more and more useless and more and more filled with noise, my opinion, if you really need a faster ADC just use an external one.

Lets start by doing a simple test program so we can see our ADC in action. For this our atmega will read an 10Kohm pot connected to pin 0 of PORT C, that is the analogic 0 of the Arduino board, and regarding the read value it will turn on or off the led that is connected in pin 5 of PORT B (the digital 13 led).
The pot will be connected following this schematic:


Open AvrStudion and start a new project, call it adc, this will be our pseudo-code:

Configure led pin
Configure ADC
Turn on ADC
Start the ADC conversions

Infinite loop{
 Read ADC value
 If ADC value > 512 turn led on
 Else turn led off
 }

Lets start by setting up our led pin to be an output:

#include <avr/io.h>

int main(void){

DDRB |= (1<<PB5);    //PB5/digital 13 is an output

//Configure ADC
//Turn on ADC
//Start ADC convertions

for(;;){        //The infinite loop could also be while(1)
 //Read ADC value
 //If ADC value > 512 turn led on
 //Else turn led off
}

return 0;
}

Until now there is nothing new, but now I will teach you how to set up the ADC, so lets grab the datasheet and jump to Chapter 23, page 251, this is the ADC chapter and as the description of every register and also about the ADC circuitry, so prepare yourself for another wall of text!

First we need to provide a clock to the ADC, and as we have seen before this signal as a maximum recommended value and this clock is generated by the prescalers, so lets first setup our prescaler. This is configured in the ADCSRA register and the bits that are related to the prescaler settings are ADPS2, ADPS1 and ADPS0, you can see in page 265 the possible values for the prescaler are 2,4,8,16,32,64 and 128, as we are running at 16Mhz we will use the 128 prescaler so our ADC clock will be of 125Khz, to set this value we need to set the three ADPS bits to 1.

Now we will configure the voltage reference that our ADC will use, in this tutorial I will use the voltage that is also used to power the rest of our Atmega(+5v), with a capacitor connected to Aref(the Arduino board as the needed cap in the Aref pin), the reference voltage source is configured in the ADMUX register with the REFS1 and REFS0 bits, this is in page 263, in our case we will put REFS1 at 0 and REFS0 at 1, its also in the ADMUX register that we can select which channel will be used to perform an analogic to digital conversion, this is selected using the MUX3 to MUX0 bits, in this first example I will not touch this bits as we are reading from channel 0 and all the bits in the registers are in the 0 state when we power up our Atmega.

Now our ADC is almost setup, we just need to turn it on(because by default its turned off so the micro-controller consumes less current), and for that we put the ADEN bit at 1 in the ADCSRA register, and we set the ADSC bit in the same ADCSRA register, and in the ADCSRB register we need to clear the ADTS2, ADTS1 and ADTS0 bits, this ones determine how is a new conversion started, all cleared means that we are running in free-running mode, but you can see in the data-sheet that a new conversion than be caused by a timer, or by setting some bits in a non free-running mode, finally we also need to set ADATE bit to 1 so that the free-running mode as a trigger source to start a new conversion.
This is way simpler when coded, so lets do it!

#include <avr/io.h>

int main(void){

DDRB |= (1<<PB5);    ///PB5/digital 13 is an output

ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //Prescaler at 128 so we have an 125Khz clock source
ADMUX |= (1<<REFS0);
ADMUX &= ~(1<<REFS1);                //Avcc(+5v) as voltage reference
ADCSRB &= ~((1<<ADTS2)|(1<<ADTS1)|(1<<ADTS0));    //ADC in free-running mode
ADCSRA |= (1<<ADATE);                //Signal source, in this case is the free-running
ADCSRA |= (1<<ADEN);                //Power up the ADC
ADCSRA |= (1<<ADSC);                //Start converting

for(;;){        //The infinite loop could also be while(1)
 //Read ADC value
 //If ADC value > 512 turn led on
 //Else turn led off
}

return 0;
}

Now the only thing that its left is to read the ADC converted value, and this is the easiest part, we only need to write one word to access the converted value, I will just complete the code, because its only a simple if statement.

#include <avr/io.h>
int adc_value;        //Variable used to store the value read from the ADC converter

int main(void){

DDRB |= (1<<PB5);    ///PB5/digital 13 is an output

ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //Prescaler at 128 so we have an 125Khz clock source
ADMUX |= (1<<REFS0);
ADMUX &= ~(1<<REFS1);                //Avcc(+5v) as voltage reference
ADCSRB &= ~((1<<ADTS2)|(1<<ADTS1)|(1<<ADTS0));    //ADC in free-running mode
ADCSRA |= (1<<ADATE);                //Signal source, in this case is the free-running
ADCSRA |= (1<<ADEN);                //Power up the ADC
ADCSRA |= (1<<ADSC);                //Start converting

for(;;){            //The infinite loop
 adc_value = ADCW;    //Read the ADC value, really that's just it
 if(adc_value > 512){
 PORTB |= (1<<PB5);    //If ADC value is above 512 turn led on
 }
 else {
 PORTB &= ~(1<<PB5);    //Else turn led off
 }
}

return 0;
}

Now its just a matter of copy paste the code to AvrStudio and compile it, it should present no errors or warnings, and upload it to your board using avrdude, if you have problems uploading read by first and second tutorial again or leave a comment.


Here the upload is already done, now just turn your pot, the led should light up when you rotate the pot clockwise past its midpoint and turn off when you turn anti-clockwise before the midpoint of the pot.


And well, that’s it, you already have the knowledge to use the ADC, but you still don’t know how to change channel/analogic input pin, so this is sort of a part 2, in part because this will a rather big tutorial and you can read until here and leave the rest to another day.

Lets learn how to change the input channel so we can read any analogic input that we want, as I have already said this can be done setting or clearing the MUX3, MUX2, MUX1 and MUX0 bits in the ADMUX register, the mapping between the MUX values and the selected channel is shown in this table:

MUX3    MUX2    MUX1    MUX0    Pin that will be read
0        0       0       0        PORTC0    Analogic0
0        0       0       1        PORTC1    Analogic1
0        0       1       0        PORTC2    Analogic2
0        0       1       1        PORTC3    Analogic3
0        1       0       0        PORTC4    Analogic4
0        1       0       1        PORTC5    Analogic5

As we are going to read different pins there is no sense to have the ADC in free-running mode, because having it in that mode imposes a limitation, when we change the adc channel/pin in free-running mode we must wait for 2 conversions to be made because that’s the time that the multiplexer takes to change from pin to pin, but if we start a single conversion after setting the multiplexer we don’t need to wait any time.
But when running in single conversion mode there are some new things that we must check, as the ADC clock is ratter slow (125Khz compared to the 16Mhz that the chip is running) and each conversion takes 13 cycles of that 125Khz clock when we read the ADC we start a new single conversion and we need to wait until its done, to do this we check the bit ADSC until its cleared, when its cleared the conversion is done, and as we are doing single conversions we don’t need to mess with the ADTS and ADATE bits, so less code.
The code that will show here is basically the code from the first part, but wrapped in two nice functions to make it a bit cleaner and the function that is used to read the ADC as the code needed to change channels already added.

#include <avr/io.h>

uint16_t adc_value;            //Variable used to store the value read from the ADC
void adc_init(void);            //Function to initialize/configure the ADC
uint16_t read_adc(uint8_t channel);    //Function to read an arbitrary analogic channel/pin

int main(void){

for(;;){            //Our infinite loop
}

return 0;
}

void adc_init(void);{

 ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //16Mhz/128 = 125Khz the ADC reference clock
 ADMUX |= (1<<REFS0);                //Voltage reference from Avcc (5v)
 ADCSRA |= (1<<ADEN);                //Turn on ADC
 ADCSRA |= (1<<ADSC);                //Do an initial conversion because this one is the slowest and to ensure that everything is up and running
}

uint16_t read_adc(uint8_t channel){
 ADMUX &= 0xF0;                    //Clear the older channel that was read
 ADMUX |= channel;                //Defines the new ADC channel to be read
 ADCSRA |= (1<<ADSC);                //Starts a new conversion
 while(ADCSRA & (1<<ADSC));            //Wait until the conversion is done
 return ADCW;                    //Returns the ADC value of the chosen channel
}

The code is still easy to ready and understand, and now we have two handy functions that you can just copy and paste into your projects and read the ADC just like you are using the Arduino functions, the number that you pass to the read_adc() function is the same number that is printed in the Arduino board, its also the pin number from PORTC, so analogic channel 0 is in PORTC0 and can be read using read_adc(0).

Now, to make this code a little bit more interesting lets go back in time and get some serial functions to send some data read from the ADC back to our computer where we can capture it using the Terminal and do for example some graphs using GnuPlot or a simple spreadsheet in Excel.

#define F_CPU 16000000UL
#define BAUDRATE 9600
#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

void USART_init(void){

 UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
 UBRR0L = (uint8_t)(BAUD_PRESCALLER);
 UCSR0B = (1<<RXEN0)|(1<<TXEN0);
 UCSR0C = (3<<UCSZ00);
}

void USART_send( unsigned char data){

 while(!(UCSR0A & (1<<UDRE0)));
 UDR0 = data;

}

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
 USART_send(*StringPtr);
 StringPtr++;}

}

Now lets merge this together, don’t get lost, its only a copy paste operation to put together the ADC and the serial code together:

#include <avr/io.h>
#define F_CPU 16000000UL
#define BAUDRATE 9600
#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

uint16_t adc_value;            //Variable used to store the value read from the ADC
void adc_init(void);            //Function to initialize/configure the ADC
uint16_t read_adc(uint8_t channel);    //Function to read an arbitrary analogic channel/pin
void USART_init(void);            //Function to initialize and configure the USART/serial
void USART_send( unsigned char data);    //Function that sends a char over the serial port
void USART_putstring(char* StringPtr);    //Function that sends a string over the serial port

int main(void){
adc_init();        //Setup the ADC
USART_init();        //Setup the USART

for(;;){        //Our infinite loop
 //Read the channels one by one and then send the read value to the terminal via serial
}

return 0;
}

void adc_init(void){
 ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //16Mhz/128 = 125Khz the ADC reference clock
 ADMUX |= (1<<REFS0);                //Voltage reference from Avcc (5v)
 ADCSRA |= (1<<ADEN);                //Turn on ADC
 ADCSRA |= (1<<ADSC);                //Do an initial conversion because this one is the slowest and to ensure that everything is up and running
}

uint16_t read_adc(uint8_t channel){
 ADMUX &= 0xF0;                    //Clear the older channel that was read
 ADMUX |= channel;                //Defines the new ADC channel to be read
 ADCSRA |= (1<<ADSC);                //Starts a new conversion
 while(ADCSRA & (1<<ADSC));            //Wait until the conversion is done
 return ADCW;                    //Returns the ADC value of the chosen channel
}

void USART_init(void){

 UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
 UBRR0L = (uint8_t)(BAUD_PRESCALLER);
 UCSR0B = (1<<RXEN0)|(1<<TXEN0);
 UCSR0C = (3<<UCSZ00);
}

void USART_send( unsigned char data){

 while(!(UCSR0A & (1<<UDRE0)));
 UDR0 = data;

}

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
 USART_send(*StringPtr);
 StringPtr++;}

}

And now we have a little problem, when using the Arduino IDE we can just use the print functions and they will take care of everything under to hood to convert for example the 10 bits data from the ADC to a nice string of number that we can read, but doing things bare-metal we don’t have such royalties. In order to convert the 10 bits ADC value to a readable number string we need to use a function called ITOA(stands for Integer TO Ascii) this functions as 3 input parameters, one is the value that we want to convert, this can be an uint8_t, uint16_t, int, and other similar types of integer variables, other of the input parameters is the base for where we want to convert, 2 to convert to binary, 8 for octal, 10 for decimal, and 16 for hexadecimal, and finally a small array that will be the output string plus the NULL string terminator, this output array must have at least space for the maximum number of chars that our number as plus one for the NULL, so as we are using 10 bits values from the ADC, this means that our maximum value is 2^10-1 = 1023, so we can have a maximum of 4 numbers plus the terminator so we need an array with space for 5 chars.  Also, itoa is one of the standart functions of C, and thus it is located in the stdlib.h header and that header must be included so the compiler doesn’t complains.In a generic mode this is how the itoa function is used:

#include <stdlib.h>        //This lib is where the itoa function is located

char buffer[5];            //The output array
uint16_t adc_value;        //A variable were we have some data that we want to convert to a string
itoa(adc_value, buffer, 10);    //And now buffer as the string with the decimal value that was in the adc_value variable

Now lets add this function to our code and add some more bits so it can scan all the ADC ports and send the data to the terminal, you can use this as I said above as a simple data logger, and this little program as already been handy to log some voltages over time and then graph then in Excel.

#include <avr/io.h>
#include <stdlib.h>
#define F_CPU 16000000UL
#include <util/delay.h>
#define BAUDRATE 9600
#define BAUD_PRESCALLER (((F_CPU / (BAUDRATE * 16UL))) - 1)

uint16_t adc_value;            //Variable used to store the value read from the ADC
char buffer[5];                //Output of the itoa function
uint8_t i=0;                //Variable for the for() loop

void adc_init(void);            //Function to initialize/configure the ADC
uint16_t read_adc(uint8_t channel);    //Function to read an arbitrary analogic channel/pin
void USART_init(void);            //Function to initialize and configure the USART/serial
void USART_send( unsigned char data);    //Function that sends a char over the serial port
void USART_putstring(char* StringPtr);    //Function that sends a string over the serial port

int main(void){
adc_init();        //Setup the ADC
USART_init();        //Setup the USART

for(;;){        //Our infinite loop
 for(i=0; i<6; i++){
 USART_putstring("Reading channel ");
 USART_send('0' + i);            //This is a nifty trick when we only want to send a number between 0 and 9
 USART_putstring(" : ");            //Just to keep things pretty
 adc_value = read_adc(i);        //Read one ADC channel
 itoa(adc_value, buffer, 10);        //Convert the read value to an ascii string
 USART_putstring(buffer);        //Send the converted value to the terminal
 USART_putstring("  ");            //Some more formatting
 _delay_ms(500);                //You can tweak this value to have slower or faster readings or for max speed remove this line
 }
 USART_send('\r');
 USART_send('\n');                //This two lines are to tell to the terminal to change line
}

return 0;
}

void adc_init(void){
 ADCSRA |= ((1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0));    //16Mhz/128 = 125Khz the ADC reference clock
 ADMUX |= (1<<REFS0);                //Voltage reference from Avcc (5v)
 ADCSRA |= (1<<ADEN);                //Turn on ADC
 ADCSRA |= (1<<ADSC);                //Do an initial conversion because this one is the slowest and to ensure that everything is up and running
}

uint16_t read_adc(uint8_t channel){
 ADMUX &= 0xF0;                    //Clear the older channel that was read
 ADMUX |= channel;                //Defines the new ADC channel to be read
 ADCSRA |= (1<<ADSC);                //Starts a new conversion
 while(ADCSRA & (1<<ADSC));            //Wait until the conversion is done
 return ADCW;                    //Returns the ADC value of the chosen channel
}

void USART_init(void){

 UBRR0H = (uint8_t)(BAUD_PRESCALLER>>8);
 UBRR0L = (uint8_t)(BAUD_PRESCALLER);
 UCSR0B = (1<<RXEN0)|(1<<TXEN0);
 UCSR0C = (3<<UCSZ00);
}

void USART_send( unsigned char data){

 while(!(UCSR0A & (1<<UDRE0)));
 UDR0 = data;

}

void USART_putstring(char* StringPtr){

while(*StringPtr != 0x00){
 USART_send(*StringPtr);
 StringPtr++;}

}

Now compile this and upload it to your Arduino, if you just want to test the program you don’t need to connect anything to the ADC inputs, they will act as antennas and capture some random noise so you will see varying number in the terminal.


I have updated my Terminal, you can also do the same or keep using the older one, there are no differences, and the configuration is exactly the same for this new one or for the older one, you can get it here: https://sites.google.com/site/terminalbpp/
And this is the result:


And that’s it, a long tutorial, but it covers a lot about the ADC and also teach a little about using the itoa function, fell free to try it and leave feedback in the comments, thanks for reading!

Corrected some errors in the text, and added the two examples to Google Code to download then as Avr Studio projects:

http://code.google.com/p/avr-tutorials/downloads/detail?name=adc1.zip

http://code.google.com/p/avr-tutorials/downloads/detail?name=adc2.zip

Ps.: Sorry for the source code aspect but WordPress likes to remove all the spaces and tabs :/

About these ads

25 responses to “Using the ADC [tutorial part 5]

  1. D: no more tutorials?

  2. I thought no “real” coder uses windows. So it looks like your not a “real” coder.

  3. hey where does ADCW comes from?

    Also, wheres the timers and pwm generation tutorial :P sorry for the pressure

    • The ADCW is a nice thing that Avr libc provides you, so instead of writing code to read ADCH, then ADCL and then do some bitwise shifts, you only write ADCW and the gcc takes care of everything.

      The timers tutorial is still yet to be finished, I will probably break it into smaller parts, so I can publish at least the first part in a short time, because my exams are just starting.

  4. Hi…your tutorial is very interesting…I had to do ADC using atmega32 and the analog signal I gave is ramp signal using signal generator. The LED doesnt change from one to another. I have used low frequency signal also, but couldnt find any changes. I tried to use AVR simulator to check the value of ADCH and ADCL but couldnt see any changes in them also. How could I know that my ADC is working? Please help me out.

    Regards,
    Shahgufta

  5. Hey, just want to say thanks for your blog posts!

    Br, Jusbe

  6. this was of great help. thanks!

  7. Hi

    Great tutorial, thank you.

    I just tried the solution of

    http://www.electronicsplanet.ch/mikrocontroller/avrcodesammlung/ATmega16ADCindex.htm

    and it made my adc running.

  8. Thanks! Really helped me. Extremely well written and thorough.

  9. Shrikant Vyas

    Hey..Wonderful explanation..! could you help with the analog write part..??

  10. thanks for such a nice tutorial :)

  11. Thank you so much… You described it so simple, and it was easy to understand. Really great Job. I’ll suggest your web to my friends.

  12. thaks a lot ..really helpful ….specially itoa() function

  13. Thanks for awesome tutorial.

    I have a question – this works, if im not using capacitor at AREF pin? That means, REFS1 and REFS0 at zero position?

  14. And how does that yours Terminal works? How can i get ADC values in my PC?

  15. this is a simple program for adc which i have written,working perfectly with atmega16,but when i write the same for atmega8 it doesn’t work…
    #include
    #define F_CPU 16000000UL
    #include
    int main()
    {
    DDRD=0xFF;
    PORTD=0×00;
    ADMUX=(1<<REFS0);
    ADCSRA=(1<<ADEN)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);
    while(1)
    {
    ADCSRA|=(1<<ADSC);
    while(!(ADCSRA & (1<<ADIF)));
    ADCSRA|=(1<<ADIF);
    PORTD=ADCW;
    _delay_ms(50);
    }
    }
    plz..tell me ,what are the changes needed to be made ???

  16. Just for reference: It’s “Analog.” There’s no such thing as “Analogic.” Don’t know if you meant to do that or what, but figured I’d tell you.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s