Using the Mega103 AtoD Converter

Ron Kreymborg

This example will show how to configure and use the Analog to Digital Converter (ADC) in the Atmel Mega103 micro. It will also show how to use the sleep instruction in conjunction with the ADC to minimise digital noise from the cpu. I will start with the simplest non-interrupt code and progress to using interrupts and power saving techniques. I assume you have the Mega103 Manual open at the section on the ADC for reference.

The '103 has a ten bit analog to digital converter (ADC) as part of its peripheral equipment. It includes simple multiplexer control and configuration and there is no specific initialisation required. Starting a reading consists of selecting the channel and setting the ADSC bit in the ADCSR control register:

    outp(channel, ADMUX);
    outp(ADC_CONTROL | 1<<ADSC, ADCSR);

The ADC_CONTROL is a local define that combines the specific ADCSR bits I am using here.

#define ADC_CONTROL   (1<<ADEN | 1<<ADPS2 | 1<<ADPS0)

This enables the ADC and selects a divide by 32 clock which, with a 4 Mhz cpu clock, gives an ADC clock of 125 Khz. The specification quotes 2 bits accuracy at 200 Khz, so there is little advantage in a slower clock. Note that constants like ADEN are defined as a bit position (ADEN=7), whereas I am going to use this define in an outp instruction where the same bit is represented by the unsigned byte 0x80. If you shift 1 left 7 times you get 0x80. Hence the strange looking 1<<ADEN.

When the conversion is complete the ADIF bit will go high. Thus the wait-until-done code is:

    while (!(inp(ADCSR) & 1<<ADIF))
        {;}
    sbi(ADCSR, ADIF);

This loop will exit when the conversion is complete. The last line clears the ADIF bit by setting it (Atmel weird!). Note that an instruction like sbi requires the bit position, so there is no need for funny shifts. All that is then required is to read the result into an integer variable. Because this is 10-bits, it requires a two stage read. You must first read the low byte, then the high byte:

    temp = inp(ADCL);
    result = temp;
    temp = inp(ADCH);
    result |= temp<<8;

Or you could rely on avrgcc processing from left to right (check the assembly listing) and do this in one line:

    result = inp(ADCL) | inp(ADCH)<<8;

In most situations you will be reading a set of channels into an integer array of results. You could enable the ADC and define the clock speed first before entering the loop:

    outp(ADC_CONTROL, ADCSR);                   // enable the ADC and set clock speed
    for (i=0; i<8; i++)
    {
        outp(i, ADMUX);                         // set the channel
        sbi(ADCSR, ADSC);                       // start the conversion
        while (!(inp(ADCSR) & 1<<ADIF))         // loop until finished
            {;}
        sbi(ADCSR, ADIF);                       // clear the ADIF bit
        Sample[i] = inp(ADCL) | inp(ADCH)<<8;   // read in the 10-bit result
    }

This works well but does not take advantage of the low noise capability of the '103 where the cpu can be turned off during the conversion. However, to use this mode requires enabling interrupts.

Using interrupts actually makes for simpler code once you decide to bite the bullet. The following code snippet assumes you have enabled global interrupts elsewhere. The ADC uses the sleep instruction in Idle mode (see Using the Sleep Instruction) as the clocks must still run but the cpu can be turned off. Note that I have re-defined the ADC_CONTROL variable to include the interrupt enable.

#define ADC_CONTROL   (1<<ADEN | 1<<ADIE | 1<<ADPS2 | 1<<ADPS0)

    cbi(MCUCR, SM0);                          // Idle mode for sleeps
    cbi(MCUCR, SM1);
    sbi(MCUCR, SE);
    outp(ADC_CONTROL, ADCSR);                 // set clock speed and enable interrupt
    for (Index=0; Index<8; Index++)
    {
        cbi(ADCSR, ADEN);                     // turn off ADC
        outp(Index, ADMUX);                   // select the channel
        outp(ADC_CONTROL | 1<<ADSC, ADCSR);   // turn ADC on and start conversion
        asm("sleep"::);                       // off to sleep until done
    }

The interrupt function will read the conversion result for us. This function is simply:

SIGNAL(SIG_ADC)
{
    Sample[Index] = inp(ADCL) | inp(ADCH)<<8;
}

The global variables Index and Sample are defined as:

static volatile int Sample[8];      // a/d converter samples
static volatile BYTE Index;         // current sample

You need to be sure that no other interrupt can occur during the conversion other than the ADC itself, as any interrupt will wake up the processor and the current sample will be lost. In a complex system where perhaps internal timers and/or external events are using interrupts for response, it is very difficult to ensure an asynchronous interrupt will not occur during a conversion. Designing around this can be very difficult where low noise is paramount. Because you must use the Idle sleep mode to ensure noise from the cpu does not interfere, you must run the ADC under interrupt control. Thus you need to come up with a system that disables interrupts from any peripheral where they could occur asynchronously, before starting the ADC sample sequence. The multi module version of these ADC examples that uses TaskR for task management shows one possible solution.

