4 Getting Started with ADC

4.1 Introduction

Author: Victor Berzan, Microchip Technology Inc.

The Analog-to-Digital Converter (ADC) peripheral converts an analog voltage to a numerical value. This peripheral is included in many AVR® microcontrollers (MCUs). A 10-bit single-ended ADC peripheral is available on most of the tinyAVR® and megaAVR® MCUs, while on the AVR DA family there is a 12-bit differential and single-ended ADC peripheral available.

This technical brief describes how the Analog-to-Digital Converter module works on tinyAVR® 0- and 1-series, megaAVR® 0-series and AVR® Dx and AVR DA microcontrollers. It covers the following use cases:
  • ADC Single Conversion:

    Initialize the ADC, start conversion, wait until the conversion is done, and read the ADC result.

  • ADC Free-Running:

    Initialize the ADC, enable Free-Running mode, start conversion, wait until the conversion is done, and read the ADC result in an infinite loop.

  • ADC Sample Accumulator:

    Initialize the ADC, enable accumulation of 64 samples, start conversion, wait until the conversion is done, and read the ADC result in a loop.

  • ADC Window Comparator:

    Initialize the ADC, set the conversion window comparator low threshold, enable the conversion Window mode, enable the Free-Running mode, start the conversion, wait until the conversion is done and read the ADC result in an infinite loop, and light-up an LED if the ADC result is below the set threshold.

  • ADC Event Triggered:

    Initialize the ADC, initialize the Real-Time Counter (RTC), configure the Event System (EVSYS) to trigger an ADC conversion on RTC overflow, toggle an LED after each ADC conversion.

Note: For each of the use cases described in this document, there are two code examples: One bare metal developed on ATmega4809, and one generated with MPLAB® Code Configurator (MCC) developed on AVR128DA48.

4.2 Overview

The Analog-to-Digital Converter (ADC) peripheral produces 10-bit results on tinyAVR and megaAVR microcontrollers and 10-bit/12-bit result on AVR DA microcontrollers. The ADC input can either be internal (e.g,. a voltage reference), or external through the analog input pins. The ADC is connected to an analog multiplexer, which allows the selection of multiple single-ended voltage inputs. The single-ended voltage inputs refer to 0V (GND).

The ADC supports sampling in bursts where a configurable number of conversion results are accumulated into a single ADC result (Sample Accumulation). The ADC input signal is fed through a sample-and-hold circuit that ensures the input voltage to the ADC is held at a constant level during sampling.

Selectable voltage references from the internal Voltage Reference (VREF) peripheral, VDD supply voltage, or external VREF pin (VREFA).

A window compare feature is available for monitoring the input signal and can be configured to only trigger an interrupt on user-defined thresholds for under, over, inside, or outside a window, with minimum software intervention required.
Figure 4-34. ADC Block Diagram

The analog input channel is selected by writing to the MUXPOS bits in the MUXPOS (ADCn.MUXPOS) register. Any of the ADC input pins, GND, internal Voltage Reference (VREF), or temperature sensor, can be selected as single-ended input to the ADC. The ADC is enabled by writing a ‘1’ to the ADC ENABLE bit in the Control A (ADCn.CTRLA) register. Voltage reference and input channel selections will not go into effect before the ADC is enabled. The ADC does not consume power when the ENABLE bit in ADCn.CTRLA is zero.

The ADC generates a 10-bit result on tinyAVR and megaAVR microcontrollers and a 10-bit/12-bit result on AVR DA microcontrollers. It can be read from the Result (ADCn.RES) register. The result is presented right adjusted.

The result for a 10-bit single-ended conversion is given as:
R E S = 1023 × V I N V R E F
where VIN is the voltage on the selected input pin and VREF is the selected voltage reference.
The result for a 12-bit single-ended conversion is given as:
R E S = 4096 × V I N V R E F

4.3 ADC Single Conversion

