ADC module reference

Header :
#include <adc.h>
Makefile :
MODULES+=adc

Description

The ADC (Analog to Digital Converter) is a peripheral dedicated to measuring voltages. Common applications include measuring the output of an analog sensor, sampling the voltage of a battery, ...

The ADC consists of different independant channels. The number of channels is package-dependant :

  • 3 for the 48-pin package
  • 7 for the 64-pin package
  • 15 for the 100-pin package
Each channel has a 12-bit resolution and can sample up to 300kS/s.

If the signal read with the ADC is unstable or incoherent, make sure that Vcc is stable and noise-free. ADCs are especially sensitive to noise on the power supply.

This module doesn't implement all the features provided by the peripheral. For more information about what the peripheral is capable of, look at the Hacking section below.

API

void init(AnalogReference analogReference=AnalogReference::VCC_OVER_2, int vref=3300)

Initialize the ADC controller. This is usually not required, since this function will be automatically called if necessary when you first enable a channel or make a measurement. However, if you want to customize the analog reference, you will need to do so by calling this function manually.

If you use an external analog reference (EXTERNAL_REFERENCE or DAC_VOUT), make sure to specify its voltage in mV as well. For VCC-related references (VCC_0625 and VCC_OVER_2), simply indicate the VCC voltage if it is not the standard 3.3V. For INTERNAL_1V, the correct value (vref=1000) will always be used.

void enable(Channel channel)
Enable the specified channel. If the module hasn't been already initialized, init() will be called automatically. Calling this function is usually not required, because it is automatically called when you first make a measurement. However, it might be useful in order to make sure that the controller is already properly initialized before making the first measurement and therefore avoid the initialization overhead.
void disable(Channel channel)
Disable the specified channel. If there is no enabled channel left at the end of this function, the controller is automatically disabled to save power.
uint16_t readRaw(Channel channel, Gain gain=Gain::X05, Channel relativeTo=0xFF)

Read the current raw value measured by the ADC on the given channel. This is a 12-bit value (between 0 and 4095) that can be interpreted according to the current configuration, especially the gain and analog reference.

If another channel number is provided in relativeTo, the measurement will be performed differentially with channel as the positive side and relativeTo as the negative side. In this case, the returned value will be mapped using the following scale : 0=-Vref, 2047=0V, 4095=+Vref.

int read(Channel channel, Gain gain=Gain::X05, Channel relativeTo=0xFF)

Read the current voltage measured by the ADC on the given channel, in mV. This function uses readRaw() above but uses the current configuration (gain and voltage reference given in init()) to calculate the actual voltage in mV.

If another channel number is provided in relativeTo, the measurement will be performed differentially with channel as the positive side and relativeTo as the negative side. In this case, the returned value will be signed.

void setPin(Channel channel, GPIO::Pin pin)
Set the GPIO pin to use for the specified channel. See the Pin muxing tutorial for more details.

Hacking

The current implementation only allows reading a single-ended or differential voltage with a customizable gain and analog reference. However, the ADC peripheral provides more powerful features such as :

  • internal timer and external pin as the sources of automatic triggers
  • DMA capabilities for automatic configuration and transfer of the conversion results in a memory buffer
  • window monitor : the ADC behaves like an analog comparator
  • Zoom mode : a part of the reference voltage is extended and mapped to the full digital range
These features could be useful for some specific applications (e.g. a digital oscilloscope?).

Some settings could also be customized in the existing module, especially around the SEQCFG register in readRaw().

Code

Header

#ifndef _ADC_H_
#define _ADC_H_

#include <stdint.h>
#include "gpio.h"
#include "error.h"

// Analog to Digital Converter
// This module is used to measure an analog voltage on a pin
namespace ADC {

    // Peripheral memory space base address
    const uint32_t ADC_BASE = 0x40038000;

    // Registers addresses
    const uint32_t OFFSET_CR =          0x000; // Control Register
    const uint32_t OFFSET_CFG =         0x004; // Configuration Register
    const uint32_t OFFSET_SR =          0x008; // Status Register
    const uint32_t OFFSET_SCR =         0x00C; // Status Clear Register
    const uint32_t OFFSET_SEQCFG =      0x014; // Sequencer Configuration Register
    const uint32_t OFFSET_CDMA =        0x018; // Configuration Direct Memory Access Register
    const uint32_t OFFSET_TIM =         0x01C; // Timing Configuration Register
    const uint32_t OFFSET_ITIMER =      0x020; // Internal Timer Register
    const uint32_t OFFSET_WCFG =        0x024; // Window Monitor Configuration Register
    const uint32_t OFFSET_WTH =         0x028; // Window Monitor Threshold Configuration Register
    const uint32_t OFFSET_LCV =         0x02C; // Sequencer Last Converted Value Register
    const uint32_t OFFSET_IER =         0x030; // Interrupt Enable Register
    const uint32_t OFFSET_IDR =         0x034; // Interrupt Disable Register
    const uint32_t OFFSET_IMR =         0x038; // Interrupt Mask Register
    const uint32_t OFFSET_CALIB =       0x03C; // Calibration Register

