LECTURE 4 Read the value of VR (variable resistance) with A/D converter

Introduction

It's gonna be very fun to use various kinds of hardware sensors like an accelerometer with your micro computer. With those sensors, your micro computer can communicate with its surrounding environments.

What those sensors will provide us is analogue value in most cases, but micro computer itself can only manipulate digital value. So you need A/D (Analogue to Digital) converter to make use of those sensor data.

Now we're going to learn how to use A/D converter of ATMega8535 and read the output value of VR (variable resistance).

About A/D conversion

A/D converter (ADC) is the circuit to turn analogue values into digital values which can be manipulated directly by micro computer. Of course there's the circuit to do things in the reverse way (D/A converter, which means Digital to Analogue Converter), but ATMega8535 has A/D converter only. The features of the A/D converter of ATMega8535 is as follows.

  • 10 bit resolution
  • 65 - 260us conversion time
  • 8 multiplexed single ended input channels
  • 2 differential input channels with optional gain of 10x and 200x
  • 0 - Vcc ADC input voltage range
  • Selectable 2.56V ADC reference voltage
  • Free running or single conversion mode
  • Interrupt on ADC conversion complete
  • Sleep mode noise canceler

We'll use 8 pins (PA0 - PA7) which are grouped as PORTA as analogue inputs (pin numbers are 40 - 33, in case of PDIP package). These pins are often refered to as ADC0 - ADC7 in some datasheets, but avr-libc doesn't have any macros with those names.

I'm going to demonstrate how to read the value of VR (variable resistence) with this function of A/D conversion in a sample program.

VR (variable resistence) is marked by a red rectangle in the photo below.
VR (variable resistence)

Registers