In most cases the results captured in the ADC handler must be stored elsewhere for subsequent processing. Most likely a mathematical conversion will be required to present the results in SI units. These conversions and transfers are usually done after the sample set is taken to ensure the measurements themselves are as close together in time as possible. Where speed or storage are at a premium, you could use global pointers to manage the storage task. It is also possible to store the data by channel (rather than by sample data set) using an array of pointers. For example the ADC control loop would remain the same, but the interrupt routine would look something like:

SIGNAL(SIG_ADC)
{
    *Pointer[Index]++ = inp(ADCL) | inp(ADCH)<<8;
}

where Pointer[0] points at the data storage area for channel 0, Pointer[1] for channel 1, etc. By incrementing the pointer in each case the data for each channel will go into contiguous arrays. Of course there would need to be some test that the arrays had eventually filled and the pointers reset to the start of the buffers.

The following two examples provide a convenient way to test these algorithms on your STK300 or similar board. Simply cut and past from the browser into your favourite editor. You can download the makefile for both examples. Note that it assumes the files below are called main.c.



Simple Idle Mode ADC Example

The first ADC control example uses the Idle sleep mode and a simple delay loop between samples, and the use of a pointer to write to the output array. The latter is hardly warranted in this case, but if the output array was two-dimensional it would make sense. Note the use of sbi/cbi instructions setting bits on PORTA. This is a very good debug tool if you have access to an oscilloscope.

/******************************************************************************

 Sample program to demonstrate using the Mega103 analog to digital
 converter (ADC) in conjunction with the simple sleep instruction to 
 minimize power use.
 
 For demonstration purposes, the high 8 bits of channel 3 (10-2) are written
 to PORTB, the set of LEDs on an STK300.
 
 Ron Kreymborg
 May 2001
 
******************************************************************************/

#include <stdlib.h>
#include <interrupt.h>
#include <signal.h>

#define BYTE            unsigned char
#define ADC_CONTROL     (1<<ADEN | 1<<ADIE | 1<<ADPS2 | 1<<ADPS0)

int main(void);
void AtoDconverter(void);
void Delay(void);

static volatile int Sample[8];      // a/d converter samples
static volatile int *Pointer[8];
static volatile BYTE Index;

int main(void)
{
    int i, value;
    
    // PORTA
    outp(0x00, PORTA);              // all low
    outp(0xff, DDRA);               // all output
    // PORTB
    outp(0xff, PORTB);              // all low
    outp(0xff, DDRB);               // all output
    sbi(ACSR, ACD);                 // disable comparator
    cbi(MCUCR, SM0);
    cbi(MCUCR, SM1);
    sbi(MCUCR, SE);

    for (i=0; i<8; i++)
        Pointer[i] = &Sample[i];    // initialise pointers
        
    Delay();                        // allow clocks to settle
    sei();                          // hello world
    
    while (1)
    {
        AtoDconverter();            // take a sample
        value = ~(Sample[2]>>2);    // display channel 3 on the STK300
        outp(value, PORTB);         //   set of leds
        Delay();
    }
    
    return 0;
}

//---------------------------------------------------------------------
// Control the ADC for an eight channel sample using the inbuilt noise
// cancelling capability.
//
void AtoDconverter(void)
{
    sbi(PORTA, 1);                           // for scoping
	
    outp(ADC_CONTROL, ADCSR);
    for (Index=0; Index<8; Index++)
    {
        sbi(PORTA, 3);                       // for scoping
        cbi(ADCSR, ADEN);                    // turn off ADC
        outp(Index, ADMUX);                  // select the channel
        outp(ADC_CONTROL | 1<<ADSC, ADCSR);  // turn ADC on and start conversion
            
        // The read is done in the interrupt routine. Here we go to
        // sleep in Idle mode until the conversion is done.
        //
        asm("sleep"::);
        cbi(PORTA, 3);                       // for scoping
    }

    cbi(PORTA, 1);                           // for scoping
}

//---------------------------------------------------------------------
// Arbitrary inter-sample delay.
//
void Delay(void)
{
    int i, j;
    
    for (i=0; i<2000; i++)
        j = i;
}

//---------------------------------------------------------------------
// ADC conversion complete interrupt
//
SIGNAL(SIG_ADC)
{
    *Pointer[Index] = inp(ADCL) | inp(ADCH)<<8;
}


Idle and Power Save ADC Example

The second ADC control example is rather more ambitious and uses both the Idle and Power Save sleep modes. The sample period is set via an interrupt from Timer0.