    // Registers fields
    const uint32_t CR_SWRST = 0;
    const uint32_t CR_TSTOP = 1;
    const uint32_t CR_TSTART = 2;
    const uint32_t CR_STRIG = 3;
    const uint32_t CR_REFBUFEN = 4;
    const uint32_t CR_REFBUFDIS = 5;
    const uint32_t CR_EN = 8;
    const uint32_t CR_DIS = 9;
    const uint32_t CR_BGREQEN = 10;
    const uint32_t CR_BGREQDIS = 11;
    const uint32_t CFG_REFSEL = 1;
    const uint32_t CFG_SPEED = 4;
    const uint32_t CFG_CLKSEL = 6;
    const uint32_t CFG_PRESCAL = 8;
    const uint32_t SR_SEOC = 0;
    const uint32_t SR_EN = 24;
    const uint32_t SR_TBUSY = 25;
    const uint32_t SR_SBUSY = 26;
    const uint32_t SR_CBUSY = 27;
    const uint32_t SR_REFBUF = 28;
    const uint32_t SEQCFG_HWLA = 0;
    const uint32_t SEQCFG_BIPOLAR = 2;
    const uint32_t SEQCFG_GAIN = 4;
    const uint32_t SEQCFG_GCOMP = 7;
    const uint32_t SEQCFG_TRGSEL = 8;
    const uint32_t SEQCFG_RES = 12;
    const uint32_t SEQCFG_INTERNAL = 14;
    const uint32_t SEQCFG_MUXPOS = 16;
    const uint32_t SEQCFG_MUXNEG = 20;
    const uint32_t SEQCFG_ZOOMRANGE = 28;


    using Channel = uint8_t;

    enum class AnalogReference {
        INTERNAL_1V = 0b000,
        VCC_0625 = 0b001, // VCC * 0.625
        EXTERNAL_REFERENCE = 0b010,
        DAC_VOUT = 0b011,
        VCC_OVER_2 = 0b100, // VCC / 2
    };

    enum class Gain {
        X1 = 0b000,
        X2 = 0b001,
        X4 = 0b010,
        X8 = 0b011,
        X16 = 0b100,
        X32 = 0b101,
        X64 = 0b110,
        X05 = 0b111, // divided by 2
    };


    // Module API
    void init(AnalogReference analogReference=AnalogReference::VCC_OVER_2, int vref=3300);
    void enable(Channel channel);
    void disable(Channel channel);
    uint16_t readRaw(Channel channel, Gain gain=Gain::X05, Channel relativeTo=0xFF);
    int read(Channel channel, Gain gain=Gain::X05, Channel relativeTo=0xFF);
    void setPin(Channel channel, GPIO::Pin pin);

}


#endif

Module

#include "adc.h"
#include "pm.h"

namespace ADC {

    // Package-dependant, defined in pins_sam4l_XX.cpp
    extern struct GPIO::Pin PINS[];

    // Bitset representing enabled channels
    uint16_t _enabledChannels = 0x0000;

    // Keep track of the state of the module
    bool _initialized = false;

    // Analog reference selected by the user
    AnalogReference _analogReference = AnalogReference::INTERNAL_1V;

    // Reference voltage value in mV
    // For VCC_0625 and VCC_OVER_2, this is simply the Vcc voltage
    int _vref = 0;


    // Initialize the common ressources of the ADC controller
    void init(AnalogReference analogReference, int vref) {
        // Voltage reference
        if (analogReference == AnalogReference::INTERNAL_1V) {
            vref = 1000;
        }
        _analogReference = analogReference;
        _vref = vref;

        // Enable the clock
        PM::enablePeripheralClock(PM::CLK_ADC);

        // Find a prescaler setting that will bring the module clock frequency below
        // the maximum specified in the datasheet (42.9.4 Analog to Digital Converter
        // Characteristics - ADC clock frequency - Max : 1.5MHz).
        // A frequency too high may otherwise result in incorrect conversions.
        unsigned long frequency = PM::getModuleClockFrequency(PM::CLK_ADC);
        uint8_t prescal = 0;
        for (prescal = 0b000; prescal <= 0b111; prescal++) {
            if ((frequency >> (prescal + 2)) <= 1500000) { // 1.5MHz
                break;
            }
        }

        // CR (Control Register) : enable the ADC
        (*(volatile uint32_t*)(ADC_BASE + OFFSET_CR))
            = 1 << CR_EN        // EN : enable ADC
            | 1 << CR_REFBUFEN  // REFBUFEN : enable reference buffer
            | 1 << CR_BGREQEN;  // BGREQEN : enable bandgap voltage reference

        // CFG (Configuration Register) : set general settings
        (*(volatile uint32_t*)(ADC_BASE + OFFSET_CFG))
            = static_cast<int>(analogReference) << CFG_REFSEL   // REFSEL : voltage reference
            | 0b11 << CFG_SPEED                                 // SPEED : 75ksps
            | 1 << CFG_CLKSEL                                   // CLKSEL : use APB clock
            | prescal << CFG_PRESCAL;                           // PRESCAL : divide clock by 4

        // SR (Status Register) : wait for enabled status flag
        while (!((*(volatile uint32_t*)(ADC_BASE + OFFSET_SR)) & (1 << SR_EN)));

        // Update the module status
        _initialized = true;
    }

    void enable(Channel channel) {
        // Set the pin in peripheral mode
        GPIO::enablePeripheral(PINS[channel]);

        // Initialize and enable the ADC controller if necessary
        if (!_initialized) {
            init();
        }
        _enabledChannels |= 1 << channel;
    }

    void disable(Channel channel) {
        // Disable the peripheral mode on the pin
        GPIO::disablePeripheral(PINS[channel]);

        // Disable the ADC controller if necessary
        _enabledChannels &= ~(uint32_t)(1 << channel);
        if (_enabledChannels == 0x0000) {
            // CR (Control Register) : disable the ADC
            (*(volatile uint32_t*)(ADC_BASE + OFFSET_CR))
                = 1 << CR_DIS;      // DIS : disable ADC
            _initialized = false;
        }
    }

    // Read the current raw value measured by the ADC on the given channel
    uint16_t readRaw(Channel channel, Gain gain, Channel relativeTo) {
        // Enable the channels if they are not already
        if (!(_enabledChannels & 1 << channel)) {
            enable(channel);
        }
        if (relativeTo != 0xFF && !(_enabledChannels & 1 << relativeTo)) {
            enable(relativeTo);
        }

        // SEQCFG (Sequencer Configuration Register) : setup the conversion
        (*(volatile uint32_t*)(ADC_BASE + OFFSET_SEQCFG))
            = 0 << SEQCFG_HWLA                                                    // HWLA : Half Word Left Adjust disabled
            | (relativeTo != 0xFF) << SEQCFG_BIPOLAR                              // BIPOLAR : single-ended or bipolar mode
            | static_cast<int>(gain) << SEQCFG_GAIN                               // GAIN : user-selected gain
            | 1 << SEQCFG_GCOMP                                                   // GCOMP : gain error reduction enabled
            | 0b000 << SEQCFG_TRGSEL                                              // TRGSEL : software trigger
            | 0 << SEQCFG_RES                                                     // RES : 12-bit resolution
            | (relativeTo != 0xFF ? 0b00 : 0b10) << SEQCFG_INTERNAL               // INTERNAL : POS external, NEG internal or external
            | (channel & 0b1111) << SEQCFG_MUXPOS                                 // MUXPOS : selected channel
            | (relativeTo != 0xFF ? relativeTo & 0b111 : 0b111) << SEQCFG_MUXNEG  // MUXNEG : pad ground or neg channel
            | 0b000 << SEQCFG_ZOOMRANGE;                                          // ZOOMRANGE : default

        // CR (Control Register) : start conversion
        (*(volatile uint32_t*)(ADC_BASE + OFFSET_CR))
            = 1 << CR_STRIG;    // STRIG : Sequencer Trigger

        // SR (Status Register) : wait for Sequencer End Of Conversion status flag
        while (!((*(volatile uint32_t*)(ADC_BASE + OFFSET_SR)) & (1 << SR_SEOC)));

        // SCR (Status Clear Register) : clear Sequencer End Of Conversion status flag
        (*(volatile uint32_t*)(ADC_BASE + OFFSET_SCR)) = 1 << SR_SEOC;

        // LCV (Last Converted Value) : conversion result
        return (*(volatile uint32_t*)(ADC_BASE + OFFSET_LCV)) & 0xFFFF;
    }

    // Return the current value on the given channel in mV
    int read(Channel channel, Gain gain, Channel relativeTo) {
        int value = readRaw(channel, gain, relativeTo);

        // Compute reference
        int vref = _vref;
        if (_analogReference == AnalogReference::VCC_0625) {
            vref = (vref * 625) / 1000;
        } else if (_analogReference == AnalogReference::VCC_OVER_2) {
            vref = vref / 2;
        } else if (_analogReference == AnalogReference::INTERNAL_1V) {
            vref = 1000;
        }

        // Convert the result to mV
        // Single-ended : value = gain * voltage / ref * 4095 <=> voltage = value * ref / (gain * 4095)
        // Differential : value = 2047 + gain * voltage / ref * 2047 <=> voltage = (value - 2047) * ref / (gain * 2047)
        if (relativeTo != 0xFF) {
            value -= 2047;
        }
        int gainCoefficients[] = {1, 2, 4, 8, 16, 32, 64};
        if (gain == Gain::X05) {
            value *= 2;
        }
        value *= vref;
        if (relativeTo != 0xFF) {
            value /= 2047;
        } else {
            value /= 4095;
        }
        if (gain != Gain::X05) {
            value /= gainCoefficients[static_cast<int>(gain)];
        }

        return value;
    }

    void setPin(Channel channel, GPIO::Pin pin) {
        PINS[channel] = pin;
    }

}