Let's see the registers related to A/D conversion. Pin names can be used when you use avr-libc.

  • ADC Multiplexer Selection Register (ADMUX)

    Bit No. 7 6 5 4 3 2 1 0
    Pin name REFS1 REFS0 ADLAR MUX4 MUX3 MUX2 MUX1 MUX0
    Initial value 0 0 0 0 0 0 0 0

    Pin name Description
    REFS1-0(Reference Selection Bits) Select the voltage reference for the A/D converter.

    REFS1 REFS0 Voltage Reference
    0 0 External voltage reference through AREF
    0 1 AVCC
    1 0 Reserved
    1 1 Internal 2.56V voltage reference

    ADLAR(ADC Left Adjust Result) Specify the format of the ADC conversion result in the ADC Data Register.
    MUX4-0(Analog Channel
    and Gain Selection Bits)
    Select the combination of analog inputs to be connected to A/D converter.

    MUX4-0 Single Input Differential Input
    Positive Negative Gain
    00000 PINA0 - - -
    00001 PINA1 - - -
    00010 PINA2 - - -
    00011 PINA3 - - -
    00100 PINA4 - - -
    00101 PINA5 - - -
    00110 PINA6 - - -
    00111 PINA7 - - -
    01000 - PINA0 PINA0 x10
    01001 - PINA1 PINA0 x10
    01010(N/A when using PDIP) - PINA0 PINA0 x200
    01011(N/A when using PDIP) - PINA1 PINA0 x200
    01100 - PINA2 PINA2 x10
    01101 - PINA3 PINA2 x10
    01110(N/A when using PDIP) - PINA2 PINA2 x200
    01111(N/A when using PDIP) - PINA3 PINA2 x200
    10000 - PINA0 PINA1 x1
    10001 - PINA1 PINA1 x1
    10010 - PINA2 PINA1 x1
    10011 - PINA3 PINA1 x1
    10100 - PINA4 PINA1 x1
    10101 - PINA5 PINA1 x1
    10110 - PINA6 PINA1 x1
    10111 - PINA7 PINA1 x1
    11000 - PINA0 PINA2 x1
    11001 - PINA1 PINA2 x1
    11010 - PINA2 PINA2 x1
    11011 - PINA3 PINA2 x1
    11100 - PINA4 PINA2 x1
    11101 - PINA5 PINA2 x1
    11110 1.23V - - -
    11111 GND - - -


  • ADC Control and Status Register A (ADCSRA)

    Bit No. 7 6 5 4 3 2 1 0
    Pin name ADEN ADSC ADATE ADIF ADIE ADPS2 ADPS1 ADPS0
    Initial value 0 0 0 0 0 0 0 0

    Pin name Description
    ADEN(ADC Enable) Writing 1 to this bit enables the ADC. The power of the ADC will be turned off when this bit is set to 0.
    ADSC(ADC Start Convertion) When ADATE is 0, setting this bit to 1 starts a conversion. When ADATE is 1, setting this bit to 1 starts the first conversion.
    ADATE(ADC Auto Trigger Enable) Setting this bit to 1 enables ADC auto triggering.
    ADIF(ADC Interrupt Flag) This bit is set when a conversion completed. When ADIE is set, ADIF will be cleared automatically by executing the interrupt handler. To manually clear ADIF, write 1 to this bit.
    ADIE(ADC Interrupt Enable) Writing 1 to this bit enables interrupt on completion of conversion.
    ADPS2-0(ADC Prescaler Select Bits) Select the division factor between the XTAL freq and the input clock to the ADC by the combination of these 3 bits.

    ADPS2 ADPS1 ADPS0 Division Factor
    0 0 0 2
    0 0 1 2
    0 1 0 4
    0 1 1 8
    1 0 0 16
    1 0 1 32
    1 1 0 64
    1 1 1 128


  • ADC Data Register

    This is the register that holds the result of an ADC conversion. ADLAR bit in ADMUX affects the form of the result held in this register. When ADCL is read, this register will not be updated until ADCH is read. So when you need 2 bytes data, read ADCL first or the result might be changed.
    When you read 2 bytes at once by accessing this register as ADC, make sure interrupts do not occur.

    Only 10 bits of this register can be used, so the maximum value is 0x3ff = (reference voltage - 1 LSB).

    --- ADCH (ADLAR=0) ---

    Bit No. 7 6 5 4 3 2 1 0
    Pin name Unused Unused Unused Unused Unused Unused - -
    Initial value 0 0 0 0 0 0 0 0

    --- ADCL (ADLAR=0) ---

    Bit No. 7 6 5 4 3 2 1 0
    Pin name - - - - - - - -
    Initial value 0 0 0 0 0 0 0 0

    When ADLAR=0, the result is right adjusted.

    --- ADCH (ADLAR=1) ---

    Bit No. 7 6 5 4 3 2 1 0
    Pin name - - - - - - - -
    Initial value 0 0 0 0 0 0 0 0

    --- ADCL (ADLAR=1) ---

    Bit No. 7 6 5 4 3 2 1 0
    Pin name - - Unused Unused Unused Unused Unused Unused
    Initial value 0 0 0 0 0 0 0 0

    When ADLAR=1, the result is left adjusted.


  • Special Function IO Register(SFIOR)
    ACME/PUD/PSR2/PSR10 is not related to A/D conversion.

    Bit No. 7 6 5 4 3 2 1 0
    Pin name ADTS2 ADTS1 ADTS0 Reserved ACME PUD PSR2 PSR10
    Initial value 0 0 0 0 0 0 0 0

    Pin name Description
    ADTS2-0(ADC Auto Trigger Source) When ADATE bit in ADCSRA is set to 1, ADTS2-0 selects the source which triggers a conversion.

    ADTS2-0 Trigger Source
    000 Free Running Mode(ADSC keeps to be 1)
    001 Analog Comparator
    010 External Interrupt Request 0
    011 Timer/Counter0 Compare Match
    100 Timer/Counter0 Overflow
    101 Timer/Counter1 Compare Match B
    110 Timer/Counter1 Overflow
    111 Timer/Counter1 Capture Event

Circuit diagram

Here is the circuit diagram.

50KΩ volume is connected to PA0 pin of PORTA, and PA0 pin is pulled up by 10KΩ. With this setting, we can make sure PA0 is 0V when the value of the variable registence is 0KΩ, and PA0 is around 4.17V (Vcc x 50K/(50K + 10K), VCC=5V) when the registence of the volume is its maximum value : 50KΩ.

