AVR® 10 ms Analog-to-Digital Converter (ADC) Samples Averaged Over 1 Second

Last modified by Microchip on 2023/11/09 09:02

In This Video

  • Modify a millisecond timer function, from Updating Pulse Width Modulator (PWM) duty cycle using a millisecond timer, we create a 10 mS timer.
  • Test the frequency, using the Data Visualizer, by toggling a pin in the timer IRQ.
  • Change Analog-to-Digital Converter (ADC) auto-trigger source to Timer Counter 0 Compare Match A.
  • Create volatile global variables accumulator, average and samples, using these to calculate an average in the ADC IRQ.
  • Verify that we are now averaging ADC light sensor readings every second.

Back to top

Procedure


Pull up the Code from a Previous Project

Once again, we will pull some code from our previous PWM project and add it to the AVR_ADC project to create the 10mS timer.  Copy the milliS_timer functiond() and paste it to just below the PWM_Init function() in the AVR_ADC project in the previous lesson.

Here is the code to be copied over...

void milliS_timer(uint8_t milliS)
{
    TCCR0A |= (1 << WGM01);                //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);   //set prescaler to 1024

    OCR0A = milliS * 7.8125 - 1;
    TIMSK0 |= (1 << OCIE0A);               //enable the interrupt  
}

In order to ADC samples every 10 ms, we need to revisit this equation...

OCRnA Formula​​​

OCR0A = (Required_Time * F_CPU) / (Prescaler_Value * 1000) - 1

In this case, the required time is 10ms, the prescaler value is 1024, and the F_CPU is 16,000,000 Hz.

Plugging these values into the formula, we get:

OCR0A = (10ms * 16,000,000 Hz) / (1024 * 1000) - 1 = (10 * 16,000) - 1 = 160,000 - 1 = 159,999

Since OCR0A is an 8-bit register, it can hold values from 0 to 255. Therefore, we need to choose the closest possible value to 159,999 that fits within the 8-bit range. In this case, the closest value is 156, which can be used as the OCR0A value to achieve a 10ms interrupt with a prescaler of 1024.

void milliS_timer0_10()
{
    TCCR0A |= (1 << WGM01);    //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);    //set prescaler to 1024
   

    OCR0A = 156; // 10mS at 1024
   TIMSK0 |= (1 << OCIE0A);               //enable the interrupt  
}

 


Test our Sample Frequency

We will test our sample frequency by setting a pin high each time the code steps into the TIMER0 ISR.  Place the following code in the main and newly created ISR for TIMER0.

Place in main

DDRD |= (1 << DDRD2);

Place the ISR below the ISR for the ADC

ISR(TIMER0_COMPA_vect)
{
    PIND |= (1 << PIND2);
}

Every time the TIMER0 interrupt occurs, PD2 will change states. We can see this in the scope plot below.

PIND2 Changing States​​​


Change ADC Trigger Source

Now that we know our TIMER0 is counting down properly, we will set the TIMER0 interrupt as a Trigger Source for the ADC.  This will change the ADC Trigger Source from Free Running Mode to Timer/Counter0 Compare Match A.

To make this happen, add the following line to the ADC_init() function just above the sei(); line.

ADCSRB |= (1 <<ADTS1) | (1 << ADTS0);

Now every time TIMER0 counts down, it generates and interrupt, and an ADC sample is taken. 


Modify the ADC Interrupt Service Routine

Create the following variables just below the #define macros. These will be used to calculate the averages.

volatile uint32_t accumulator = 0;
volatile uint16_t average = 0;
volatile uint16_t samples = 0

Here is the modified ISR(ADC_vect) which will gather the data and perform calculations for the average.

ISR(ADC_vect)
{
   uint16_t duty = ADC;
    OCR1B = duty;
   
    accumulator += duty;
    samples++;
   if (samples == 100)
    {
        average = accumulator/100;
        accumulator = 0;
        samples = 0;
    }
}

Complete Code

Here is the complete code for the lesson.

#include <avr/io.h>
#include
<avr/interrupt.h>
#define F_CPU 16000000UL

#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)

volatile uint32_t accumulator = 0;
volatile uint16_t average = 0;
volatile uint16_t samples = 0;

ISR(TIMER1_COMPA_vect)
{
   LED_ON;
}

ISR(TIMER1_COMPB_vect)
{
   LED_OFF;
}

ISR(ADC_vect)
{
   uint16_t duty = ADC;
    OCR1B = duty;
   
    accumulator += duty;
    samples++;
   if (samples == 100)
    {
        average = accumulator/100;
        accumulator = 0;
        samples = 0;
    }
}

ISR(TIMER0_COMPA_vect)
{
    PIND |= (1 << PIND2);
}

void PWM_Init(void)
{
    TCCR1B |= (1 << CS10) | (1 << WGM12);  //No Prescaler & set mode to CTC
   TIMSK1 |= (1 << OCIE1A) | (1 << OCIE1B); //Enable the CTC interrupt
   OCR1A = 800;
    OCR1B = 400;
}

void milliS_timer0_10()
{
    TCCR0A |= (1 << WGM01);    //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);    //set prescaler to 1024
   

    OCR0A = 156; // 10mS at 1024
   TIMSK0 |= (1 << OCIE0A);               //enable the interrupt  
}

void ADC_init(void)
{
   ADMUX |= (1 << REFS0) | (1 << MUX0);   //AVCC equal to VCC and ADC1 input
  ADCSRA |= (1 << ADEN) | (1 << ADSC) | (1 << ADATE) | (1 << ADIE) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
   ADCSRB |= (1 <<ADTS1) | (1 << ADTS0);
   sei();
}


int main(void) {
    DDRD |= (1 << DDRD2);
    DDRB |= (1 << DDRB5);
    ADC_init();
    PWM_Init();
    milliS_timer0_10();
   
   while (1) {
    }
}

Set a breakpoint within the ADC interrupt service routine, enter debug mode, and watch the accumulator and average values.  You should these update every time the application hits the breakpoint and is restarted.

ADC Watch Variables​​​

 

Back to top


Learn More

Back to top