ADC on Atmega328. Part 1

Microcontrollers are meant to deal with digital information. They only understant ‘0’ and ‘1’ values. So what if we need to get some non digital data in to 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 ADC module. Atmega328 microcontroller also have 8 (or 6 in PDIP package) ADC input channels.

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

Lets see how this is easy.

Operation Modes of ADC in ATmega328

First of all we need to keep in mind, that 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 a great choice in most situations like reading sensor data or reading waveforms. So AVR ADC module has 10-bit resolution with +/-2LSB accuracy. It is able to convert data at up to 76.9kSPS which goes down when higher resolution is used. We mentioned that that there are 8 ADC channels available on pins, but there are also three internal channels that can be selected with multiplexer decoder. These are: temperature sensor (channel 8), band gap reference (1.1V) and GND (0V). These specific channels may be handy in various situations. Temperature sensor is no doubt useful in many situations. Band gap 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. Lets see how single conversion can be done by analyzing following example.

ADC Single conversion mode

Before writing program for analog to digital conversion, we need to take care of analog part of AVR chip. This include powering analog peripherals by applying voltage to AVCC, setting reference voltage level in AREF pin and ensuring some protection from supply noise by applying low pass filter. For simple applications datasheet recommends adding 100nF capacitor and 10uH inductor to AVCC pin that perform as low pas filter.

In our example we set reference voltage same as power supply voltage. So we need to connect AREF pin to AVCC source. If we would use internal 1.1V reference voltage we would have to connect a capacitor between VREF pin and GND to reduce chance of noise. In our example we are going to measure a potentiometer value, bang gap voltage and send data via USART. Potentiometer is connected to ADC0 channel.

In order to start using ADC we need to initialize it first. For this we write 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 reference voltage source. By setting REFS0 fuze in ADMUX register. As datasheet says AREF is connected to AVCC and we only need to connect capacitor between AREF pin and ground.

AVR ADC must be clocked at 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.

Now ADC is set and turned. We can start conversion. For this we prepare following function that reads ADC value from 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 selecting ADC channel in ADMUX register we use a mask (0b00001111) which protects from unintentional alteration of ADMUX register.

After 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 indicator to decide when data is ready. So we return ADC value after ADSC bit is reset.

This is all we need. Here is a full code that reads potentiometer value and Vbg voltage that are 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
// Transmit data
UDR0 = u8Data;
return 0;
//set stream pointer
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
//Initialize USART0
//assign our stream to standart I/O streams
	//reading potentiometer value and recalculating to Ohms
	//sending potentiometer avlue to terminal
	printf("Potentiometer value = %u Ohm\n", (uint16_t)potval);
	//reading band gap voltage and recalculating to volts
	//printing value to terminal
	printf("Vbg = %4.2fV\n", vbg);
	//approximate 1s

And results:

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

Bookmark the permalink.


  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:

    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

Your email address will not be published. Required fields are marked *