Using Analog Inputs

Teensy 2.0 and Teensy++ 1.0 & 2.0 have a 10 bit analog to digital converter (ADC) which can be used to read analog voltages, such as signals from sensors. Teensy 1.0 does not have analog inputs.

Simple ADC Usage

The simplest way to use the ADC is to manually begin a conversion, wait for it to complete, and read the result.

int16_t adc_read(uint8_t mux)
{
    uint8_t low;

    ADCSRA = (1<<ADEN) | ADC_PRESCALER;             // enable ADC
    ADCSRB = (1<<ADHSM) | (mux & 0x20);             // high speed mode
    ADMUX = aref | (mux & 0x1F);                    // configure mux input
    ADCSRA = (1<<ADEN) | ADC_PRESCALER | (1<<ADSC); // start the conversion
    while (ADCSRA & (1<<ADSC)) ;                    // wait for result
    low = ADCL;                                     // must read LSB first
    return (ADCH << 8) | low;                       // must read MSB only once!
}
Download this simple ADC code.

Analog Reference

The reference determines what range of voltages the ADC can read. In the code above, "aref" is a global static variable which configures the reference. There are 3 options.

FunctionADMUX ValueAREF Pin
Vcc (Power Supply)(1<<REFS0)Leave unconnected
Internal 2.56V(1<<REFS0)|(1<<REFS1)Leave unconnected
External Reference0Connect Voltage Reference

Choosing the right reference for your application will give you the best results. Each is intended for certain uses.

Vcc (Power Supply Voltage) Reference

Use this reference when your analog signal is created from the power supply. Reading the position of a trim pot is a good example.

If the power supply voltage changes, so will your signal, but the ADC reference will also change the ADC's range to compensate, so you will continue to get correct results.

Resistive temperature sensors can be used this way, together with an accurate resistor.

Internal 2.56V Reference

The internal reference allows you to measure specific voltages, since it will remain at (approximately) 2.56 volts, even when the power supply voltage changes. The ADC input range will be 0 to 2.56 volts.

Atmel only guarantees the internal will be between 2.4 to 2.8 volts, which is ±7.8% error. There is also no specification for it's stability over temperature changes. Often it will perform quite well, but there is no guarantee.

The internal reference is good enough for many basic applications. Often you can calibrate by measuring a known accurate voltage, and store the internal reference's actual voltage in your code or in EEPROM, then use it in calculations to correct for the error.

External Reference

When you need accurate voltage measurements, an external voltage reference chip is required. Because it affects the entire range of the ADC, the accuracy of your measurements depends on the accuracy of the voltage reference. Even inexpensive reference chips outperform the internal reference. For example, a LM385BLP-2.5 has ±1.5% initial accuracy (no calibration and adjustment), and a LT1009 has ±0.2%, both with very good stability over temperature changes.

You can buy voltage reference chips in different voltages. 4.096 volts can be nice, because it gives a larger signal range, and each step of the ADC is exactly 4 mv. The external reference must be less than or equal to the power supply voltage.

Configuring The Pins

The analog input pins can also be used as normal digital I/O pin. When a pin is used for analog input, it should be in input mode without the pullup resistor, so it doesn't interfere with the incoming analog signal. This is the default mode, so normally you don't need to do anything.

However, analog signals can cause the digital input circuitry to consume extra power. The chip provides a way to disable the input circuits on the analog pins. This isn't necessary to make analog signals work, but it is nice to avoid wasting power, especially if running from a battery.

RegisterBit 7Bit 6Bit 5Bit 4 Bit 3Bit 2Bit 1Bit 0
DIDR0Pin F7Pin F6Pin F5Pin F4 Pin F3Pin F2Pin F1Pin F0
DIDR2Pin B6Pin B5 Pin B4Pin D7Pin D6Pin D4

Normally you would write to these registers near the beginning of your program, with a 1 in each bit corresponding to a pin where you have connected an analog signal. For example:

    DIDR0 = 0x13;   // Pins F4, F1, F0 have analog signals
    DIDR2 = 0x04;   // Pin B4 also has an analog signal

Sampling At Free Running Speed

Sometimes you may want to repeatedly measure a signal as fast as possible. Often it's important for every measurement to be taken at consistent time intervals, called "sampling". Fortunately, the ADC has auto triggering modes, where it will begin each conversion automatically. The default auto triggering mode begins each new conversion immediately after the prior one finishes.

Because measurements are taken at high speed, usually an interrupt is needed to automatically read the result and store it into a buffer. Your program can perform other actions which may take longer than one ADC conversion time while the interrupt code takes care of reading the data. Later you can read the measurements from the buffer.