The simplest mode of using the ADC is to make a single conversion. The ADC input pin needs to have the digital input buffer and the pull-up resistor disabled, to have the highest possible input impedance. Pin PD6/AIN6 is used for ADC input in this example.
Figure 4-35. ADC0.MUXPOS Selection
ADC0.MUXPOS = ADC_MUXPOS_AIN6_gc;
The ADC Clock Prescaler can be used to divide the clock frequency. In this particular example, the clock is divided by 4. The ADC can use VDD, external reference, or internal reference for its positive reference. The internal reference is used in this example.
Figure 4-36. ADC0.CTRLC Voltage Reference Selection
ADC0.CTRLC |= ADC_PRESC_DIV4_gc;
ADC0.CTRLC |= ADC_REFSEL_INTREF_gc;
The ADC resolution is set by the RESSEL bit in the ADC0.CTRLA register. The ADC is enabled by setting the ENABLE bit in the ADC0.CTRLA register.
Figure 4-37. ADC0.CTRLA Resolution Selection
ADC0.CTRLA |= ADC_RESSEL_10BIT_gc;
ADC0.CTRLA |= ADC_ENABLE_bm;
The ADC conversion is started by setting the STCONV bit in the ADC0.COMMAND register.
Figure 4-38. ADC0.COMMAND - Start Conversion
ADC0.COMMAND = ADC_STCONV_bm;
When the conversion is done, the RESRDY bit in the ADC0.INTFLAGS gets set by the hardware. The user must wait for that bit to get set before reading the ADC result.
Figure 4-39. ADC0.INTFLAGS - Hardware-Set RESRDY Bit
while (!(ADC0.INTFLAGS & ADC_RESRDY_bm))
{
    ;
}
The user must clear the RESRDY bit by writing ‘1’ to it before starting another conversion.
ADC0.INTFLAGS = ADC_RESRDY_bm;
The conversion result can be read from the ADC0.RES register.
adcVal = ADC0.RES;
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

4.4 ADC Free-Running

When configuring the ADC in Free-Running mode, the next conversion starts immediately after the previous one completes. To activate this mode, the FREERUN bit in the ADC0.CTRLA must be set in addition to the normal ADC initialization.
Figure 4-40. ADC0.CTRLA - Set the FREERUN Bit
ADC0.CTRLA |= ADC_FREERUN_bm;
The ADC conversion is started by setting the STCONV bit in the ADC0.COMMAND register.
ADC0.COMMAND = ADC_STCONV_bm;
Then the ADC results can be read in a while loop.
while(1)
{
    if (ADC0_conersionDone())
    {
        adcVal = ADC0_read();
    }
}
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

4.5 ADC Sample Accumulator

In Sample Accumulator mode the ADC can add up to 64 samples in an accumulator register, thus filtering the signal and reducing the noise. It is useful when reading analog sensor data where a smooth signal is required. By using a hardware accumulator instead of adding those readings in software, it reduces the CPU load. To activate this mode, the Sample Accumulation Number in the ADC0.CTRLB register must be set in addition to the normal ADC initialization.
Figure 4-41. ADC0.CTRLB - Set the SAMPNUM Bit
ADC0.CTRLB = ADC_SAMPNUM_ACC64_gc;
The ADC conversion is started by setting the STCONV bit in the ADC0.COMMAND register.
ADC0.COMMAND = ADC_STCONV_bm;
The samples will be added up in the ADC0.RES register. The ADC_RESRDY flag is set after the number of samples specified in ADC0.CTRLB is acquired.
while (!(ADC0.INTFLAGS & ADC_RESRDY_bm))
{
    ;
}
The user can read that value and divide it by the number of samples, to get an average value.
adcVal = ADC0.RES;
adcVal = adcVal >> ADC_SHIFT_DIV64;
The user must clear the RESRDY bit by writing ‘1’ to it before starting another conversion.
ADC0.INTFLAGS = ADC_RESRDY_bm;
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

4.6 ADC Window Comparator

