Analogue Sensors On The Raspberry Pi Using An MCP3008

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.

Enable Hardware SPI

To enable hardware SPI on the Pi we need to make a modification to one of the system files :

sudo nano /etc/modprobe.d/raspi-blacklist.conf

Add a ‘#’ character in front of the line spi-bcm2708. Use CTRL-X, then Y, then Return to save the file and exit. Reboot using the following :

sudo reboot

To check the change has worked run the following command :

lsmod

You should see “spi_bcm2708″ listed in the output.

Install Python SPI Wrapper

In order to read data from the SPI bus in Python we can install a library called ‘py-spidev’. To install it we first need to install ‘python-dev’ :

sudo apt-get install python-dev

Then to finish we can download ‘py-spidev’ and compile it ready for use :

mkdir py-spidev
cd py-spidev
wget https://raw.github.com/doceme/py-spidev/master/setup.py
wget https://raw.github.com/doceme/py-spidev/master/spidev_module.c
sudo python setup.py install

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.

This entry was posted in Hardware, Python, Tutorials & Help and tagged , , , , , . Bookmark the permalink.

34 Responses to Analogue Sensors On The Raspberry Pi Using An MCP3008

  1. Great guide – thanks for keeping it simple. :-)

  2. richard lines says:

    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.

    • andycrofts says:

      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.

    • Roman says:

      Quick question: do you know how the data= line would change for the 3208 (which has 12 bits instead of 10?)

  3. richard lines says:

    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

  4. Fred Smith says:

    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!

    • Matt says:

      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.

      • George says:

        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….

  5. Schwabinger says:

    Cool! I modified your script and now I can use a potentiometer to “tune” my RaspRadio …

    http://mightyohm.com/forum/viewtopic.php?f=2&t=3420

    Thank’s a lot!
    Schwabinger

  6. Matt says:

    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

  7. George says:

    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

  8. Schwabinger says:

    Hi,

    in my radio project I now use your solution to control two pots (volume, select station) and two push buttons (signal station number, initiate shutdown/reboot).

    http://www.flickr.com/photos/rigasw/sets/72157638612312533/

    Do you know whether it does work as well with Pi Version A?

    Schwabinger

    • Matt says:

      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.

  9. Schwabinger says:

    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

    • Matt says:

      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.

  10. Ton says:

    Hi,

    Thank you for this extensive and very clear explanation. It’s very helpful for a newbie like me.

    Ton

  11. Barney Ward says:

    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

  12. Pedro says:

    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.

    • Matt says:

      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.

  13. George says:

    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

    • Matt says:

      I would use a potentiometer to feed 0V – 3.3V to an input and see what result that gives.

      • George says:

        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?

  14. Thanks once again for the tutorial. im using it to re-tool the Picorder. :)

  15. George says:

    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….

  16. Steve says:

    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.

    • Matt says:

      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?

  17. Steve says:

    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.

  18. Roman says:

    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

    • Matt says:

      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 :)

  19. Found out what happens if you wire it up wrong. Chip = Radiator :-D

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>