Analogue Sensors On The Raspberry Pi Using An MCP3008

66

The Raspberry Pi has no built in analogue inputs which means it is a bit of a pain to use many of the available sensors. I wanted to update my garage security system with the ability to use more sensors so I decided to investigate an easy and cheap way to do it. The MCP3008 was the answer.

The MCP3008 is a 10bit 8-channel Analogue-to-digital converter (ADC). It is cheap, easy to connect and doesn’t require any additional components. It uses the SPI bus protocol which is supported by the Pi’s GPIO header.

This article explains how to use an MCP3008 device to provide 8 analogue inputs which you can use with a range of sensors. In the example circuit below I use my MCP3008 to read a temperature and light sensor.

Here are the bits I used :

  • Raspberry Pi
  • MCP3008 8 channel ADC
  • Light dependent resistor (LDR)
  • TMP36 temperature sensor
  • 10 Kohm resistor

The first step is enabling the SPI interface on the Pi which is usually disabled by default.

Please follow my Enabling The SPI Interface On The Raspberry Pi article to setup SPI and install the SPI Python wrapper.

Circuit

MCP3008

The following list shows how the MCP3008 can be connected. It requires 4 GPIO pins on the Pi P1 Header.

VDD   3.3V
VREF  3.3V
AGND  GROUND
CLK   GPIO11 (P1-23)
DOUT  GPIO9  (P1-21)
DIN   GPIO10 (P1-19)
CS    GPIO8  (P1-24)
DGND  GROUND

The CH0-CH7 pins are the 8 analogue inputs.

Here is my breadboard circuit :

MCP3008 BreadboardIt uses CH0 for the light sensor and CH1 for the TMP36 temperature sensor. The other 6 inputs are spare.

Here is a photo of my test circuit on a small piece of breadboard :

MCP3008 Example Circuit #2

LDR ExampleLight Dependent Resistor

I chose a nice chunky LDR  (NORPS-12, datasheet). Under normal lighting its resistance is approximately 10Kohm while in the dark this increases to over 2Mohm.

When there is lots of light the LDR has a low resistance resulting in the output voltage dropping towards 0V.

When it is dark the LDR resistance increases resulting in the output voltage increasing towards 3.3V.

TMP36 Temperature SensorTMP36 Temperature Sensor

The TMP36 temperature sensor is a 3 pin device (datasheet). You can power it with 3.3V and the middle Vout pin will provide a voltage proportional to the temperature.

A temperature of 25 degrees C will result in an output of 0.750V. Each degree results in 10mV of output voltage.

So 0 degrees will give 0.5V and 100 degrees will give 1.5V.

Reading The Data Using a Python Script

The ADC is 10bit so it can report a range of numbers from 0 to 1023 (2 to the power of 10). A reading of 0 means the input is 0V and a reading of 1023 means the input is 3.3V. Our 0-3.3V range would equate to a temperature range of -50 to 280 degrees C using the TMP36.

To read the data I used this Python script :

#!/usr/bin/python

import spidev
import time
import os

# Open SPI bus
spi = spidev.SpiDev()
spi.open(0,0)

# Function to read SPI data from MCP3008 chip
# Channel must be an integer 0-7
def ReadChannel(channel):
  adc = spi.xfer2([1,(8+channel)<<4,0])
  data = ((adc[1]&3) << 8) + adc[2]
  return data

# Function to convert data to voltage level,
# rounded to specified number of decimal places.
def ConvertVolts(data,places):
  volts = (data * 3.3) / float(1023)
  volts = round(volts,places)
  return volts

# Function to calculate temperature from
# TMP36 data, rounded to specified
# number of decimal places.
def ConvertTemp(data,places):

  # ADC Value
  # (approx)  Temp  Volts
  #    0      -50    0.00
  #   78      -25    0.25
  #  155        0    0.50
  #  233       25    0.75
  #  310       50    1.00
  #  465      100    1.50
  #  775      200    2.50
  # 1023      280    3.30

  temp = ((data * 330)/float(1023))-50
  temp = round(temp,places)
  return temp

# Define sensor channels
light_channel = 0
temp_channel  = 1

# Define delay between readings
delay = 5

while True:

  # Read the light sensor data
  light_level = ReadChannel(light_channel)
  light_volts = ConvertVolts(light_level,2)

  # Read the temperature sensor data
  temp_level = ReadChannel(temp_channel)
  temp_volts = ConvertVolts(temp_level,2)
  temp       = ConvertTemp(temp_level,2)

  # Print out results
  print "--------------------------------------------"
  print("Light: {} ({}V)".format(light_level,light_volts))
  print("Temp : {} ({}V) {} deg C".format(temp_level,temp_volts,temp))

  # Wait before repeating loop
  time.sleep(delay)