In Window Comparator mode, the device can detect if the ADC result is below or above a specific threshold value. This is useful when monitoring a signal that is required to be maintained in a specific range, or for signaling low battery/overcharge, etc. The window comparator can be used in both Free-Running mode and Single Conversion mode. In this example, the window comparator is used in Free-Running mode, because a monitored signal requires continuous sampling, and the Free-Running mode reduces the CPU load by not requiring a manual start for each conversion.

For this example, a low threshold is set in the ADC0.WINLT register.
Figure 4-42. ADC-WINLT - Set the Low Threshold
ADC0.WINLT = WINDOW_CMP_LOW_TH_EXAMPLE;
The conversion Window mode is set in the ADC0.CTRLE register.
Figure 4-43. ADC0.CTRLE - Set the Window Comparator Mode
ADC0.CTRLE = ADC_WINCM_BELOW_gc;
If the ADC result is below the set threshold value, the WCOMP bit in the ADC0.INTFLAGS register is set by the hardware.
Figure 4-44. ADC0.INTFLAGS - Hardware-Set WCOMP Bit
The user must clear that bit by writing ‘1’ to it.
ADC0.INTFLAGS = ADC_WCMP_bm;
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

4.7 ADC Event Triggered

An ADC conversion can be triggered by an event. This is enabled by writing a ‘1’ to the Start Event Input (STARTEI) bit in the Event Control (ADCn.EVCTRL) register.
Figure 4-45. ADC0.EVTRL - Enable the STARTEI Bit
ADC0.EVCTRL |= ADC_STARTEI_bm;

Any incoming event routed to the ADC through the Event System (EVSYS) will trigger an ADC conversion. The event trigger input is edge sensitive. When an event occurs, STCONV in ADCn.COMMAND is set. STCONV will be cleared when the conversion is complete.

For example, to start the ADC conversion on RTC overflow, the following settings must be made:
  1. The RTC overflow event must be linked to channel 0 of the Event System.
  2. The Event User ADC0 must be configured to take its input from channel 0.
  3. The STARTEI bit in the EVCTRL register of the ADC must be set to enable the ADC conversion to be triggered by events.
EVSYS.CHANNEL0 = EVSYS_GENERATOR_RTC_OVF_gc; /* Real Time Counter overflow */
EVSYS.USERADC0 = EVSYS_CHANNEL_CHANNEL0_gc; /* Connect user to event channel 0 */
ADC0.EVCTRL |= ADC_STARTEI_bm; /* Enable event triggered conversion */
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

4.9 Appendix

ADC Single Conversion Code Example

/* RTC Period */
#define RTC_PERIOD          (511)

#include <avr/io.h>
#include <avr/interrupt.h>

uint16_t adcVal;

void ADC0_init(void);
uint16_t ADC0_read(void);

void ADC0_init(void)
{
    /* Disable digital input buffer */
    PORTD.PIN6CTRL &= ~PORT_ISC_gm;
    PORTD.PIN6CTRL |= PORT_ISC_INPUT_DISABLE_gc;

    /* Disable pull-up resistor */
    PORTD.PIN6CTRL &= ~PORT_PULLUPEN_bm;

    ADC0.CTRLC = ADC_PRESC_DIV4_gc          /* CLK_PER divided by 4 */
                | ADC_REFSEL_INTREF_gc;     /* Internal reference */

    ADC0.CTRLA = ADC_ENABLE_bm              /* ADC Enable: enabled */
                | ADC_RESSEL_10BIT_gc;      /* 10-bit mode */

    /* Select ADC channel */
    ADC0.MUXPOS = ADC_MUXPOS_AIN6_gc;
}

uint16_t ADC0_read(void)
{
    /* Start ADC conversion */
    ADC0.COMMAND = ADC_STCONV_bm;

    /* Wait until ADC conversion done */
    while ( !(ADC0.INTFLAGS & ADC_RESRDY_bm) )
    {
        ;
    }

    /* Clear the interrupt flag by writing 1: */
    ADC0.INTFLAGS = ADC_RESRDY_bm;

    return ADC0.RES;
}

