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.

Sunday, October 26, 2014

Let's do it by Java

Today we wanted to compile native C source code for atmega2560 and run it on arduino. But we didn't want to use Arduino IDE. We wanted to make the compilation process complete automatic by using Java. To make the long story short:

First we had to find out how to compile source code for Arduino Mega2560 from command line. Second use Java to run commands.

Our sample program will be simple blinking application. The LED located at Arduino Mega2560 board (pin PB7 represented as pin 13 on Arduino) will blink at 1Hz frequency.

 #define F_CPU 16000000UL  
 #include <avr/io.h>  
 #include <util/delay.h>  
 #define SET_BIT(Port, Bit) Port |= (1 << Bit)  
 #define CLEAR_BIT(Port, Bit) Port &= ~(1 << Bit)  
 int main() {  
      SET_BIT(DDRB, 7);  
      while(1) {  
           SET_BIT(PORTB, 7);  
           _delay_ms(500);  
           CLEAR_BIT(PORTB, 7);  
           _delay_ms(500);  
      }  
      return 0;       
 }  

Save this source code into the file called source.c. Now compile it by invoking this command:

 avr-gcc -mmcu=atmega2560 -Os -Wall -osource.elf source.c  

This will compile the source.c file and store the result of compilation in file called source.elf. The next step is to extract portions of the binary (source.elf) and save the information into .hex files. The GNU utility that does this is called avr-objcopy.

 avr-objcopy -j.text -j.data -Oihex source.elf source.hex  

And now just upload it by using avrdude to Arduino:

 avrdude -CC:\tools\avr\etc\avrdude.conf -cwiring -patmega2560 -PCOM6 -b115200 -F -Uflash:w:source.hex -D  

Note: the -D option means disable chip erasing before programming. But the bootloader erases the flash by default and the erase is probably not implemented by the bootloader because the avrdude fail to upload the hex file without the -D option. It will print the following error.

 avrdude: NOTE: FLASH memory has been specified, an erase cycle will be performed  
      To disable this feature, specify the -D option.  
 avrdude: erasing chip  
 avrdude: stk500v2_command(): command failed  

If the upload process was successful the LED should start to blink.

Now we are ready to implement the compilation and upload process in Java. The preferred way of invoking commands in Java is to use ProcessBuilder. One thing to mention is, always read the process output. Otherwise the output buffer may become full and the process will stop until the buffer becomes empty.

 package com.mechatronichacks.arduino;  
 import java.io.File;  
 import java.io.IOException;  
 import java.util.Scanner;  
 public class ArduinoUpload {  
      public ArduinoUpload() {  
           try {  
                int result = this.execute(new String[]{"C:\\tools\\avr\\bin\\avr-gcc", "-mmcu=atmega2560", "-Os", "-Wall", "-osource.elf", "source.c"});  
                if(result == 0) {  
                     System.out.println("Compilation successful");  
                }  
                if(result == 0)     {  
                     result = this.execute(new String[]{"C:\\tools\\avr\\bin\\avr-objcopy", "-j.text", "-j.data", "-Oihex", "source.elf", "source.hex"});  
                }  
                if(result == 0) {  
                     System.out.println("Linking successful");  
                }  
                if(result == 0) {  
                     result = this.execute(new String[]{"C:\\tools\\avr\\bin\\avrdude", "-CC:\\tools\\avr\\etc\\avrdude.conf", "-cwiring", "-patmega2560", "-PCOM6", "-b115200", "-F", "-Uflash:w:source.hex", "-D"});       
                }  
                if(result == 0) {  
                     System.out.println("Upload successful");  
                }  
           } catch (IOException e) {  
                e.printStackTrace();  
           } catch (InterruptedException e) {  
                e.printStackTrace();  
           }  
      }  
      private int execute(String[] command) throws IOException, InterruptedException {  
           ProcessBuilder builder = new ProcessBuilder(command);  
           //now set the working directory for process execution  
           builder.directory( new File("C:\\MechatronicHacks\\").getAbsoluteFile() );   
           builder.redirectErrorStream(true);  
            Process process = builder.start();  
           Scanner s = new Scanner(process.getInputStream());  
           StringBuilder text = new StringBuilder();  
           while (s.hasNextLine()) {  
                text.append(s.nextLine());  
                text.append("\n");  
           }  
           s.close();  
           System.out.println(text);  
           return process.waitFor();  
      }  
      public static void main(String[] args) {  
           new ArduinoUpload();  
      }  
 }