Sunday, November 2, 2014

Conquer Arduino's ADC


We ware writing source code for Arduino in a pure C and we wanted to use analog to digital converter (ADC). Nobody likes writing the same source code again and again. So we wanted to make a simple template/library. Our requirements ware:

  • the ADC conversion have to be simply implemented
  • we would not control the ADC conversion, it must be completely automatic
  • the variables that hold analog values can not be randomly changed
  • the variables that hold analog values will be updated only after we let it to do so

First we need to initialize the Arduino's ADC. Datasheet for ATMega2560 stays that the ADC clock frequency should be between 50 - 250 kHz. Values around 50 kHz will give us more accurate measurements whereas values around 250 kHz will lead to less precise measurements but we get a higher conversion speed. So we have to negotiate between speed and precision. For a best compromise we are going to use value somewhere between.

Arduino Mega has a 16MHz crystal that drives processor frequency. This frequency is also used as clock for AD converter. The AD converter can't operate at such high frequency so we need to reduce the clock speed by setting the ADC prescaler. We want a clock frequency between 50 - 250kHz. The best prescaler for this purpose is 128.  This way we get ADC clock frequency at 125 kHz.

      ADCSRA = 0x8F;      //Enable the ADC and its interrupt feature  
                        //and set the ACD clock pre-scalar to clk/128  
                        //at 16Mhz/128 = 125 000 Hz: suggested frequency 50 - 250 Khz  

Next thing is to set up the Arduino's ADC according it's connection to real world. If we look at the Arduino Mega schematic we see that there is a 100n capacitor between AREF (pin 98) pin and GND. Then the AVCC pin is connected to +5V what is the voltage reference we want.



      ADMUX = 0x40; //Select AVCC with external capacitor at AREF pin  
                     //right aligned result and select ADC0 as input channel  

After the init set up we let the ADC to start conversion:

      ADCSRA |= (1<<ADSC); //Start first Conversion  

The whole init source code looks as follows:

 void initADC() {  
      ADCSRA = 0x8F; //Enable the ADC and its interrupt feature  
                      //and set the ACD clock pre-scalar to clk/128  
                      //at 16Mhz/128 = 125 000 Hz: suggested frequency 50 - 250 Khz  
      ADMUX = 0x40;  //Select AVCC with external capacitor at AREF pin  
      setADCChannel();   //and select ADC0 as input channel   
      ADCSRA |= (1<<ADSC); //Start first Conversion  
 }  

Now we have to process the conversion results. Each time a conversion finishes an interrupt service
routine ISR(ADC_vect) will takes place. In ISR we need to get data from ADC registers. We have selected the right adjusted result so we have to read data in following order first ADCL register and just then ADCH. Otherwise the next conversion after ISR will not start!

Note from AT-Mega 2560 Datasheet
When ADCL is read, the ADC Data Register is not updated until ADCH is read. Consequently, if the result is left adjusted and no more than 8-bit precision (7 bit + sign bit for differential input channels) is required, it is sufficient to read ADCH. Otherwise, ADCL must be read first, then ADCH.

      adc_result = ADCL;  
      adc_result |= (ADCH << 8);  

After invoking this few lines the adc_result variable holds the result of ADC conversion. We are not going to convert the result to measured voltage right now because we want the ISR to ends as soon as possible.

Another part of ISR is to prepare conversion on next ADC channel.

We are running conversion only on selected channels. We are selecting channels by inserting theirs number into the adc_channels array. Now after each conversion we run conversion on next ADC channel. To select next ADC channel we use a global variable adc_channels_index. The adc_channels_index is incremented in each ISR call and if we reach the end of adc_channels array we jump on index 0 and begin to process ADC channels from begin. To set the ADC channel we use
the following function.

 void setADCChannel() {  
      unsigned char selectedChannel = adc_channels[adc_channels_index];  
      /*  
       * if we are invoking conversion on ADC channel greater than 7  
       * we have to set bit MUX5 in ADCSRB register. Otherwise we clear  
       * that bit.  
       */  
      if(selectedChannel > 7) {  
           SET_BIT(ADCSRB, MUX5);  
      } else {  
           CLEAR_BIT(ADCSRB, MUX5);  
      }  
      selectedChannel &= 0b00000111;  
      ADMUX = (ADMUX & 0xF8) | selectedChannel;  
 }  

The whole ISR source code looks as follows:

 ISR(ADC_vect) {  
      unsigned int adc_result = 0;  
      adc_result = ADCL;  
      adc_result |= (ADCH << 8);  
      adc_value[adc_channels_index] = adc_result;  
      adc_channels_index++;  
      if(adc_channels_index >= channels_count) {  
           adc_channels_index = 0;  
      }  
      setADCChannel();  
      ADCSRA |= (1<<ADSC); //Start next Conversion  
 }  