AVCC is connected to VCC (5V). AREF is also connected to VCC, so 0x3ff (the maximum value of the result of A/D conversion) + 1LSB corresponds to VCC (5V). If you open the volume completely, we can expect the result of A/D conversion would be around 853.182 (0x3ff x (4.17V/5V)) at maximum.

Sample program

Here is a sample program reading the value of a variable resistance using A/D conversion. You can download the source code from here. You also can download it as a tar archive of all the sample programs at this page.

This source code contains the following.

  • Makefile
  • adc.h
  • adc.c
  • util.h
  • util.c
  • usart.h
  • usart.c
  • main-int.c
  • main-noint.c

usart.h/usart.c are the same with those found in LECTURE 3 Serial communication using USART. Make file is a bit changed but is easy to understand. So here, I'll show you other 6 files below. All the source code is distributed under modified BSD license (new BSD license). See the License. Licenses for documentation and source code are described there.

If you've never done fuse bits configuration, do "make fuse" first. See the description of the sample program at LECTURE 2 Make LED blink for the "make fuse".

If you want to upload the program which uses interrupt, do "make uisp-int". If you want to use the program which does not use interrupt, do "make uisp-noint". What these 2 programs do is the same. Just the way of implementation is different.

This sample program reads the value of the variable resistance every 500ms by an A/D conversion, and then sends a string like "PINA0 input = ..." via serial communication. The result of the A/D conversion is shown in decimal. To receive the result via serial communication, connect the board with the serial port of your PC (9 pin D-SUB) using a straight cable and runs your terminal soft with the following parameters.

  • Asynchronous operation
  • Baud rate is 9600 bps
  • no parity check
  • 1 stop bit
  • 8 bits data

If you change the value of the variable resistance, the numeric part of the result string will change.