/******************************************************************************

 Sample program to demonstrate using the Mega103 analog to digital converter 
 (ADC) in conjunction with the Idle sleep mode during sampling and Power Save
 sleep mode between samples for lowest possible power when the cpu must restart
 itself. Note that if the cpu can be interrupted by an external event, you could 
 use the Power Down mode for even lower power. 
 
 For demonstration purposes, the high 8 bits of channel 3 (10-2) are written
 to PORTB, the set of LEDs on an STK300.
 
 Ron Kreymborg
 May 2001
 
******************************************************************************/

#include <stdlib.h>
#include <interrupt.h>
#include <signal.h>

#define BYTE            unsigned char
#define SLEEP           asm("sleep"::)
#define CLK0_DIVIDER    0x05            // PCK0 / 128
#define CLK0_COUNT      256-128         // 500 mSec tick
#define ADC_CONTROL     (1<<ADEN | 1<<ADIE | 1<<ADPS2 | 1<<ADPS0)

typedef enum
{
    UNDEFINED_MODE,
    POWER_SAVE_MODE,
    IDLE_MODE
} MODE;

int main(void);
void AtoDconverter(void);
void SetPowerSaveMode(MODE type);
void InitAtoD(void);
void InitTicker(void);
void StartupDelay(void);

static volatile int Sample[8];      // a/d converter samples
static volatile BYTE Index;         // current sample

int main(void)
{
    // PORTB
    outp(0xff, PORTB);              // all low
    outp(0xff, DDRB);               // all output
    sbi(ACSR, ACD);                 // disable comparator

    InitTicker();                   // configure Timer0
    InitAtoD();                     // init the A/D converter

    StartupDelay();                 // allow clocks to settle
    sei();                          // hello world
    
    AtoDconverter();                // never comes back
    
    return 0;
}

//---------------------------------------------------------------------
//
void AtoDconverter(void)
{
    BYTE value;
    
    while (1)
    {
        SLEEP;                                   // power save sleep

        // Ensure sleep mode during conversions is Idle mode.
        //
        SetPowerSaveMode(IDLE_MODE);
        
        // Take the current sample readings
        //
        for (Index=0; Index < 8; Index++)
        {
            cbi(ADCSR, ADEN);                    // turn off ADC
            outp(Index, ADMUX);                  // select the channel
            outp(ADC_CONTROL | 1<<ADSC, ADCSR);  // turn ADC on and start conversion
            SLEEP;                               // idle sleep
        }
        
        // Restore sleep mode to Power Save for maximum power savings.
        //
        SetPowerSaveMode(POWER_SAVE_MODE);
		
        value = ~(Sample[2]>>2);                // display channel 3
        outp(value, PORTB);
    }
}

//-----------------------------------------------------------------------------
//
void InitAtoD(void)
{
    // Enable ADC with a 125,000 KHz clock
    //
    outp(ADC_SAMPLE, ADCSR);
    
    // Setup to start in Power Save sleep mode
    //
    SetPowerSaveMode(POWER_SAVE_MODE);
}

//---------------------------------------------------------------------
// Setup the MCUCR register as requested.
//
void SetPowerSaveMode(MODE type)
{
    switch (type)
    {
    case POWER_SAVE_MODE:
        sbi(MCUCR, SM0);
        sbi(MCUCR, SM1);
        sbi(MCUCR, SE);
        break;
        
    case IDLE_MODE:
        cbi(MCUCR, SM0);
        cbi(MCUCR, SM1);
        sbi(MCUCR, SE);
        break;
    }
}

//---------------------------------------------------------------------
// Initialise the Timer0 to provide a sample period
//
void InitTicker(void)
{
    sbi(ASSR, AS0);                 // clock from external crystal
    outp(CLK0_DIVIDER, TCCR0);
    outp(CLK0_COUNT, TCNT0);
    sbi(TIMSK, TOIE0);              // enable Timer0 interrupts
}

//---------------------------------------------------------------------
// Startup delay
//
void StartupDelay(void)
{
    int i, j;
    
    for (i=0; i<200; i++)
        j = i;
}


//-----------------------------------------------------------------------------
//        INTERRUPT HANDLERS
//-----------------------------------------------------------------------------
//
//---------------------------------------------------------------------
// The event that triggers an A/D sample.
//
SIGNAL(SIG_OVERFLOW0)
{
    outp(CLK0_COUNT, TCNT0);        // reset counter
}

//-----------------------------------------------------------------------------
// The current AtoD conversion has completed. Copy the value (in ADCH and ADCL)
// to the current array location.
//
SIGNAL(SIG_ADC)
{
    // Read the ADC into the current entry
    //
    Sample[Index] = inp(ADCL) | inp(ADCH)<<8;
}