Now we are ready to convert ADC values to measured voltage on Arnuino's inputs. The referenced voltage is 5V and we are using 10bit ADC converter so the ADC constant is given by equation:

 ADC_CONST = 5V/1024steps = 0.0048828125 V/step  

To convert ADC value stored in adc_value array we need only one line of code:

 double voltageOnADC0 = adc_value[0]*ADC_CONST;  

According to one of our requirements we want to update variables only after we let it to do so. So we insert the conversion equations into function called updateADC and if we want to update AD values we simply call this function and each variable that holds converted AD value will be updated to latest data available.

 void updateADC() {  
      voltageOnADC0 = adc_value[0]*ADC_CONST;  
 }  

The following source contains complete source code for AT Mega 2560 microprocesors. It measure regulary data on AD channels 1,2,8 and 15. Then the conversion from ADC to voltage takes place by calling updateADC function.

 #define F_CPU 16000000UL  
 #include <avr/io.h>  
 #include <util/delay.h>  
 #include <avr/interrupt.h>  
 #define ADC_CONST 0.0048828125  
 #define SET_BIT(Port, Bit) Port |= (1 << Bit)  
 #define CLEAR_BIT(Port, Bit) Port &= ~(1 << Bit)  
 volatile unsigned char adc_channels[] = {1, 2, 8, 15};  
 volatile unsigned char channels_count = 4;  
 volatile unsigned int adc_value[] = {0, 0, 0, 0};  
 volatile unsigned char adc_channels_index = 0;  
 double voltageOnADC1 = 0.0;   
 double voltageOnADC2 = 0.0;   
 double voltageOnADC8 = 0.0;   
 double voltageOnADC15 = 0.0;   
 void setADCChannel() {  
      unsigned char selectedChannel = adc_channels[adc_channels_index];  
      if(selectedChannel > 7) {  
           SET_BIT(ADCSRB, MUX5);  
      } else {  
           CLEAR_BIT(ADCSRB, MUX5);  
      }  
      selectedChannel &= 0b00000111;  
      ADMUX = (ADMUX & 0xF8) | selectedChannel;  
 }  
 ISR(ADC_vect) {  
      unsigned int adc_result = 0;  
      adc_result = ADCL;  
      adc_result |= (ADCH << 8);  
      adc_value[adc_channels_index] = adc_result;  
      adc_channels_index++;  
      if(adc_channels_index >= channels_count) {  
           adc_channels_index = 0;  
      }  
      setADCChannel();  
      ADCSRA |= (1<<ADSC); //Start next Conversion  
 }  
 void initADC() {  
      ADCSRA = 0x8F;      //Enable the ADC and its interrupt feature  
                //and set the ACD clock pre-scalar to clk/128  
                //at 16Mhz/128 = 125 000 Hz: suggested frequency 50 - 250 Khz  
      ADMUX = 0x40;     //Select AVCC with external capacitor at AREF pin  
                //and select ADC0 as input channel   
      setADCChannel();  
      ADCSRA |= (1<<ADSC); //Start first Conversion  
 }  
 void updateADC() {  
      voltageOnADC1 = adc_value[0]*ADC_CONST;  
      voltageOnADC2 = adc_value[1]*ADC_CONST;  
      voltageOnADC8 = adc_value[2]*ADC_CONST;  
      voltageOnADC15 = adc_value[3]*ADC_CONST;  
 }  
 int main() {  
      SET_BIT(DDRL, 7);  
      SET_BIT(DDRL, 6);  
      SET_BIT(DDRL, 5);  
      SET_BIT(DDRL, 4);  
      initADC();  
      while(1) {  
           cli(); //Disable Global Interrupts  
           updateADC();  
           sei(); //Enable Global Interrupts  
           if(voltageOnADC1 >= 2.5) {  
                SET_BIT(PORTL, 7);  
           } else {  
                CLEAR_BIT(PORTL, 7);  
           }  
           if(voltageOnADC15 >= 2.5) {  
                SET_BIT(PORTL, 6);  
           } else {  
                CLEAR_BIT(PORTL, 6);  
           }  
           if(voltageOnADC8 >= 2.5) {  
                SET_BIT(PORTL, 5);  
           } else {  
                CLEAR_BIT(PORTL, 5);  
           }  
           if(voltageOnADC2 >= 2.5) {  
                SET_BIT(PORTL, 4);  
           } else {  
                CLEAR_BIT(PORTL, 4);  
           }  
           _delay_ms(500);  
      }  
 }  

Hope you enjoy this blog post and if you want to learn more follow us on Twitter.

No comments:

Post a Comment