Analog to Digital Conversion (ADC)

The PIC32’s Analog to Digital Converter (ADC) is surprisingly difficult to set up if you’re using it for the first time. Like so many of the PIC peripherals, the number of configuration options seem endless. This tutorial will help you to understand some of the fundamental ADC options and configure the ADC in two different ways.

PIC32 ADC Basics

Fig. 1: ADC block diagram, figure 21-1 in datasheet

The 10-bit ADC can be split into two sections: Acquisition and Conversion.


The acquisition or sampling section of the block diagram is highlighted in purple. Acquisition is when a capacitor is filled to match the input analog voltage. After a specified amount of time (hopefully when the voltage across the capacitor matches the input pin voltage), the capacitor is released from the acquisition circuit and the voltage is considered to be sampled. This “sample and hold” (SHA) capacitor and switch is outlined in orange in the block diagram.


The conversion circuit is outlined in blue in the block diagram. Conversion is when the voltage in the sample and hold capacitor is converted into a binary representation usable in software. For PIC32’s, the conversion is done by means of a successive approximation register (SAR). A successive approximation ADC compares the pins voltage to an internal analog voltage generated by an internal DAC (digital-to-analog converter of course!). The DAC voltage will be incremented until a match is found. When this happens, the 10-bit value used to drive the DAC becomes the 10-bit analog value.

The PIC32 ADC has its own clock with a configurable period. Acquisition takes a minimum of 132 ns and the minimum ADC clock period (TAD) is listed as being 65 ns. Since conversion takes 12 TAD (one per bit plus a start/stop bit), in theory, the fastest the ADC can complete one sample/convert sequence is 912 ns. I’ve found that increasing these times also increases the ADC’s accuracy and will do so below. The higher the input impedance, the longer the acquisition/conversion times should be. See table 29-33 in the datasheet for more timing information.

When configuring the ADC, pins will generally be referenced by their analog pin number. This is not the same pin number often used for the I/O registers and the ANSELx special function register. Analog pin numbers are highlighted in purple below.

Manual Mode Configuration

In manual mode, the programmer must make an individual request every time the ADC starts a sample. In the version presented here, the acquisition is started manually by setting AD1CON1bits.SAMP HIGH. Conversion will then start automatically after an amount of time specified in the AD1CON3 SFR. When acquisition is finished, the SAMP bit will go LOW. When conversion is done, the AD1CON1bits.DONE bit will go HIGH. The result will be stored in one of the ADC1BUFx registers.

Since we’re only requesting the analog value of one pin at a time in this manual mode, the result is always stored in the first buffer register or ADC1BUF0.

The AD1CHS is the channel select SFR. Of importance to us is that AD1CHS<16:19> control which pin is being input to the ADC.

With this information, we can make a simple function that takes as input an analog pin number and returns the 10-bit analog voltage as an int from 0-1023 as follows.