int main(void)
{
    ADC0_init();

    adcVal = ADC0_read();

    while (1) 
    {
        ;
    }
}

ADC Free-Running Code Example

#include <avr/io.h>
#include <stdbool.h>

uint16_t adcVal;

void ADC0_init(void);
uint16_t ADC0_read(void);
void ADC0_start(void);
bool ADC0_conersionDone(void);

void ADC0_init(void)
{
    /* Disable digital input buffer */
    PORTD.PIN6CTRL &= ~PORT_ISC_gm;
    PORTD.PIN6CTRL |= PORT_ISC_INPUT_DISABLE_gc;
    
    /* Disable pull-up resistor */
    PORTD.PIN6CTRL &= ~PORT_PULLUPEN_bm;
    
    ADC0.CTRLC = ADC_PRESC_DIV4_gc          /* CLK_PER divided by 4 */
                | ADC_REFSEL_INTREF_gc;     /* Internal reference */
    
    ADC0.CTRLA = ADC_ENABLE_bm              /* ADC Enable: enabled */
                | ADC_RESSEL_10BIT_gc;      /* 10-bit mode */
    
    /* Select ADC channel */
    ADC0.MUXPOS = ADC_MUXPOS_AIN6_gc;
    
    /* Enable FreeRun mode */
    ADC0.CTRLA |= ADC_FREERUN_bm;
}

uint16_t ADC0_read(void)
{
    /* Clear the interrupt flag by writing 1: */
    ADC0.INTFLAGS = ADC_RESRDY_bm;
    
    return ADC0.RES;
}

void ADC0_start(void)
{
    /* Start conversion */
    ADC0.COMMAND = ADC_STCONV_bm;
}

bool ADC0_conersionDone(void)
{
    return (ADC0.INTFLAGS & ADC_RESRDY_bm);
}

int main(void)
{
    ADC0_init();
    ADC0_start();
    
    while(1)
    {
        if (ADC0_conersionDone())
        {
            adcVal = ADC0_read();
            /* In FreeRun mode, the next conversion starts automatically */
        }
    }
}

ADC Sample Accumulator Code Example

#define ADC_SHIFT_DIV64    (6)

#include <avr/io.h>

uint16_t adcVal;

void ADC0_init(void);
uint16_t ADC0_read(void);

void ADC0_init(void)
{
    /* Disable digital input buffer */
    PORTD.PIN6CTRL &= ~PORT_ISC_gm;
    PORTD.PIN6CTRL |= PORT_ISC_INPUT_DISABLE_gc;
    
    /* Disable pull-up resistor */
    PORTD.PIN6CTRL &= ~PORT_PULLUPEN_bm;
    
    ADC0.CTRLC = ADC_PRESC_DIV4_gc          /* CLK_PER divided by 4 */
                | ADC_REFSEL_INTREF_gc;     /* Internal reference */
    
    ADC0.CTRLA = ADC_ENABLE_bm              /* ADC Enable: enabled */
                | ADC_RESSEL_10BIT_gc;      /* 10-bit mode */
    
    /* Select ADC channel */
    ADC0.MUXPOS  = ADC_MUXPOS_AIN6_gc;
    
    /* Set the accumulator mode to accumulate 64 samples */
    ADC0.CTRLB = ADC_SAMPNUM_ACC64_gc;
}

uint16_t ADC0_read(void)
{
    /* Start ADC conversion */
    ADC0.COMMAND = ADC_STCONV_bm;
    
    /* Wait until ADC conversion done */
    while ( !(ADC0.INTFLAGS & ADC_RESRDY_bm) )
    {
        ;
    }
    
    /* Clear the interrupt flag by writing 1: */
    ADC0.INTFLAGS = ADC_RESRDY_bm;
    
    return ADC0.RES;
}

