Add Analog to Digital Conversion Capability to The Raspberry Pi Without an ADC Chip

One of the few disadvantages of the Raspberry Pi is that it lacks a built-in analog to digital converter(ADC). This can be remedied by connecting a dedicated ADC chip to the RPi Board via SPI (or even I2C).  But there are other ways to get analog to digital conversion going on the Raspberry Pi.

This entry demonstrates how to add analog to digital conversion capabilities to the Raspberry Pi  with a few external components (a comparator, two resistors and a capacitor) and some software. Why not just use an ADC chip you say! I say why not! The objective here is to be playfully clever and possibly learn something along the way.

The entire process is summarized as follows: The PWM1 peripheral in the Raspberry Pi is used as a digital to analog converter (DAC) with the aid of a simple passive RC filter. This DAC outputs a variety of analog voltages that the comparator chip compares to a target analog voltage (to be converted to a digital value). The comparator’s output indicates to  the  successive approximation algorithm, which bits of the final digital result should be ‘1’ and  ‘0’. The successive approximation algorithm runs on the Raspberry Pi.

This ‘hack’ is not new and has been used many times with microcontrollers that lacked built-in ADC’s, but had PWM generators. In some cases where the microcontrollers also lacked  PWM generators, R2R ladders were used as DACs instead.

I’ve yet to see this sort of ‘hack’ applied to the Raspberry Pi however.

The schematic for the circuit is shown below.

Basic ADC Hack schematic

Basic ADC Hack schematic

Using the RPi’s PWM output as a DAC

The Raspberry Pi has two PWM peripherals; only one (PWM1)  can be mapped to a GPIO pin (GPIO18) on the Raspberry Pi’s 26-pin header. A C++ class (rpiPWM1)  has already been developed to enable and control the PWM1 peripheral.

To understand how a PWM peripheral can be used as a DAC, consider the three waveforms below:

PWM as DAC (https://www.diolan.com/articles/dac.html)

PWM as DAC (https://www.diolan.com/articles/dac.html)

For all three waveforms assume that the ‘High’ voltage corresponds to 3.3V and the low voltage corresponds to 0V. The ON time of the first waveform is roughly a third of its period. And since the periodic waveform repeats, it’s safe to assume that the first waveform is ‘ON’ 33.3% of the total time. This percentage is known as the duty cycle. It so happens that if we pass such a waveform into a low pass filter (LPF) that isolates only the DC value of the signal (i.e. the ‘0Hz component’ of the signal, also known as the average value of this signal), the voltage at the output of the filter would be a DC voltage that’s 33.3% of the ‘High’ voltage. In our case this would be 0.333* 3.3 =  1.1V.

Similarly,  a 50% duty cycle going into the LPF will be translated to a DC voltage of 0.5*3.3 = 1.65V  and  a 75% duty cycle going into the LPF will be translated to a DC voltage of 0.75*3.3 = 2.475V.

This is a very important property of PWM waveforms. To generate a specific DC voltage between 0 and VDD, one needs only make the duty cycle of the PWM waveform proportional to the target DC voltage. The low pass filter does the rest.

i.e. Target Duty Cycle(%) = Target Voltage / VDD

OR  Target Voltage = Target Duty Cycle(%) * VDD 

Calling the voltage at the output of the LPF a DC voltage is somewhat misleading. It is a DC voltage so long as the duty cycle doesn’t change. In fact, a PWM DAC can be used to generate all sorts of time-varying waveforms so long as the duty cycle is varied fast enough.

A passive RC low pass filter has the advantage of being easy to use and requiring only two components. It does have many disadvantages however, including a large settling time (3-5RC) due to its inherent RC time constant. Also it suffers from poor roll-off characteristics. In our design we chose to build an RC filter using R=1000Ohms and C=10nf. This gives us a settling time of 5*R*C=50uS and a cutoff frequency of 1/(2*Pi*R*C) = 15.913KHz.  This implies that the frequency of our PWM waveform should be at least 2 orders of magnitude larger that 15.9KHz in order for the filter to function adequately.  The PWM frequency’s we chose to use vary from 9.37KHz (10-bit ADC) to 37.5KHz (8-bit ADC), but because we are using the PWM1 peripheral in PWMMODE (as opposed to MSMODE), the effective PWM frequency in both cases is in the 3-4MHz range which is  more than two orders of magnitude larger that the LPF’s cut-off frequency.

It’s also important to note that the reference voltage VREF for a PWM DAC (and ultimately the ADC)  will always be the same as the supply voltage VDD (3.3V).

The Analog Comparator

For the demo, the LM311 analog comparator was chosen. The main reason for choosing it is because it was lying around.  One minor issue with this comparator is that it needs a 5V supply on its VCC pin. Luckily the LM311 has an open-collector output whose level can still be set to 3.3v with a 10K pull-up resistor (to the RPI’s 3.3V rail).

The LM311 is a classical analog comparator. It compares the voltages at its  +ve and -ve input terminals; if the voltage at the +ve terminal is larger than that of the -ve terminal, the comparator output is high, else it’s low.

In our setup, if the voltage to be digitized (+ve terminal) is larger that the voltage generated by the PWM DAC (-ve terminal) the comparator output is high, else it’s low.

The Successive Approximation Algorithm

The best way to illustrate how successive approximation works is by example. Assume that we want to build an 8-bit ADC. Our PWM DAC will need exactly 8-bits of resolution; bit7-bit0. Let’s also assume that VDD (and by definition the reference voltage) is 3.3V. At this point we would like to examine the digital value and analog voltage that each bit represents. To achieve this we will use the following equation:

Analog value = (Digital value / (2^(ADC bitwidth) – 1))*VREF

where bitwidth is 8 and VREF=VDD=3.3V.

  • Bit7 represents a digital value of 2^7 = 128. This is equivalent to 128/255*3.3 =   1.66v
  • Bit6 represents a digital value of 2^6 = 64. This is equivalent to 64/255*3.3 =   0.828v
  • Bit5 represents a digital value of 2^5= 32. This is equivalent to 32/255*3.3 =   0.414v
  • Bit4 represents a digital value of 2^4 = 16. This is equivalent to 16/255*3.3 =   0.207v
  • Bit3 represents a digital value of 2^3 = 8. This is equivalent to 8/255*3.3 =   0.104v
  • Bit2 represents a digital value of 2^2 = 4. This is equivalent to 4/255*3.3 =   0.0518V
  • Bit1 represents a digital value of 2^1 = 2. This is equivalent to 2/255*3.3 =   0.0259v
  • Bit0 represents a digital value of 2^0 = 1. This is equivalent to 1/255*3.3 =  0.0129V

Now let’s say that the voltage that we want to digitize (voltage at +ve terminal) is actually 2.02V. The successive approximation algorithm will perform the following tasks:

  • First we will set an accumulator variable to 0.
  • Set PWM DAC duty cycle to that of bit7  i.e. to 128/256 (50%). This will cause the -ve input of the comparator to have 1.66V on it. Since the voltage at the +ve terminal 2.02V is larger,  the comparator output will be high and the software will set bit7  in the accumulator variable (accum = 0b10000000).
  • Next set PWM DAC duty cycle to that of bit7 (from accumulator) & bit6 i.e. (0b11000000 or  192) 192/256 (75%).  This will cause the-ve input of the comparator to have 1.66V+0.828V =  2.488V on it. Since the voltage at the +ve terminal 2.02V is smaller,  the comparator output will be low and the software will clear bit6 in the accumulator variable (accum = 0b10000000).
  • Next set PWM DAC duty cycle to that of bit7 (from accumulator) & bit5 i.e. (0b10100000 or  160) 160/256 (62.5%).  This will cause the-ve input of the comparator to have 1.66V+0.414V = 2.074V on it. Since the voltage at the +ve terminal 2.02V is smaller,  the comparator output will be low and the software will clear bit5 in the accumulator variable (accum = 0b10000000).
  • Next set PWM DAC duty cycle to that of bit7 (from accumulator) & bit4 i.e. (0b10010000 or  144) 144/256 (56.25%).  This will cause the-ve input of the comparator to have 1.66V+0.207V = 1.867V on it. Since the voltage at the +ve terminal 2.02V is larger,  the comparator output will be high and the software will set bit4 in the accumulator variable (accum = 0b10010000).
  • Next set PWM DAC duty cycle to that of bit7, bit4 (from accumulator) & bit3 i.e. (0b10011000 or  152) 152/256 (59.375%).  This will cause the -ve input of the comparator to have 1.66V+0.207V+0.104 = 1.971V on it. Since the voltage at the +ve terminal 2.02V is larger,  the comparator output will be high and the software will set bit3 in the accumulator variable (accum = 0b10011000).
  • Next set PWM DAC duty cycle to that of bit7,bit4,bit3 (from accumulator) & bit2 i.e. (0b10011100 or  156) 156/256 (60.9375%).  This will cause the-ve input of the comparator to have 1.66V+0.207V+0.104+0.0518 = 2.0228V on it. Since the voltage at the +ve terminal 2.02V is smaller,  the comparator output will be low and the software will clear bit2 in the accumulator variable (accum = 0b10011000).
  • Next set PWM DAC duty cycle to that of bit7,bit4,bit3 (from accumulator) & bit1 i.e. (0b10011010 or  154) 154/256 (60.15625%).  This will cause the -ve input of the comparator to have 1.66V+0.207V+0.104+0.0259 = 1.9969V on it. Since the voltage at the +ve terminal 2.02V is larger,  the comparator output will be high and the software will set bit1 in the accumulator variable (accum = 0b10011010).
  • Finally set PWM DAC duty cycle to that of bit7,bit4,bit3,bit1 (from accumulator) & bit0 i.e. (0b10011011 or  155) 155/256 (60.546875%).  This will cause the -ve input of the comparator to have 1.66V+0.207V+0.104+0.0259+0.0129 = 2.0098v on it. Since the voltage at the +ve terminal 2.02V is larger,  the comparator output will be high and the software will set bit0 in the accumulator variable (accum = 0b10011011).
  • At this point the algorithm is complete and the accumulator variable contains the digital representation of 2.02V which was calculated to be 155.

To verify this result let’s use the following equation again and work our way backwards:

 Analog value = (Digital value / (2^(ADC bitwidth) – 1))*VREF

                 2.01V =  (155/255)*3.3

We can see that when calculating the analog equivalent of ‘155’ we got 2.01V;  a value that’s incredibly close to the real value of 2.02V. The reason why we were not able to get the exact binary equivalent of 2.02V is due to the quantization errors inherent in the digitization process.

Setting up The  Circuit

The hardware setup is pretty straightforwards. Both the schematic illustrated at the beginning of the article and the breadboard diagram below illustrate the same circuit. The PWM output (GPIO18) is connected to the RC low pass filter. The output of the filter is then connected to the -ve terminal of the LM311. The analog voltage to be converted is then connected to the +ve terminal of the LM311. In the schematic/diagram shown here we used a potentiometer to try converting a variety of analog voltages, but one could replace the potentiometer with any analog sensor/source of their choosing.

Breadboard diagram for the ADC hack

Breadboard diagram for the ADC hack

The LM311 comparator needs 5V for power, so its VCC pin was connected to the 5V rail, whereas the output from the LM311 is connected via pull-up to the 3.3V rail, as well as to GPIO4. The software discussed in the next section also allows for the re-assignment of the comparator output to other pins on the RPI header.

The Code

The adcHack class was designed to facilitate this AD Conversion solution. The adcHack supports two resolutions; 8-bit (AdcHack::ADC_RES_8) and 10-bit (AdcHack::ADC_RES_10). The class also allows for the variation of the ADC’s settling time. Larger settler times mean more accurate conversion. The default settling time is 60us. The class also allows the assignment of the comparator output to any GPIO pin on the Raspberry Pi (except GPIO18 which is already used for PWM output). The class definition file is shown below:

#ifndef ADCHACK_H
    #define ADCHACK_H
#include "mmapGpio.h"
#include "rpiPWM1.h"
#include <stdio.h>
#include <math.h>
class adcHack{

public:
    adcHack();// default constructor 10-bit resolution Comparator output mapped to GPIO4
    adcHack(unsigned int pinnum);// overloaded constructor 1. 10-bit resolution, Comparator output can be reassigned to 'pinnum'
    adcHack(unsigned int pinnum, const int &resolution);// overloaded constructor. Let's user choose resolution & Comparator pin
    unsigned int adcRead();// This method performs the ADC conversion
    int setResolution(const int &resolution); // setter for setting/changing the resolution between adcHack::ADC_RES_8 & adcHack::ADC_RES_10
    void setSettlingTimeUs(unsigned int settle);// setter for changing the settling time. Default value is 55

    unsigned int getSettlingTime() const {return sett;}// getters for settling time and adcresolution
    int getAdcResolution() const {return res;}

    static int const ADC_RES_8 = 0; // const indicating 8-bit resolution
    static int const ADC_RES_10 = 1; // const indicating 10-bit resolution
    static int const ERR_RES = 1; // error code for setting erroneous resolution value

private:
    mmapGpio inFromComparator; // mmapGpio object to control GPIO used connected to comparator output
    rpiPWM1 dac; // rpiPWM1 object to control the PWM1 peripheral
    int res; // resolution variable
    unsigned int sett; // settling time variable
};
#endif

The adcRead() method is the one responsible for performing ADC conversions. It implements the successive approximation algorithm.

The adcHack class relies heavily on the rpiPWM1 class  to control the PWM1 peripheral and on the mmapGpio class to read the comparator output via GPIO4. A sample main code that uses the adcHack class is shown below:

#include "adcHack.h"

int main(void){
    int DigitalResult = 0;
    adcHack adc(4,adcHack::ADC_RES_10); // initialize adcHack to 10-bit resolution 
    // & Comparator output to GPIO4
    int i = 20; 
    adc.setResolution(adcHack::ADC_RES_10);// set resolution to 10-bit (just testing...)
    adc.setSettlingTimeUs(200); // set settling time
    while(i > 0){ 

        DigitalResult = adc.adcRead(); // perform conversion
        usleep(100); // every 100us...you change this to whatever value you like...just an interval
        printf("Digital value is %d and analog value is %0.4lf volts. \n",DigitalResult, (DigitalResult/1024.0)*3.3);
        //print out result
        i--; 
    }   
    printf("settling time was %d us\n",adc.getSettlingTime()); //use getter for testing!
    return 0;
}

The code is well documented and can be downloaded along with a makefile from here. The output of the example code above is presented below. In this instance, the analog voltage applied to the +ve terminal of the comparator (and hence the voltage to be digitized) was 2.015v.

output of customAdc (adcHack main test program) when a voltage of 2.02V is applied to the +Ve terminal of the comparator

output of customAdc (adcHack main test program) when a voltage of 2.02V is applied to the +Ve terminal of the comparator

I was able to achieve a AD conversion speed of approximately 750 samples per second with 10-bit resolution. With 8-bit resolution I expect the algorithm attain faster sampling speeds.

This entry was posted in ADC, GPIO, PWM, Raspberry Pi, Raspberry Pi Peripherals. Bookmark the permalink.

8 Responses to Add Analog to Digital Conversion Capability to The Raspberry Pi Without an ADC Chip

  1. Pingback: ADC For Raspi Without Using An ADC

  2. Pingback: ADC For Raspi Without Using An ADC - Tech key | Techzone | Tech data

  3. Pingback: ADC For Raspi Without Using An ADC | vyagers

  4. Pingback: ADC For Raspi Without Using An ADC | Hack The Planet

  5. Peter says:

    Dark blue lines on black? It’s a small thing, to be sure, but with schematics, best to keep them high contrast. You can’t go wrong with black lines on a white background.

  6. Pingback: ADC For Raspi Without Using An ADC | Ad Pub

  7. mike says:

    herta – awesome article;
    been hunting around for cheap or common ways to make the rasp-pi do adc. Found your article to be very well explained and now i’m off to give it a try.
    Thanks!

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>