Use and AVR microcontroller's incorporated ADC to take analog readings from your surroundings!
In the last article, we learned about using digital I/O and some useful bit manipulation on the ATmega168. In this article, we will learn about taking analog readings from the real world!
A Quick Recap on ADCs
Most people believe that the realm of analog is behind them, but the truth is the analog realm has never been stronger! With the rise of the IoT and production of sensors, billions of analog readings are being taken every second all over the globe. This is just one of the many reasons producers of microcontrollers incorporate ADC peripherals directly into their devices.
- ADCs convert analog signals into digital numbers
- ADCs are typically measured in their bit width (i.e. 10-bit ADC)
- Because ADCs are digital the analog signal is quantized into discrete steps
- This means that ADCs can only accurately measure specific voltage levels
- The largest digital value is equal to the ADCs positive reference (usually VCC)
- The smallest digital value is equal to the ADCs negative reference (usually GND)
- ADCs take time to convert their signals
For the ATmega168, the ADC has the following properties:
- 10-bit resolution (1024 discrete voltage levels between +Vref and -Vref)
- Accuracy to 2LSB (upper 8-bit accuracy guarantee)
- Up to 15,000 samples/second
- 6 multiplexed input sources
- 1.1V band gap reference
Configuring the ADC
Before we can use the ADC, both the peripheral and the I/O pins that will be used for ADC measurements need to be configured.
Left Justified or Right Justified?
When the ADC has completed a conversion operation, the result is stored in a pair of 8-bit registers (the 10-bit result won’t fit in a single 8-bit register). Because the ATmega is a native 8-bit device, it makes more sense to use 8-bit ADC results instead of 10-bit results, however, this will reduce the resolution of the result. But reading an 8-bit result from a 10-bit number must be done correctly, or the result will be incorrect. To do this, we need to read the top 8-bits (bits 9–2), not the bottom 8 bits. This is easily achieved using the ADLAR bit, which, when set to 1, will make the ADC result left justified. This means that instead of needing to read both result registers (ADCH and ADCL) and doing some bit manipulation, we can directly read ADCH and forget about ADCL.
Configure I/O Port
Now that we can directly read ADCH for an 8-bit result and not have to worry about bit manipulation (at the cost of resolution), we now need to configure our analog pins. By default, I/O pins on the ATmega168 are configured as digital pins, which means that they can only deal with 1s and 0s, which is not helpful. So, to configure inputs as analog pins, we use the DIDR0 register, which stands for Digital Input Disable Register. Unfortunately, not every pin has the capability of being an analog input, so be sure to keep an eye on pins with the label ADCx. For example, pins 23 to 28 are analog input pins on the ATmega168.
Configure ADC module
The last stage in configuring the ADC involves turning on the ADC, setting the pre-scaler times, and determining the reference for the ADC.
Turning on the ADC module is done by setting the ADEN bit found in the ADCSRA register.
For most situations, the pre-scaler is not too important, and for the sake of simplicity, we will set the pre-scaler to its maximum value (setting all ADPSx bits in the ADCSRA register).
The ADC on the ATmega168 can be between 0V and some reference, which is normally set to VCC. Since this will be the case for most circuits, we need to connect the Aref pin to a capacitor that is also connected to ground, and we also need to set our REFSx bits to use AVCC as the reference.
Using the ADC
Using the ADC is rather simple. The channel that the analog reading will be taken from is selected, then, to start a conversion, the ADSC bit (found in ADCSRA) is turned on. Once the conversion is complete, the ADC hardware automatically clears the ADSC bit.
Selecting the analog pin to read from is done by setting the appropriate multiplexer bits MUX3–MUX0 found in the ADMUX register.
This example reads analog values from ADC0 (PC0, pin 23) and compares them to a specific value. If the analog reading goes beyond the specified value (defined as TRIGPOINT), the LED (which is connected to PD0, pin 2), is turned on. Once the ADC reading goes below the specified value, the LED is turned off!
* AVR IO.c
* Created: 03/01/2018 11:25:21
* Author : RobinLaptop
#define F_CPU 1000000UL
#define TRIGPOINT 128
// Configure PORT D bit 0 to an output
DDRD = 0b00000001;
// Configure PORT C bit 0 to an input
DDRC = 0b00000000;
// Configure ADC to be left justified, use AVCC as reference, and select ADC0 as ADC input
ADMUX = 0b01100000;
// Enable the ADC and set the prescaler to max value (128)
ADCSRA = 0b10000111;
// Main program loop
// Start an ADC conversion by setting ADSC bit (bit 6)
ADCSRA = ADCSRA | (1 << ADSC);
// Wait until the ADSC bit has been cleared
while(ADCSRA & (1 << ADSC));
if(ADCH > TRIGPOINT)
// Turn LED on
PORTD = PORTD | (1 << PD0);
// Turn LED off
PORTD = PORTD & ~(1 << PD0);
This article only covers the basics behind the ADC, yet already we can start taking analog measurements from the real world. Of course, you can read the datasheet and learn about the other more advanced features, including triggering and other voltage references. But for now, this should provide you with enough knowledge to go and start making AVR projects that need to read analog values!