Download this auto trigger example code.

First, a buffer to store the samples is needed. This needs to be large enough to accomodate the longest delay your program may spend before it removes the data.

static volatile uint8_t head, tail;
static volatile int16_t buffer[BUFSIZE];

Unlike the simple example which accesses the ADC every time, with auto triggering you only need to configure the ADC once, and then it will operate on its own.

void adc_start(uint8_t mux, uint8_t aref)
{
    ADCSRA = (1<<ADEN) | ADC_PRESCALER;     // enable the ADC, interrupt disabled
    ADCSRB = (1<<ADHSM) | (mux & 0x20);
    ADMUX = aref | (mux & 0x1F);            // configure mux and ref
    head = 0;                               // clear the buffer
    tail = 0;                               // and then begin auto trigger mode
    ADCSRA = (1<<ADSC) | (1<<ADEN) | (1<<ADATE) | (1<<ADIE) | ADC_PRESCALER;
    sei();
}

Every time a measurement is made, the ADC will generate an interrupt. There are many ways you might choose to store the data. This example uses a circular buffer with a head and tail index.

ISR(ADC_vect)
{
    uint8_t h;
    int16_t val;

    val = ADC;                      // grab new reading from ADC
    h = head + 1;
    if (h >= BUFSIZE) h = 0;
    if (h != tail) {                // if the buffer isn't full
        buffer[h] = val;            // put new data into buffer
        head = h;
    }
}

When your program is ready to work with the data, it simply removes the data from the buffer. If you have spent substantial time doing other tasks, there may be many samples in the buffer.

int16_t adc_read(void)
{
    uint8_t h, t;
    int16_t val;

    do {
        h = head;
        t = tail;                   // wait for data in buffer
    } while (h == t);
    if (++t >= BUFSIZE) t = 0;
    val = buffer[t];                // remove 1 sample from buffer
    tail = t;
    return val;
}

Sampling At External Trigger

Instead of taking measurements at maximum speed, it can be useful to automatically take a measurement when some an occurs. In this example, the INT0 pin is used.

Download this external trigger example code.

Configuring the ADC is similar to free running mode above, but with a few small changes. ADCSRB gets addition bits to configure the automatic trigger mode. When writing to ADCSRA to enable the auto trigger mode and interrupt, the start conversion bit is not written. The first conversion will occur when INT0 changes. Finally, INT0 needs to be configured for the desired edge type.

void adc_start(uint8_t mux, uint8_t aref)
{
    ADCSRA = (1<<ADEN) | ADC_PRESCALER;     // enable the ADC, interrupt disabled
    ADCSRB = (1<<ADHSM) | (mux & 0x20) | ADC_TRIGGER_EXT_INT0;
    ADMUX = aref | (mux & 0x1F);            // configure mux and ref
    head = 0;                               // clear the buffer
    tail = 0;                               // and then begin auto trigger mode
    ADCSRA = (1<<ADEN) | (1<<ADATE) | (1<<ADIE) | ADC_PRESCALER;
    EICRA |= ((1<<ISC01) | (1<<ISC00));     // config ext INT0 rising edge
    sei();
}

The only other minor change required is clearing the INT0 flag. This is the same flag that would cause an INT0 interrupt, and would be cleared automatically by that interrupt. But the INT0 interrut is not actually enabled (the ADC interrupt is), so this bit needs to be cleared manually. Some interrupt flags, like INT0, are actually cleared by writing a 1 to them. Yes, it seems strange, but that's the way Atmel designed the chip, and documented it in the datasheet.

ISR(ADC_vect)
{
        uint8_t h;
        int16_t val;

        val = ADC;                      // grab new reading from ADC
        EIFR = 0x01;                    // clear INT0 flag

Events from timers can also be used to trigger the ADC. The process is similar, with manual clearing of the flag, and of course setup for the timer to create the events.

Sampling Multiple Channels

TODO: write this section... how to switch the mux while sampling.

Higher Speed Conversions

TODO: write this section... ways to run faster, and do some actual tests to get an idea of what the real performance is (datasheet doesn't specify)

Differential Inputs & Amplifier

TODO: write this section... how to use the differential amplifier, also example of sign extending the result

Ground Loop Problems

TODO: write this section... examples of ground current causing noisy readings. Wire up actual example of PWM driving LEDs on same board as reading small voltage with diff amp at 200X gain, show "before" and "after".

Source Impedance Problems

TODO: write this section... examples (and actual tests) of noise in readings due to high source impedance. How to know when an amplifier is necessary and when you can cheat by just adding a capacitor (and how to compute the loss of bandwidth in doing so).