int main(void)
{
    ADC0_init();
    
    while (1) 
    {
        adcVal = ADC0_read();
        
        /* divide by 64 */
        adcVal = adcVal >> ADC_SHIFT_DIV64;
    }
}

ADC Window Comparator Code Example

#define WINDOW_CMP_LOW_TH_EXAMPLE    (0x100)

#include <avr/io.h>
#include <stdbool.h>

uint16_t adcVal;

void ADC0_init(void);
uint16_t ADC0_read(void);
void ADC0_start(void);
bool ADC0_conersionDone(void);
bool ADC0_resultBelowTreshold(void);
void ADC0_clearWindowCmpIntFlag(void);
void LED0_init(void);
void LED0_on(void);
void LED0_off(void);

void ADC0_init(void)
{
    /* Disable digital input buffer */
    PORTD.PIN6CTRL &= ~PORT_ISC_gm;
    PORTD.PIN6CTRL |= PORT_ISC_INPUT_DISABLE_gc;
    
    /* Disable pull-up resistor */
    PORTD.PIN6CTRL &= ~PORT_PULLUPEN_bm;
    
    ADC0.CTRLC = ADC_PRESC_DIV4_gc          /* CLK_PER divided by 4 */
                | ADC_REFSEL_INTREF_gc;     /* Internal reference */
    
    ADC0.CTRLA = ADC_ENABLE_bm              /* ADC Enable: enabled */
                | ADC_RESSEL_10BIT_gc;      /* 10-bit mode */
    
    /* Select ADC channel */
    ADC0.MUXPOS = ADC_MUXPOS_AIN6_gc;
    
    /* Set conversion window comparator low threshold */
    ADC0.WINLT = WINDOW_CMP_LOW_TH_EXAMPLE;
    
    /* Set conversion window mode */
    ADC0.CTRLE = ADC_WINCM_BELOW_gc;
    
    /* Enable FreeRun mode */
    ADC0.CTRLA |= ADC_FREERUN_bm;
}

uint16_t ADC0_read(void)
{
    /* Clear the interrupt flag by writing 1: */
    ADC0.INTFLAGS = ADC_RESRDY_bm;
    
    return ADC0.RES;
}

void ADC0_start(void)
{
    /* Start conversion */
    ADC0.COMMAND = ADC_STCONV_bm;
}

bool ADC0_conersionDone(void)
{
    return (ADC0.INTFLAGS & ADC_RESRDY_bm);
}

bool ADC0_resultBelowTreshold(void)
{
    return (ADC0.INTFLAGS & ADC_WCMP_bm);
}

void ADC0_clearWindowCmpIntFlag(void)
{
    /* Clear the interrupt flag by writing 1: */
    ADC0.INTFLAGS = ADC_WCMP_bm;
}

void LED0_init(void)
{
    /* Make High (OFF) */
    PORTB.OUT |= PIN5_bm;
    /* Make output */
    PORTB.DIR |= PIN5_bm;
}

void LED0_on(void)
{
    /* Make Low (ON) */
    PORTB.OUT &= ~PIN5_bm;
}

void LED0_off(void)
{
    /* Make High (OFF) */
    PORTB.OUT |= PIN5_bm;
}

int main(void)
{
    ADC0_init();
    LED0_init();
    
    ADC0_start();
    
    while(1)
    {
        if (ADC0_conersionDone())
        {        
            if(ADC0_resultBelowTreshold())
            {
                LED0_on();
                ADC0_clearWindowCmpIntFlag();
            }
            else
            {
                LED0_off();
            }
            
            adcVal = ADC0_read();
        }
    }
}

ADC Event Triggered Code Example

/* RTC Period */
#define RTC_PERIOD            (511)

#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint16_t adcVal;

void ADC0_init(void);
void LED0_init(void);
void LED0_toggle(void);
void RTC_init(void);
void EVSYS_init(void);