int analogRead(char analogPIN){
    AD1CHS = analogPIN << 16;       // AD1CHS<16:19> controls which analog pin goes to the ADC
    AD1CON1bits.SAMP = 1;           // Begin sampling
    while( AD1CON1bits.SAMP );      // wait until acquisition is done
    while( ! AD1CON1bits.DONE );    // wait until conversion done
    return ADC1BUF0;                // result stored in ADC1BUF0

To configure for this mode, we set the conversion to trigger automatically after acquisition is done by setting the SSRC bits found at AD1CON1<5:7>. Choosing manual mode is also found in this register. In AD1CON3, we will set the source of the ADC clock, how long a period (TAD) is, and how many periods of this clock per acquisition. Conversion is always 12 TAD cycles long. In our program, we’ll set the analog clock period to be four times the peripheral bus clock period which will be equal to SYSCLK. That is, TAD = 4*TPB. At FPB = 40MHz, TAD = 4*TPB = 100ns. To be on the safe side, we’ll configure the acquisition period as 15*TAD = 1.5us. Thus, the entire analog-to-digital conversion takes 27*TAD = 2.7us. The configuration is shown below.

void adcConfigureManual(){
    AD1CON1CLR = 0x8000;    // disable ADC before configuration
    AD1CON1 = 0x00E0;       // internal counter ends sampling and starts conversion (auto-convert), manual sample
    AD1CON2 = 0;            // AD1CON2<15:13> set voltage reference to pins AVSS/AVDD
    AD1CON3 = 0x0f01;       // TAD = 4*TPB, acquisition time = 15*TAD 

With this configuration, a call to our analogRead() function takes a tested time of 4us. This is relatively slow for the PIC32 but is very stable and fast enough for many applications. If faster times are desired, one solution is to use the automatic configuration as described below.

Before we get into that whole business though, here’s some code and the corresponding circuit to make a poor man’s theremin with a 4-11kohm CdS photocell and piezo buzzer. There’s a pushbutton between pin RB5 and the piezo buzzer for your sanity. Although this little circuit can be fun, don’t expect to sound like Clara Rockmore when playing it.

Poor Man’s Theremin Hardware and Code

/* Reads the analog voltage at pin RB3 and modifies the output frequency of a square wave
 * output to pin RB5 with this value
 * Input is expected to be a CdS photocell in a voltage divide configuration
 * Output is expected to be a piezo buzzer
// include header files
#include <plib.h>
#include <p32xxxx.h>
// Config Bits
#pragma config FNOSC = FRCPLL       // Internal Fast RC oscillator (8 MHz) w/ PLL
#pragma config FPLLIDIV = DIV_2     // Divide FRC before PLL (now 4 MHz)
#pragma config FPLLMUL = MUL_20     // PLL Multiply (now 80 MHz)
#pragma config FPLLODIV = DIV_2     // Divide After PLL (now 40 MHz)
                                    // see figure 8.1 in datasheet for more info
#pragma config FWDTEN = OFF         // Watchdog Timer Disabled
#pragma config ICESEL = ICS_PGx1    // ICE/ICD Comm Channel Select (pins 4,5)
#pragma config JTAGEN = OFF         // Disable JTAG
#pragma config FSOSCEN = OFF        // Disable Secondary Oscillator
// Defines
#define SYSCLK (40000000)
int analogRead(char analogPIN){
    AD1CHS = analogPIN << 16;       // AD1CHS<16:19> controls which analog pin goes to the ADC
    AD1CON1bits.SAMP = 1;           // Begin sampling
    while( AD1CON1bits.SAMP );      // wait until acquisition is done
    while( ! AD1CON1bits.DONE );    // wait until conversion done
    return ADC1BUF0;                // result stored in ADC1BUF0
void delay_us( unsigned t)          // See Timers tutorial for more info on this function
    T1CON = 0x8000;                 // enable Timer1, source PBCLK, 1:1 prescaler
    // delay 100us per loop until less than 100us remain
    while( t >= 100){
        TMR1 = 0;
        while( TMR1 < SYSCLK/10000);
    // delay 10us per loop until less than 10us remain
    while( t >= 10){
        TMR1 = 0;
        while( TMR1 < SYSCLK/100000);
    // delay 1us per loop until finished
    while( t > 0)
        TMR1 = 0;
        while( TMR1 < SYSCLK/1000000);
    // turn off Timer1 so function is self-contained
    T1CONCLR = 0x8000;
} // END delay_us()
void adcConfigureManual(){
    AD1CON1CLR = 0x8000;    // disable ADC before configuration
    AD1CON1 = 0x00E0;       // internal counter ends sampling and starts conversion (auto-convert), manual sample
    AD1CON2 = 0;            // AD1CON2<15:13> set voltage reference to pins AVSS/AVDD
    AD1CON3 = 0x0f01;       // TAD = 4*TPB, acquisition time = 15*TAD
} // END adcConfigureManual()
int main( void)
        // Configure pins as analog inputs
        ANSELBbits.ANSB3 = 1;   // set RB3 (AN5) to analog
        TRISBbits.TRISB3 = 1;   // set RB3 as an input
        TRISBbits.TRISB5 = 0;   // set RB5 as an output (note RB5 is a digital only pin)
        adcConfigureManual();   // Configure ADC
        AD1CON1SET = 0x8000;    // Enable ADC
        int foo;
	while ( 1)
            foo = analogRead( 5); // note that we call pin AN5 (RB3) by it's analog number
            delay_us( foo);       // delay according to the voltage at RB3 (AN5)
            LATBINV = 0x0020;     // invert the state of RB5
	return 0;

Automatic Scan Configuration

In the automatic scan mode we’ll be using, the ADC peripheral will be sampling and converting a specified number of analog pins as long as the ADC has power. Whenever the processor then requests the 10-bit voltage representation of some of these pins, the ADC peripheral will give the most recent completed conversion.

With this method, ADC call latency is severely reduced! Once the ADC is configured and running, every additional pin request is as simple as a single assignment line. However, this method uses slightly more power as the ADC is constantly sampling and converting voltages. Also, while the ADC call time is very short, there is a minimum time between calls to make sure new analog voltages have been sampled. We’re only talking a few microseconds and even this can be reduced by careful planning of the ADC clock (TAD) and how many cycles are necessary for sampling in your particular application!

For our example, we’ll scan the analog pins RB1 (AN3), RB2 (AN4), and RB3 (AN5). During every scan cycle, the voltage of RB1 will be sampled and converted because it has the lowest analog pin number (AN3). After conversion ends, RB2 will immediately be sampled/converted followed by RB3. Once all conversions are done, the ADC interrupt flag will be set. Even if an ISR isn’t entered, this flag will signify that an entire scan has been complete. Scans will continue like this until the ADC is shut off or reconfigured.

When accessing the ADC buffers, it’s important to not request a buffer that is being written to as it’s being accessed. We instead configure the ADC such that scans alternate between writing to buffers ADC1BUF0 – ADC1BUF7 and buffers ADC1BUF8 – ADC1BUFF. The bit AD1CON2bits.BUFS keeps track of which set of buffers is being written to.

The last precaution before reading the buffers in this automatic scan mode is to temporarily shut down the ADC. Knowing which set of buffers is being written to is meaningless if the ADC can switch the set while we’re reading from them! We do this by clearing the automatic sample bit AD1CON1bits.ASAM while accessing the buffers.

The following code will read the stable ADC buffers for three analog pins where the lower number buffer corresponds with the lower number analog pin. Note that the ADC interrupt flag must be cleared manually. If this code is not entered continuously, there should be no wait time while checking the interrupt flag. If desired remove it and the chance will exist that analog values will be re-read twice in a row. Not really a bad thing to do but the flag is here for reference.

int an1, an2, an3;
while( ! IFS0bits.AD1IF);       // wait until buffers contain new samples
AD1CON1bits.ASAM = 0;           // stop automatic sampling (essentially shut down ADC in this mode)
if( AD1CON2bits.BUFS == 1){     // check which buffers are being written to and read from the other set
    an1 = ADC1BUF0;
    an2 = ADC1BUF1;
    an3 = ADC1BUF2;
    an1 = ADC1BUF8;
    an2 = ADC1BUF9;
    an3 = ADC1BUFA;
AD1CON1bits.ASAM = 1;           // restart automatic sampling
IFS0CLR = 0x10000000;           // clear ADC interrupt flag

Our configure function would then be something like the following.

void adcConfigureAutoScan( unsigned adcPINS, unsigned numPins){
    AD1CON1 = 0x0000; // disable ADC
    // AD1CON1<2>, ASAM    : Sampling begins immediately after last conversion completes
    // AD1CON1<7:5>, SSRC  : Internal counter ends sampling and starts conversion (auto convert)
    AD1CON1SET = 0x00e4;
    // AD1CON2<1>, BUFM    : Buffer configured as two 8-word buffers, ADC1BUF7-ADC1BUF0, ADC1BUFF-ADCBUF8
    // AD1CON2<10>, CSCNA  : Scan inputs
    AD1CON2 = 0x0402;
    // AD2CON2<5:2>, SMPI  : Interrupt flag set at after numPins completed conversions
    AD1CON2SET = (numPins-1) << 2;
    // AD1CON3<7:0>, ADCS  : TAD = TPB * 2 * (ADCS<7:0> + 1) = 4 * TPB in this example
    // AD1CON3<12:8>, SAMC : Acquisition time = AD1CON3<12:8> * TAD = 15 * TAD in this example
    AD1CON3 = 0x0f01;
    // AD1CHS is ignored in scan mode
    AD1CHS = 0;
    // select which pins to use for scan mode
    AD1CSSL = adcPINS;

An example call to configure the same three pins (RB3, RB4, and RB5) would be as follows.

adcConfigureAutoScan( 0x0038, 3);
AD1CON1SET = 0x8000;                // start ADC

3 thoughts on “Analog to Digital Conversion (ADC)

  1. i think the first line of adcConfigureAutoScan should be

    AD1CON1 = 0;

    setting 0x8000 turns adc on, while setting 0 turns it off.

    the next line,

    AD1CON1SET = 0x00e4;

    sets the ASAM bit on, which i’m thinking shouldn’t be done until the end when turning the thing on? (but, as above, it is already on) or is it ok to set it here then just flip on the on bit later?

    1. Good catch and fixed! As for AD1CON1SET = 0x00e4. It shouldn’t matter if the ASAM bit is set prior to turning the ADC on or if it’s done soon after. Setting or clearing it prior to enabling the ADC won’t do much since the ADC is completely inactive. I generally prefer to do all of my configuration in one function and enable the peripheral at a separate point in the code when it makes sense.

      Thanks for the notice.

  2. This is wonderful. Saved me a lot of time. Nice to see such an accurate easy to understand example shared.

Leave a Reply

Your email address will not be published. Required fields are marked *