ADC on Atmega328. Part 1

Microcontrollers are meant to deal with digital information. They only understand ‘0’ and ‘1’ values. So what if we need to get some non-digital data into the microcontroller. The only way is to digitize or simply speaking to convert analog in to digital. This is why almost all microcontrollers are featured with the ADC module. Atmega328 microcontroller also has 8 (or 6 in PDIP package) ADC input channels.

All these can be used to read an analog value that is within the reference voltage range.

Let us see how this is easy.

Operation Modes of ADC in ATmega328

First of all, we need to keep in mind that the internal ADC module in any microcontroller doesn’t pretend to be the best choice in all applications. It is meant to be used in relatively slow and not extremely accurate data acquisitions. Anyway, this is an excellent choice in most situations, like reading sensor data or reading waveforms.

AVR ADC module has 10-bit resolution with +/-2LSB accuracy. It can convert data at up to 76.9kSPS, which goes down when higher resolution is used. We mentioned that there are 8 ADC channels available on pins, but there are also three internal channels that can be selected with the multiplexer decoder. These are temperature sensor (channel 8), bandgap reference (1.1V) and GND (0V).

These specific channels may be handy in various situations. The temperature sensor is no doubt useful in many cases. Bandgap voltage source remains constant when VCC varies, so it can be used to read supply voltage level itself (as we will see later).

ADC can be set up for free running conversion, single conversion, and interrupt based conversion. Let us see how single conversion can be done by analyzing the following example.

ADC Single conversion mode

Before writing the program for analog to digital conversion, we need to take care of the analog part of the AVR chip. This includes powering analog peripherals by applying the voltage to AVCC, setting a reference voltage level in AREF pin and ensuring some protection from supply noise by using low pass filter.

For simple applications, datasheet recommends adding 100nF capacitor and 10uH inductor to AVCC pin that performs as low pass filter.

In our example, we set reference voltage the same as the power supply voltage. So we need to connect AREF pin to AVCC source. If we used internal 1.1V reference voltage, we would have to connect a capacitor between VREF pin and GND to reduce the chance of noise.

In our example, we are going to measure a potentiometer value, bang gap voltage, and send data via USART. The potentiometer is connected to ADC0 channel.

Initialization of ADC

To start using ADC we need to initialize it first. For this, we write a simple function:

void InitADC()
{
 // Select Vref=AVcc
 ADMUX |= (1<<REFS0);
 //set prescaller to 128 and enable ADC 
 ADCSRA |= (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)|(1<<ADEN);    
}

As we can see, first of all, we have to select the reference voltage source. By setting REFS0 fuze in ADMUX register. As datasheet says, AREF is connected to AVCC, and we only need to connect a capacitor between AREF pin and ground.

AVR ADC must be clocked at the frequency between 50 and 200kHz. So we need to set proper prescaller bits so that scaled system clock would fit in this range. As our AVR is clocked at 16MHz, we are going to use 128 scaling factor by setting ADPS0, ADPS1 and ADPS2 bits in ADCSRA register. This gives 16000000/128=125kHz of ADC clock.

And lastly, we enable ADC module by setting ADEN bit in ADCSRA register.

ADC conversion

Now ADC is set and turned. We can start conversion. For this, we prepare the following function that reads ADC value from the selected channel and returns as 16-bit value:

uint16_t ReadADC(uint8_t ADCchannel)
{
 //select ADC channel with safety mask
 ADMUX = (ADMUX & 0xF0) | (ADCchannel & 0x0F);
 //single conversion mode
 ADCSRA |= (1<<ADSC);
 // wait until ADC conversion is complete
 while( ADCSRA & (1<<ADSC) );
 return ADC;
}

before deciding ADC channel in ADMUX register, we use a mask (0b00001111) which protects from an unintentional alteration of ADMUX register.

After the channel is selected, we start single conversion by setting ADSC bit in ADCSRA register. This bit remains high until conversion is complete. So we are going to use this bit as an indicator to decide when data is ready. So we return ADC value after ADSC bit is reset.

Full source code

This is all we need. Here is a complete code that reads potentiometer value and Vbg voltage that is sent to USART terminal:

#include <stdio.h>
#include <avr/io.h>
#include <util/delay.h>
#define USART_BAUDRATE 9600
#define UBRR_VALUE (((F_CPU / (USART_BAUDRATE * 16UL))) - 1)
#define VREF 5
#define POT 10000
void USART0Init(void)
{
// Set baud rate
UBRR0H = (uint8_t)(UBRR_VALUE>>8);
UBRR0L = (uint8_t)UBRR_VALUE;
// Set frame format to 8 data bits, no parity, 1 stop bit
UCSR0C |= (1<<UCSZ01)|(1<<UCSZ00);
//enable transmission and reception
UCSR0B |= (1<<RXEN0)|(1<<TXEN0);
}
int USART0SendByte(char u8Data, FILE *stream)
{
   if(u8Data == '\n')
   {
        USART0SendByte('\r', stream);
   }
//wait while previous byte is completed
while(!(UCSR0A&(1<<UDRE0))){};
// Transmit data
UDR0 = u8Data;
return 0;
}
//set stream pointer
FILE usart0_str = FDEV_SETUP_STREAM(USART0SendByte, NULL, _FDEV_SETUP_WRITE);
void InitADC()
{
    // Select Vref=AVcc
    ADMUX |= (1<<REFS0);
    //set prescaller to 128 and enable ADC  
    ADCSRA |= (1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0)|(1<<ADEN);     
}
uint16_t ReadADC(uint8_t ADCchannel)
{
    //select ADC channel with safety mask
    ADMUX = (ADMUX & 0xF0) | (ADCchannel & 0x0F); 
    //single conversion mode
    ADCSRA |= (1<<ADSC);
    // wait until ADC conversion is complete
    while( ADCSRA & (1<<ADSC) );
   return ADC;
}
int main()
{
double vbg, potval;
//initialize ADC
InitADC();
//Initialize USART0
USART0Init();
//assign our stream to standart I/O streams
stdout=&usart0_str;
while(1)
{
    //reading potentiometer value and recalculating to Ohms
    potval=(double)POT/1024*ReadADC(0);
    //sending potentiometer avlue to terminal
    printf("Potentiometer value = %u Ohm\n", (uint16_t)potval);
    //reading band gap voltage and recalculating to volts
    vbg=(double)VREF/1024*ReadADC(14);
    //printing value to terminal
    printf("Vbg = %4.2fV\n", vbg);
    //approximate 1s
    _delay_ms(1000);
} 
}

ADC conversion results

After reset, we get Vbg value 0.96 and then steady 1.11V. This is because bandgap voltage (also internal reference) needs time to stabilize after switched on. So it is best practice to discard first readings as they may be inaccurate. The datasheet mentions that bandgap voltage is near 1.1V we get 1.11 – this is close enough. In the next tutorial part, we will discuss using Interrupt to detect conversion complete and automatic triggering of ADC conversions.

readADC.zip

Bookmark the permalink.

10 Comments

  1. A few comments:

    1. There is no need to set ADEN last. You can set all of ADCSRA at the same time.

    2.ReadADC() is flawed in that if you call ReadADC(1) then ReadADC(2) you will actually convert channel 3. I would do this:
    ADMUX = (ADMUX & 0xF0) | (ADCchannel & 0x0F);

    3. USART0SendByte()
    Why the bizarre recursive calling? Why not simply re-write the value of u8Data?

  2. Another one I noticed:
    potval=(uint16_t)(POT/1024*ReadADC(0));

    This actually compiles to potval = 9 * ReadADC(0);
    This is because POT/1024 is computed as 9, leading to a fairly large error. You should either do ((ReadADC(0) * POT) / 1024) using 32bit ints, or since you have some floating point already, just use that.

  3. Hi DT,
    Thank you for checking things out.
    1. I just left it for tutorial purposes. But actually this doesn’t make sense, so fixed according to your comments.
    2. Fixed.
    3. I use recursive function to make things simpler. Instead of writing printf(“\r\n”); i have to write printf(“\n”); Rewriting ‘\n’ with ‘\r’ would give different view in terminal window.
    4. Fixed potentiometer value calculation.

  4. RE: 3
    I see what you are doing but I’d hardly call it simpler than just using “\n\r”. Its certainly less portable, since it assumes you only want to send ASCII data. If “\n\r” this leads to extra RAM usage, then use printf_P(). I’d also argue that recursion is not a practice to encourage on embedded systems.

    Another note: the float format string %1.2f doesn’t make much sense. The 1 refers to the total field width, but this has to be ignored since to do so the function wouldn’t be able to fulfil the “.2” bit. In this case (if I understood your intent) “%4.2f” makes more sense i.e 1 ‘units’ digit, the decimal point, plus 2 fractional digits totalling a width of 4.

  5. I agree with you about recursions. Will avoid this in future tutorials.
    And yes I was a bit inertial with float format string. Somehow I made it look like what I wanted to see. Fixed. Thanks.

  6. vbg=(double)VREF/1024*ReadADC(14); – is that correct?? ReadADC(14)? not ReadADC(30)?
    Am I right that this references to Table 22-4. Input Channel and Gain Selections of the datasheet?
    Why the mask if 0f and f0? it seems DT made a mistake, it should read 1f and e0 instead! please, check the datasheet and explain, what’s going on. it’s difficult to understand. what was the initial version?

  7. Hi, I’m new to c programming and embedded systems. I tried to run your program on my mac but this is the error I get:
    prueba.c: In function ‘USART0SendByte’:
    prueba.c:22:1: error: stray ‘\302’ in program
    prueba.c:22:1: error: stray ‘\240’ in program
    prueba.c:23:1: error: stray ‘\302’ in program
    prueba.c:23:1: error: stray ‘\240’ in program
    prueba.c:24:1: error: stray ‘\302’ in program
    prueba.c:24:1: error: stray ‘\240’ in program
    prueba.c:25:1: error: stray ‘\302’ in program
    prueba.c:25:1: error: stray ‘\240’ in program
    prueba.c: At top level:

    In line 22 I have

    if(u8Data == ‘\n’)

    I hope you can help me thanks!

  8. Could be that you have wrong characters due to copied code from website. I have attached project files at the end of post.

  9. I have observed that in the world these days, video games include the latest popularity with kids of all ages. Periodically it may be extremely hard to drag the kids away from the games. If you want the best of both worlds, there are plenty of educational games for kids. Great post. babeagcegfcbgdka

Leave a Reply