void ADC0_init(void)
{
    /* Disable digital input buffer */
    PORTD.PIN6CTRL &= ~PORT_ISC_gm;
    PORTD.PIN6CTRL |= PORT_ISC_INPUT_DISABLE_gc;
    
    /* Disable pull-up resistor */
    PORTD.PIN6CTRL &= ~PORT_PULLUPEN_bm;
    
    ADC0.CTRLC = ADC_PRESC_DIV4_gc          /* CLK_PER divided by 4 */
                | ADC_REFSEL_INTREF_gc;     /* Internal reference */
    
    ADC0.CTRLA = ADC_ENABLE_bm              /* ADC Enable: enabled */
                | ADC_RESSEL_10BIT_gc;      /* 10-bit mode */
    
    /* Select ADC channel */
    ADC0.MUXPOS = ADC_MUXPOS_AIN6_gc;
    
    /* Enable interrupts */
    ADC0.INTCTRL |= ADC_RESRDY_bm;
    
    /* Enable event triggered conversion */
    ADC0.EVCTRL |= ADC_STARTEI_bm;
}

void LED0_init(void)
{
    /* Make High (OFF) */
    PORTB.OUT |= PIN5_bm;
    /* Make output */
    PORTB.DIR |= PIN5_bm;
}

void LED0_toggle(void)
{
    PORTB.IN |= PIN5_bm;
}

ISR(ADC0_RESRDY_vect)
{
    /* Clear flag by writing '1': */
    ADC0.INTFLAGS = ADC_RESRDY_bm;
    adcVal = ADC0.RES;    
    LED0_toggle();
}

void RTC_init(void)
{
    uint8_t temp;

    /* Initialize 32.768kHz Oscillator: */
    /* Disable oscillator: */
    temp = CLKCTRL.XOSC32KCTRLA;
    temp &= ~CLKCTRL_ENABLE_bm;
    /* Enable writing to protected register */
    CPU_CCP = CCP_IOREG_gc;
    CLKCTRL.XOSC32KCTRLA = temp;

    while(CLKCTRL.MCLKSTATUS & CLKCTRL_XOSC32KS_bm)
    {
        ; /* Wait until XOSC32KS becomes 0 */
    }

    /* SEL = 0 (Use External Crystal): */
    temp = CLKCTRL.XOSC32KCTRLA;
    temp &= ~CLKCTRL_SEL_bm;
    /* Enable writing to protected register */
    CPU_CCP = CCP_IOREG_gc;
    CLKCTRL.XOSC32KCTRLA = temp;

    /* Enable oscillator: */
    temp = CLKCTRL.XOSC32KCTRLA;
    temp |= CLKCTRL_ENABLE_bm;
    /* Enable writing to protected register */
    CPU_CCP = CCP_IOREG_gc;
    CLKCTRL.XOSC32KCTRLA = temp;

    /* Initialize RTC: */
    while (RTC.STATUS > 0)
    {
        ; /* Wait for all register to be synchronized */
    }

    RTC.CTRLA = RTC_PRESCALER_DIV32_gc      /* 32 */
                | RTC_RTCEN_bm              /* Enable: enabled */
                | RTC_RUNSTDBY_bm;          /* Run In Standby: enabled */

    /* Set period */
    RTC.PER = RTC_PERIOD;

    /* 32.768kHz External Crystal Oscillator (XOSC32K) */
    RTC.CLKSEL = RTC_CLKSEL_TOSC32K_gc;

    /* Run in debug: enabled */
    RTC.DBGCTRL |= RTC_DBGRUN_bm;
}

void EVSYS_init(void)
{
    /* Real Time Counter overflow */
    EVSYS.CHANNEL0 = EVSYS_GENERATOR_RTC_OVF_gc;
    /* Connect user to event channel 0 */
    EVSYS.USERADC0 = EVSYS_CHANNEL_CHANNEL0_gc;
}

int main(void)
{
    ADC0_init();
    LED0_init();
    RTC_init();
    EVSYS_init();
    
    /* Enable Global Interrupts */
    sei();
    
    while (1) 
    {
        ;
    }
}