Search This Blog

Thursday, February 28, 2008

Using the Mega Analog to Digital Converter

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);

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))

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

#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:

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:

*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. Note that it assumes the files below are called main.c.

- Ron Kreymborg

No comments: