Tutorials‎ > ‎

Interrupt-Driven Analog Conversion With an ATMega328p

This tutorial is a direct result of some of the work that I did for my Motorized Camera Dolly project. However, I am recreating the code here for simplicity sake.

First, the jargon. I'm using ADC as two different nouns, depending on context. It could stand for either one of : "Analog-Digital Converter" or "Analog-Digital Conversion". Hopefully this isn't too confusing.

I own an Arduino Uno, which runs an ATMega328p microprocessor (datasheet).  The Arduino interface is great - a whole bunch of folks have worked really hard to build a portable hardware abstraction for a range of AVR micros. However, sometimes these abstractions mean you can't utilize the processor to its full potential. This tutorial strips away some layers of the Arduino language, and interfaces directly with hardware. That means it isn't portable without some modification, but allows for direct manipulation of registers within the processor. If you're using a different Arduino this code can still work for you, but it may take some searching in the datasheet and tweaking of some of the register bits.

A perfect example of this is with reading analog voltages from the "AD" pins on the Arduino board.

Arduino supplies the function int AnalogRead(int pin) which tells the microprocessor to read an analog value from the specified pin, and return it.

On the surface, this works great! I can request a value, I store it in a variable, and then continue on processing. However, Arduino's implementation of this is (very) slow! The onboard ADC requires 13 clock cycles in order to process an analog value. However, in order to get accurate results, the clock at the ADC needs to be slower than the overall system clock. A prescaling constant, by default 128, is used to divide the system clock before supplying it to the ADC.

So, on an Arduino running at 16MHz (16,000KHz), the ADC clock is at (16,000/128)KHz, or 125KHz. At 13 clock cycles, that's about 104us (microseconds) to perform an ADC.

Depending on the application, that's a lot of time! The prescaling constant can be adjusted inside the processor, but doing so can compromise the accuracy of the result.

Luckily, there's another way around this. The ATMega328p has the ability to start an ADC (which takes almost no time at all), and then provide an interrupt when it is done converting. This means that while the ADC is running, the processor can be executing other code.

Without further ado, here's the code!

// Testing interrupt-based analog reading
// ATMega328p

// Note, many macro values are defined in <avr/io.h> and
// <avr/interrupts.h>, which are included automatically by
// the Arduino interface

// High when a value is ready to be read
volatile int readFlag;

// Value to store analog result
volatile int analogVal;

// Initialization
void setup(){
  // clear ADLAR in ADMUX (0x7C) to right-adjust the result
  // ADCL will contain lower 8 bits, ADCH upper 2 (in last two bits)
  ADMUX &= B11011111;
  // Set REFS1..0 in ADMUX (0x7C) to change reference voltage to the
  // proper source (01)
  ADMUX |= B01000000;
  // Clear MUX3..0 in ADMUX (0x7C) in preparation for setting the analog
  // input
  ADMUX &= B11110000;
  // Set MUX3..0 in ADMUX (0x7C) to read from AD8 (Internal temp)
  // Do not set above 15! You will overrun other parts of ADMUX. A full
  // list of possible inputs is available in Table 24-4 of the ATMega328
  // datasheet
  ADMUX |= 8;
  // ADMUX |= B00001000; // Binary equivalent
  // Set ADEN in ADCSRA (0x7A) to enable the ADC.
  // Note, this instruction takes 12 ADC clocks to execute
  ADCSRA |= B10000000;
  // Set ADATE in ADCSRA (0x7A) to enable auto-triggering.
  ADCSRA |= B00100000;
  // Clear ADTS2..0 in ADCSRB (0x7B) to set trigger mode to free running.
  // This means that as soon as an ADC has finished, the next will be
  // immediately started.
  ADCSRB &= B11111000;
  // Set the Prescaler to 128 (16000KHz/128 = 125KHz)
  // Above 200KHz 10-bit results are not reliable.
  ADCSRA |= B00000111;
  // Set ADIE in ADCSRA (0x7A) to enable the ADC interrupt.
  // Without this, the internal interrupt will not trigger.
  ADCSRA |= B00001000;
  // Enable global interrupts
  // AVR macro included in <avr/interrupts.h>, which the Arduino IDE
  // supplies by default.
  // Kick off the first ADC
  readFlag = 0;
  // Set ADSC in ADCSRA (0x7A) to start the ADC conversion
  ADCSRA |=B01000000;

// Processor loop
void loop(){

  // Check to see if the value has been updated
  if (readFlag == 1){
    // Perform whatever updating needed
    readFlag = 0;
  // Whatever else you would normally have running in loop().

// Interrupt service routine for the ADC completion

  // Done reading
  readFlag = 1;
  // Must read low first
  analogVal = ADCL | (ADCH << 8);
  // Not needed because free-running mode is enabled.
  // Set ADSC in ADCSRA (0x7A) to start another ADC conversion
  // ADCSRA |= B01000000;

You'll notice that in the initialization section, I've broken out exactly what I am writing to each bit, instead of using AVR's supplied #define statements for each bit. For example,

  // Set ADIE in ADCSRA (0x7A) to enable the ADC interrupt.
  // Without this, the internal interrupt will not trigger.
  ADCSRA |= B00001000;

could have instead been written as:

  ADCSRA |= (1 << ADIE);

However, as I was debugging this made it difficult to see exactly what state each bit was in, so I opted for binary manipulation.

I also was careful to always AND or OR the proper bit(s) into the register. For example, the line:

  ADMUX &= B11011111;

will set bit 6 to zero (clear it), while leaving all other bits where they were.  Similarly :

  ADMUX |= B01000000;

sets bit 6 to one (set it), while leaving the other bits unchanged.

The setup() function is mostly self-explanatory - register bits inside the processor are set to proper values to control the ADC functionality. If you aren't sure about any register or bit, search for it in the datasheet. Section 24.9 also has a detailed description of every register and bit related to the ADC, and is a great resource.

This code takes advantage of the AVR interrupt service routine (ISR) functionality. An ISR is just another function that is called internally by the program whenever an event (in this case, ADC_vect) occurs.

Because these events happen very quickly (in our case, about once every 100us), it is important to keep the code inside the ISR as short as possible. So, in this case, all that happens is a flag is set, and the value from the ADC stored. Any additional processing of the new value happens on the next iteration of loop(). This ensures that the interrupt function completes well before the next one occurs!

Hopefully this code is a good example on using an interrupt to reduce time spent waiting for an analogRead() to finish in standard Arduino code.

As always, if anything is incorrect, unclear, or you have any questions, don't hesitate to contact me.