Here is a screen-shot of the output :

MCP3008 Screenshot

You can download this script directly to your Pi using :

wget https://bitbucket.org/MattHawkinsUK/rpispy-misc/raw/master/mcp3008/mcp3008_tmp36.py

It can then be run using :

sudo python mcp3008_tmp36.py

Alternatively if you are a Git fan you can clone my Raspberry Pi misc scripts repo using :

git clone https://bitbucket.org/MattHawkinsUK/rpispy-misc.git

Additional Explanation of spi.xfer2

Lots of people have asked about the spi.xfer2 line. This sends 3 bytes to the device. The first byte is 1 which is equal to 00000001 in binary.

“8+channel” is 00001000 in binary (where channel is 0). “<<4” shifts those bits to the left which gives 10000000. The last byte is 0 which is 00000000 in binary.

So “spi.xfer2([1,(8+channel)<<4,0])” sends 00000001 10000000 00000000 to the device. The device then sends back 3 bytes in response. The “data=” line extracts 10 bits from that response and this represents the measurement.

The exact reason why you do the above is explained in the datasheet but that is outside the scope of this article.

More Information

If you want more technical information about the device please take a look at the official Microchip MCP3008 datasheet. If you want to know more about the SPI bus then take a look at the Serial Peripheral Interface Bus Wikipedia page.

 

Share.