volume values that will be displayed on your console

  • adc.h
        1    #ifndef ___ADC_H_INCLUDED_
        2    #define ___ADC_H_INCLUDED_
        3    
        4    #include <avr/io.h>
        5    #include <inttypes.h>
        6    
        7    
        8    /* Maximum value of the result of A/D conversion. */
        9    #define ADC_MAX     0x3ff
       10    
       11    /* Definition of macros to select the clock value for A/D conversion. */
       12    #define ADC_CK_2    (_BV(ADPS0))
       13    #define ADC_CK_4    (_BV(ADPS1))
       14    #define ADC_CK_8    (_BV(ADPS0) | _BV(ADPS1))
       15    #define ADC_CK_16   (_BV(ADPS2))
       16    #define ADC_CK_32   (_BV(ADPS0) | _BV(ADPS2))
       17    #define ADC_CK_64   (_BV(ADPS1) | _BV(ADPS2))
       18    #define ADC_CK_128  (_BV(ADPS0) | _BV(ADPS1) | _BV(ADPS2))
       19    
       20    /* Definition of macros to select if you use completion interrupt. */
       21    #define ADC_NO_INT  0
       22    #define ADC_USE_INT 1
       23    
       24    
       25    #define adc_set_ch(ch) { ADMUX = (ch) & (_BV(MUX2) | _BV(MUX1) | _BV(MUX0)); }
       26    #define adc_wait()     loop_until_bit_is_set(ADCSRA, ADIF)
       27    #define adc_get()      ADC
       28    #define adc_start()    { ADCSRA |= _BV(ADIF); ADCSRA |= _BV(ADSC); }
       29    
       30    
       31    void adc_set_interrupt(void (*func)(void));
       32    void adc_init(uint8_t clock, int use_interrupt);
       33    
       34    
       35    #endif
       36    
       37    /* End of adc.h */
    
    
    • line 11 - 18

      Defining some macros to select the clock value for A/D conversion. Specify one of these macros to adc_init() described later.

    • line 25 - 28

      adc_set_ch() macro can be used to specify analog input as PINA0 - PINA7(or PORTA0/PA0 - PORTA7/PA7). This macro sets MUX2-0 bit of ADMUX register to the specified value, so this program only supports 8 channel single inputs.

      adc_wait() macro blocks till the completion of A/D conversion. Use this after executing adc_start() to wait for the end of A/D conversion. When A/D conversion completes, ADIF bit of ADCSRA register will be set to 1, so this macro checks the ADIF bit using loop_until_bit_is_set(sft, bit) of avr-libc.

      adc_get() macro returns the value of ADC register (A/D conversion data register). Make sure any interrupts will not occur when you use this macro. In this program, ADLAR bit of ADMUX register becomes always 0, so the result of A/D conversion is the lower 10 bits of ADC register.

      You can start A/D conversion by calling adc_start() macro, which sets the ADCS bit of ADCSRA register to 1. Before setting ADCS bit, uses _BV(ADIF) to clear the ADIF bit (this bit will be cleared by setting it to 1).

  • adc.c
        1    #include <avr/interrupt.h>
        2    #include <avr/io.h>
        3    #include <inttypes.h>
        4    
        5    #include "adc.h"
        6    
        7    
        8    static void (*adc_interrupt_handler)(void);
        9    
       10    
       11    /*
       12     * A/D conversion complete interrupt handler called by avr-libc.
       13     * This will be enabled when adc_init(???,ADC_USE_INT) is called.
       14     */
       15    ISR(ADC_vect)
       16    {
       17            adc_interrupt_handler();
       18    }
       19    
       20    
       21    /*
       22     * The setter of user function
       23     * which will be called from A/D conversion complete interrupt handler.
       24     */
       25    void
       26    adc_set_interrupt(void (*func)(void))
       27    {
       28            adc_interrupt_handler = func;
       29    }
       30    
       31    
       32    /*
       33     * Initializes A/D converter.
       34     * Specify one of the clock definition macros defined in adc.h to clock argment.
       35     *
       36     * If your program uses A/D conversion complete interrupt,
       37     * call sei() after executing this function (adc_init()).
       38     */
       39    void
       40    adc_init(uint8_t clock, int use_interrupt)
       41    {
       42            clock &= (_BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0));
       43    
       44            if (use_interrupt) {
       45                    /* Enables A/D conversion and complete interrupt */
       46                    ADCSRA = (_BV(ADEN) | _BV(ADIE) | clock);
       47            }
       48            else {
       49                    /* Enables A/D conversion */
       50                    ADCSRA = (_BV(ADEN) | clock);
       51            }
       52    }
       53    
       54    /* End of adc.c */
    
    
    • line 11 - 18

      Describes the interrupt handler, which will be used if you enable A/D conversion complete interrupt. As already explained in LECTURE 3 Serial communication using USART, you must write the code of interrupt handler like ISR(interrupt_id_macro){ ... } when you use avr-libc. The macro that means A/D conversion completion is defined as ADC_vect in avr/interrupt.h.

    • line 21 - 29 (adc_set_interrupt())

      Registers the user function. Interrupt handler of A/D conversion completion will call this user function.

    • line 32 - 52 (adc_init())

      Initializes A/D converter. You must specify ADC_CK_X macro defined in adc.h to clock argument.

      When you don't need A/D conversion complete interrupt, set 1 to ADEN bit of ADCSRA register to enable A/D conversion and set the clock value for A/D conversion at the same time. If you need A/D conversion complete interrupt, set 1 to ADIE bit of ADCSRA register additionally.

  • util.h
        1    #ifndef ___UTIL_H_INCLUDED_
        2    #define ___UTIL_H_INCLUDED_
        3    
        4    #include <avr/io.h>
        5    
        6    
        7    #define pullup() {\
        8            PORTA = 0xff; \
        9            PORTB = 0xff; \
       10            PORTC = 0xff; \
       11            PORTD = 0xff; \
       12    }
       13    
       14    void delay_250ms(void);
       15    void delay_500ms(void);
       16    int  itoa(char *buf, int target, int base, int width, char pad);
       17    
       18    
       19    #endif
       20    
       21    /* End of util.h */
    
    
  • util.c
        1    #include <util/delay.h>
        2    #include "util.h"
        3    
        4    
        5    void
        6    delay_250ms(void)
        7    {
        8            int i;
        9    
       10            for (i = 0; i < 25; i ++) {
       11                    _delay_ms(10);
       12            }
       13    }
       14    
       15    
       16    void 
       17    delay_500ms(void)
       18    {
       19            int i;
       20    
       21            for (i = 0; i < 50; i ++) {
       22                    _delay_ms(10);
       23            }
       24    }
       25    
       26    
       27    #define INT64_MAX_DIGIT 20
       28    
       29    int 
       30    itoa(char *buf, int target, int base, int width, char pad)
       31    {
       32            char tmpbuf[INT64_MAX_DIGIT + 1];
       33            char *tmppos = tmpbuf;
       34            char *pos = buf;
       35            char *left;
       36            char *right;
       37            char swap;
       38            char ch;
       39            int  length = 0;
       40            int  i;
       41    
       42            do {
       43                    *(tmppos ++) = "0123456789abcdef"[target % base];
       44                    length ++;
       45            }
       46            while (target /= base);
       47    
       48            *tmppos = '\0';
       49            left = tmpbuf;
       50            right = tmppos - 1;
       51    
       52            while (left < right) {
       53                    swap = *left;
       54                    *(left ++) = *right;
       55                    *(right --) = swap;
       56            }
       57    
       58            for (i = 0; i < width - length; i ++) {
       59                    *(pos ++) = pad;
       60            }
       61    
       62            tmppos = tmpbuf;
       63    
       64            while ((ch = *(tmppos ++))) {
       65                    *(pos ++) = ch;
       66            }
       67    
       68            *pos = '\0';
       69            return (width > length) ? width : length;
       70    }
       71    
       72    /* End of util.c */
    
    
    • line 16 - 24(delay_500ms())

      I newly defined delay_500ms() to wait 500ms using _delay_ms() provided by avr-libc.

    • line 27 - 70(itoa())

      itoa() can be used to convert an integer value to a string to be displayed. itoa() inserts the specified character while the length of the resulting string becomes the specified one. For example, itoa(buf, 10, 10, 5, '*') will return "***10".

      int
      itoa(char *buf, int target, int base, int width, char pad);
      
      buf    : The buffer to save the resulting string.
      target : The number you want to convert to a string.
      base   : The base number.
      width  : The length of the resulting string.
      pad    : The character to insert to the left of the resulting string while the length reaches the width.
      
      

      I wanted to use some functions defined in avr-libc, but if I link avr-libc into my program, it easily exceeds the size of the flash memory region (8KB in case of ATmega8535). So I implemented the necessary code by myself.

  • main-int.c(uses A/D conversion complete interrupt)
        1    #include <avr/interrupt.h>
        2    #include <avr/io.h>
        3    #include <avr/sleep.h>
        4    #include <inttypes.h>
        5    
        6    #include "adc.h"
        7    #include "usart.h"
        8    #include "util.h"
        9    
       10    
       11    static void adc_interrupt(void);
       12    
       13    
       14    int
       15    main(void)
       16    {
       17            pullup();
       18            PORTA &= ~_BV(PINA0); /* Disables PINA0 pullup. */
       19    
       20            /* (8MHz/64) = 125KHz */
       21            adc_init(ADC_CK_64, ADC_USE_INT);
       22            adc_set_interrupt(adc_interrupt);
       23            adc_set_ch(PINA0);
       24            usart_init(USART_NO_INT);
       25    
       26            sei();
       27    
       28            while (1) {
       29                    sleep_mode();
       30                    delay_500ms();
       31            }
       32    
       33            return 0; /* Just avoid compiler warnings... */
       34    }
       35    
       36    
       37    static void
       38    adc_interrupt(void)
       39    {
       40            char abuf[5];
       41            char *pos = abuf;
       42            char ch;
       43            char buf[32] = "PINA0 input =     \r\n";
       44            int index = 14;
       45    
       46            uint16_t data = adc_get();
       47    
       48            itoa(abuf, data, 10, 4, '0');
       49    
       50            while ((ch = *(pos ++))) {
       51                    buf[index ++] = ch;
       52            }
       53    
       54            usart_tx_string(buf);
       55    }
       56    
       57    /* End of main-int.c */
    
    

    main() which uses A/D conversion completion interrupt is defined in this file.

    • line 14 - 34(main())

      First of all, executes the following sequence.

      • pullup() (pullup.h, enables internal pullup of all the input port.)
      • Disables PINA0 pullup because we need it as an input.
      • adc_init() (adc.h, A/D converter initialization.)
      • adc_set_interrupt() (adc.h, registers a user function, which will be executed automatically when A/D conversion completes.)
      • adc_set_ch() (adc.h, set PINA0 as an analog input.)
      • usart_init() (usart.h, initializes USART.)

      Then, call sei() to enable system interrupts.After this, adc_interrupt() will be called every time A/D conversion complete. And then repeats the following.

      • Calls sleep_mode() and go into an idle mode.
      • Waits 500ms by using delay_500ms().

      In this file, there is no code to start A/D conversion explicitly. It uses ADC(A/D Converter) Noise Reduction feature, which enables conversion during sleep mode to reduce noise. ADC Noise Reduction can be used as following.

      • Set the ADEN bit of the ADCSRA register to 1 (starts A/D conversion).
      • Check if the ADSC bit of the ADCSRA register is 0.
      • Check if the ADATE bit of the ADCSRA register is 0 (auto conversion is disabled).
      • Set the ADIE bit of the ADCSRA register to 1 (enables A/D conversion complete interrupt).
      • Enter Idle mode (A/D conversion starts right after CPU stopped).
      • When A/D conversion completes, A/D conversion complete interrupt ocurres, and wakes up the CPU.

      In main.c, main() starts A/D conversion by calling sleep_mode(), process an interrupt by adc_interrupt(), and starts A/D conversion again by sleep_mode() after waiting 500ms.

    • line 37 - 55 (adc_interrupt())

      This is the user function which will be executed every time A/D conversion complete.

      First of all get the result of A/D conversion by adc_get(). Then use itoa() to get a 4-character string expression of the resulting value of A/D conversion. '0' will be inserted until the length of the string becomes 4 letter length. After that, puts the resulting string to the USART sending buffer by usart_tx_string().

  • main-noint.c(without A/D conversion complete interrupt)
        1    #include <avr/io.h>
        2    #include <inttypes.h>
        3    
        4    #include "adc.h"
        5    #include "usart.h"
        6    #include "util.h"
        7    
        8    
        9    #define INDEX_TO_INSERT 14
       10    
       11    
       12    int
       13    main(void)
       14    {
       15            char abuf[5];
       16            char *pos;
       17            char ch;
       18            char buf[32] = "PINA0 input =     \r\n";
       19            int index;
       20            uint16_t data;
       21    
       22            pullup();
       23            PORTA &= ~_BV(PINA0); /* Disables PINA0 pullup. */
       24    
       25            /* (8MHz/64) = 125KHz */
       26            adc_init(ADC_CK_64, ADC_NO_INT);
       27            adc_set_ch(PINA0);
       28            usart_init(USART_NO_INT);
       29    
       30            while (1){
       31                    adc_start();
       32                    adc_wait();
       33                    data = adc_get();
       34    
       35                    itoa(abuf, data, 10, 4, '0');
       36                    index = INDEX_TO_INSERT;
       37                    pos = abuf;
       38    
       39                    while ((ch = *(pos ++))) {
       40                            buf[index ++] = ch;
       41                    }
       42    
       43                    usart_tx_string(buf);
       44                    delay_500ms();
       45            }
       46    
       47            return 0; /* Just avoid compiler warnings... */
       48    }
       49    
       50    /* End of main-noint.c */
    
    

    This file is the another version that doesn't use A/D conversion complete interrupt.

    • line 31 - 32

      First of all, executes the following sequence.

      • pullup() (pullup.h, enables internal pullup of all the input port.)
      • Disables PINA0 pullup because we need it as an input.
      • adc_init() (adc.h, A/D converter initialization.)
      • adc_set_ch() (adc.h, set PINA0 as an analog input.)
      • usart_init() (usart.h, initializes USART.)

      Then enter while loop. Within while loop, executes adc_start() and adc_wait().

      adc_start();
      adc_wait();
      
      

      By this, start A/D conversion explicitly and waits it to finish. adc_wait() blocks during A/D conversion.

      After A/D conversion finished, gets the resulting value by adc_get(). Then uses itoa() and puts the resulting string to the USART sending buffer by usart_tx_string(). After that, executes delay_500ms() to wait for 500ms.

You can use various external sensors by A/D conversion. Make fun of it!