66 Comments

  1. Great article that addresses one of the Pi’s main shortcomings.

    Why do people settle for 10 bits with the MCP3008 when the MCP3208 costs pennies more and does 12 bits? I believe it’s pin and code compatible as well. You might as well go for all the resolution available…admittedly it’s up to the user to analogue filter the input to really make good use of the available resolution.

    • Analog filtering’d work, but cheaper to average the incoming voltage in software.
      Thinking of my days with early Nicolet spectrum analysers, we got an extra ½ bit rersolution by adding a bit’s-worth of noise to the analog input. We had to filter first (anti-aliasing – Nyquist and all that…) but in the temp. measurement aliasing isn’t an issue, so any ‘noise’ might actually be beneficial, if averaged over, say 4 samples before presentation…then adjust the resolution in the ‘places’ variable. Worth trying, YMMV.

  2. Yes, Analogue filtering depends on your application…if all you’r doing is monitoring an essentially static voltage then software filtering is fine for removing the residual noise ….if you’re looking at a signal with a known AC content then you need to worry about Nyquist sampling rates and anti-aliasing.

    Dithering is an old trick with lower resolution converters; beyond 12 bits you generally get it for free, especially with breadboard layouts.

    I’d like to see a full-blown datalogger done with the Raspberry Pi. Adafruit sell a 16 bit 4 channel ADC assembled on to a small pcb; four of those taken with a RTC chip would make a very useful 16 channel datalogger which could be run off a 12v battery, reasonably low power and no moving parts (fans, disc drives etc), and wouldn’t tie up a PC. I keep planning to do it myself but never get around to it…..
    RL

  3. If i was to follow this project, would it be recommended that i use the exact same LDR and ADC? Thanks for any help, great article!

    • You could use any LDR. The article deals specifically with the MCP3008 ADC but you could use another … but it would need to be pin compatible and also use the SPI protocol.

      • Hi Matt,

        I just tried this with a different LDR, a TEPT5700 Vishay Photodiode, and an IR photodiode.

        For each I used the same circuitry and resistor (10K Ohm) and the results were very much the same.

        The fun begins….

  4. Hey what could the problem be if the SPI is only read once in a while…

    It’s always 0 and then suddenly shows the correct value and then 0 again?

    Thanks Matt

  5. Hi,

    Just wanted to say thanks for posting this.
    Just been and bought one and some ir led + ir photodiodes.

    I’ve previously set up a way to pass morse code from one RPi to another so this would be an ace way to try that out with light.

    George

    • I can’t see why it wouldn’t work with the Model A. The GPIO pins are the same so should be identical to the Model B as far as the MCP3008 is concerned.

  6. Hi,

    thanks for your answer. As I am trying to make my web-radio solar/battery powered, I will get an A model. They are supposed to draw only about 1/3 of energy compared to model B.

    The one existing usb-port will be used for a wlan-adapter.

    Schwabinger

    • When I did my AA battery tests the power consumption of the Model A was indeed about 1/3 of the Model B. I usually develop on a Model B and then move the card to the Model A when I don’t need the network socket any more.

  7. For those of us trying to get to grips with both the Spi interface any Python, could you explain how the bits in the brackets on the two lines calling the spi.xfer2 function and organising the data actually work?:

    adc = spi.xfer2([1,(8+channel)<<4,0])
    data = ((adc[1]&3) << 8) + adc[2]

    The datasheet is pretty complex from a standing start!

    Regards,

    Barney Ward

  8. Hi,
    thanks for this post, it’s really interesting!
    But I have one doubt.

    Is it possible to use the MCP3008 and the RPi to monitor a 12V battery voltage?
    And use, for example, a temperature sensor / LDR along with the monitoring of the 12V battery voltage within the same MCP3008?

    How the connections would be for this?
    I would like to manage some renewable energy with the pi (I’m doing it now with a 12F675 ), but in this case I don’ know how, as I need the ground and positive from the battery, and read the MCP with the Pi :/

    Thanks in advance.

    • I’m sure you could monitor a 0-12V level but you would need to scale the 12V maximum to 3.3V. Perhaps using a simple resistor divider? The MCP3008 has got 8 input channels so you could monitor voltage and a light sensor at the same time.

  9. I’m following this tutorial and only using a photocell. I’ve eliminated the temperature sensor.

    When I run the python code in idle I always get the same data back
    adc [0 , 3, 255]
    volt 3.3v
    Light 1023

    No matter what I do, I can’t seem to figure out what my problem is.
    The photocell I have has been tested and it works.
    I’ve tried different channels on the mcp3008 and give the same readings.

    My next test is going to be with a potentiometer to see if I can get readings from it.

    Any help would be greatly appreciated.

    Thanks,
    George

      • I just tried this and the voltage stays at 3.3v

        When I test the Ohms on the pot it comes back with varying resistance like it should.

        This however is not reflected when it is hooked up to the pi.

        Any other thoughts?

  10. My Full Sized breadboard is actually split in two. I was running 3v on one half and not the other. So the chip was never getting power.

    Rookie mistake….

  11. Hi. An excelllent tutorial.

    I am only using the temperature part, so have commented out the lines for the light sensor. Everything works fine, but I have two issues:
    1 – The temperature only ever reads in full degrees, even though it’s presented to one decimal place. Am I missing something really obvious? I’d really like to get a more detailed temperature reading.
    2 – I have the tmp36 on about 1m of cable (spare piece of cat5). It seems to be reading 4 to 5 degrees below the known temperature. Presumably the length of cable could be the issue here (I’ve tried two different types of cable with roughly the same result). How would I go about some kind of correction for this?

    I’m hoping these are really simple things that a noob like me has just not seen. I’d really, really appreciate any pointers in the right direction.

    Thanks,
    Steve.

    • Hi Steve,

      You spotted a bug. When dividing by an integer (1023) it forces the output to be an integer. So it loses the decimal points in the calculation. I have wrapped the 1023 value in “float” which now retains the decimal places. These are then rounded to 2 decimal places.

      As for the accuracy I’m not sure as I haven’t used the sensor on a long cable. It could be due to interference on the wire? What if you try a shorter length?

      • Excellent and well written tutorial, thanks Matt for your time and effort in making this an easy task for the rest of us. I’ve played with the temperature sensor and also have a similar offset. Yet when checking the output from the sensor the voltage is correct to the temperature. A quick bit of googling has lead me to believe that this may be due to the input impedance of the MCP3008 and that some other simple circuitry may be needed to boost the signal from the sensor.

          • Hi Ulf. Thanks for this suggestion. I was slowly coming to the conclusion that there might be interference due to length of wire, in my case about 5m. With zero wire length I was, even then, reading about 5C out. With this long wire it is -25C out! If interference is the factor what value of capacitor across Vin and Vout do you suggest? (Do Vin and Vout equate to Din and Dout?)Thanks, Clive.

  12. Hi Matt,

    Thanks for the quick reply.

    That all makes sense. I have wrapped the 1023 division with float and now it returns the temperature to two decimal places (just like I’d hope it would). That’s brilliant!

    I’ll experiment with putting the tmp36 directly into the breadboard vs using a length of wire. Annoyingly, I’ve sealed the tmp36 in multiple layers of heat-shrink to make it waterproof. I know I could have used a DS18B20 (which I have one of), but I’ve got another project running on GPIO4 which the 1-wire interface needs to use. Plus I wanted to learn how to use the mcp3008.

    Slowly but surely, it’s all coming together nicely :)

    Thanks,
    Steve.

  13. Hi there,

    I just have a quick question. I am trying to figure out how to amend the “xfer2” and “data =” if I am using the 8-channel 12-bit MCP3208?

    Thank you for your help!

    Roman

    • I think the data line would become :

      data = ((adc[1]&15) < < 8) + adc[2] If your received data is : XXXXXXXX XXXXDDDD dddddddd It grabs the first 4 bits of the second byte (DDDD), shifts them 8 to the left (to give (DDDD0000). It then adds on the third byte to give DDDDdddddddd. That’s your 12 bits. In the calculation to convert the data value into volts you should use 4095 (2^12) rather than 1023 (2^10). I haven’t tried this as I don’t have an MCP3208 but I think it looks right :)

  14. Hey

    I’m a newbie to raspberry pi and python but I would like to create a project with analog inputs, but I would like them to display real-time in a graph, could anyone give me some help with the code/ other examples of this?

    Thanks a lot

    PS: You created a really good guide that already helped me a lot 😀

    Simon

  15. Hi Matt,
    Wonderful post. I am trying to do the very same thing on my Beaglebone Black. Yes I know that the Black has endless Analog IOs but I’m using the MCP3008 as protection, seeing that the BBB could only take 1.8 volt.
    This is my code so far:

    import Adafruit_BBIO.SPI, time, os
    spi = Adafruit_BBIO.SPI.SPI()
    spi.open (0,0)

    def ReadChannel(channel):
    adc = spi.xfer2([1,(8+channel)<<4,0]) #
    data = ((adc[1]&3) << 8) + adc[2]
    return data

    the rest is the same as yours from here on.

    The problem is I keep getting this error message when I run my program.

    *** glibc detected *** python: double free or corruption (fasttop): 0x0008fbb0 ***

    Can you shed some light? Thanks in advance.

  16. Hi, i’m tryng to misure light but, how could R impact over V in that circuit? V is always 3.3V, because resistance is changing with current so… V is the same 3.3V. Am i wrong?

    • It forms a voltage divider. If the LDR is 10K then the voltage at the ADC input is 1.65V. If the LDR is a large resistance the voltage is closer to 3.3V. If the LDR is very small the voltage will tend to 0V. So as the resistance changes the voltage will vary between 3.3V and 0V.

  17. A few people have asked about the code changes for an MCP3208. You have to change four lines. The first is to send the data earlier to allow time for the greater number of bits in the response:

    adc = spi.xfer2([6+((4&channel)>>2),(3&channel)<<6,0])

    You then have to include the extra bits when calculating the output:

    data = ((adc[1]&15) << 8) + adc[2]

    Finally you have to cope with the extra bits when converting to voltages and temperatures:

    volts = (data * 3.3) / float(4095)
    temp = ((data * 330)/float(4095))-50

    My complete code is available at http://www.jseddon.co.uk/mcp3208_jon.py

    Thanks for a great tutorial – that saved me loads of time.

  18. Hi, great post, I’ve eventually got mine working thanks to this guide!! :0)

    Just now want to link this to a GUI, ie Glade or WebIOPi.

    Do you, or anyone have any examples of doing this?? I’ve been trying for ages but getting no where. Just basically see a value – temperature in a text box……..??

  19. Matt,

    This is a very well written and useful post. I needed to create the exact circuit and your guide helped me get it right.

    Thanks for taking the time to publish this so others like me can really understand what is going on when requiring ADC tools.

    Sopwith

  20. This has stopped working after an RPi firmware update. (It installed today as part of a Raspbian Jessie aptitude update; aptitude upgrade.) Something is wrong with the /dev/spidevx.x devices after that upgrade.
    My solution was to go back to an older kernel/firmware.
    The last working one can be installed by using this:
    rpi-update f74b92120e0d469fc5c2dc85b2b5718d877e1cbb
    gives me: uname -a
    Linux raspberrypi 3.12.36+ #737 PREEMPT Wed Jan 14 19:40:07 GMT 2015 armv6l GNU/Linux
    If anyone knows how this can be made to work in a recent (Raspberry Pi 2 -Ready) Raspbian, please post here and/or amend the blog.
    Cheers,
    Max

    • I’ve just updated my SPI setup article and appears to work ok now with the latest version of Raspbian. the SPI Python wrapper has been updated and there is a new technique for enabling the SPI kernel module.

  21. Nice tutorial, though when I run this code on my pi, the spi.xfer2 line gives me an ‘invalid syntax’ error. Could someone please tell me what I’m doing wrong?
    PS- It seems like I have the spidev module installed
    Thanks in advance

  22. Hi!

    Great tutorial, very clear explanation!

    I want to measure temperature sensors in a car. These sensors ground to the car, so they have only one connector. And they are normally used with 12V.
    Could I use this chip to measure these sensors with a Raspberry Pi?

    Greetings,
    Gerrelt.

    • Did you use wget to grab the text file from Bitbucket or did you copy and paste from the webpage? If you copy and paste it sometimes converts spaces into strange ASCII characters.

  23. Could you please explain why you have connected Vdd and Vref to the same input? I am using an AD22100. It takes a 5V input. Does that mean that I will have to use 5V for Vref in the circuit instead of the 3.3 V? And mainly, can I use 2 different rails for dictating 3.3V in the A/D converter and 5 V for the sensor and the Vref?
    Thanks.

    • Vref sets the voltage range of the inputs so I connected to 3.3V. Vdd is the supply voltage and the MCP3008 runs off 3.3V so I connected it to the same pin. If you are using a Pi you do not want to ever put 5V on the Pi’s GPIO pins. If you power it from 5V and use 5V for Vref I think that would result in 5V outputs. Which is bad. So I would run it off 3.3V. You could assume that if you aren’t going to exceed 85 degrees your sensor will never exceed 3.3V anyway.

      • Not sure if this is safe but I was playing with an analog feedback servo which (I think) returns a voltage between 0 and 5V (since I was driving the servo with a 5 V power supply). The feedback voltage gives you the position of the servo. I simply connected the feedback lead to the MCP3008. Since I wasn’t sure what to do with the Vref, I kept it on the 3.3V rail (same as Vdd). It seemed to work just fine. I had two feedback servos hooked up and I calibrated the MCP3008 reading to the position of one of them. Then I could use that reading and mirror the position onto the second servo. I thought it was pretty slick. I’m a newbie so take that fact as a disclaimer.

  24. Francisco Saravia on

    I am just getting started. Could you post a video on how to wire the raspberry pi to the MCP3008. I am a newbie on this.

    • Francisco S. on

      Disregard my last comment. it worked. I was using an IDE cable. but went for the straight connectors. Perfect thank you !

  25. I want to use the MCP3008 but I’m having trouble installing SPI for python 3 onto a Raspberry A+. I’ve gone through the raspi-config and SPI appears to be enabled but after I install the SPI wrapper, my python program does not seem to be able of find it.

    I am using idle3 because I like the interface. Since the GPIO operations need to be done w/ root privileges, I use the terminal and “sudo idle3” to launch idle3. That’s been working well for me until now.

    I found another tutorial that shows how to use the MCP3008 on the generic GPIO pins and got it working just fine. But I would like to learn more about SPI so came here.

    Any help would be much appreciated. Thanks.

    • I figured it out…..

      On a whim (and out of frustration), I changed:

      sudo apt-get install python2.7-dev
      to
      sudo apt-get install python3.2-dev

      and

      sudo python setup.py install
      to
      sudo python3 setup.py install

      Now it’s working. So if anyone else has this problem, try that.

  26. what if i want to use raspberry pi to display an analog signal with a frequency higher than 1Mhz , mcp3008 can do that job ? or it will be very slow display . what is the best alternatives to use , i mean the ADC .

    • To be honest the Pi probably isn’t the best choice to measure such a signal. You would have to take accurate measurements at 2MHz+.

  27. Hi,
    thanks for this nice tutorial.
    For my current application I need to read out two ADCs simultaneously, so I got two MCP3008, one connected to CE0 the other to CE1. Using the spi.open(0,x) command I can address either of them. But how can I read out both of them?
    Checking out the xfer-protocol, it seems there is no address included. Does that mean I have to open and close the spi-connection after reading out either MCP in order to switch to the other one? That seems rather stupid for a bus protocol but I couldn’t come with a working solution yet.
    Any suggestions on how to do that?

  28. Hello,

    When I try to use your code, I ve got this error meassage :

    TypeError: Argument must be a list of at least one, but not more than 4096

    I don’t understand why?

    Do you know how to correct this error ?

    Thanks,
    Mat

  29. Hello I am also getting the same error with MAT. Everythin is the same with you bu getting TypeError: Argument must be a list of at least one, but not more than 4096 in the line adc=spi.xfer2([1,(8 + channel)<<4,0]). Read channel function does not work. How can we fix it? Thank you

Leave A Reply