Saturday, December 22, 2012

Analog Interface

The capability to read analog inputs is a feature that is greatly missed on the Raspberry Pi, but I agree with the decision to omit this capability in order to keep the price down.  Besides, if they did include an analog interface, many would complain that it isn't adequate for their purpose.  How many input channels do you need?  What resolution? 8 bit, 10 bit, 12 bit 16 bit?  What throughput rate?

Fortunately, there are many analog input chips that use the SPI or I2C bus, making it almost trivial to add analog inputs to the Pi.  I chose the MCP3008, an 8 channel 10 bit ADC available from Adafruit.com.  Add a bi-directional logic level converter and some connectors and you're ready to go.

The level converter is on my main interface board which provides two SPI bus connectors.  The analog interface is a simple board which includes a connector for SPI, the MCP3008 chip, a jumper to choose the analog reference, and screw terminals for the inputs.  I ended up adding several more screw terminals connected to 5V in order to power thermistors. 

The SPI serial bus is full duplex, but the way it works may seems odd to a programmers point of view.  (It makes perfect sense if you understand how the hardware works.)  You may be familiar with how full duplex works on an RS-232 line: data can be sent and received at the same time, but independently.  That independence is due to the fact that RS-232 is an asynchronous protocol.  SPI is a synchronous protocol; meaning everything is driven by a clock pulse.
Data bits will be sent out the MISO line on each cycle of the CLOCK line.  At the same time data bits are being read in on the MOSI line.  The number of bits out is the number of bits you will read back in.  This means that you may have to write more bits than expected for a given command and you may read bits that are unused.

Fortunately, we don't have to worry much about the ugly details at the lower level.  There is a device driver for SPI that is included with the recent versions of Raspbian and the WiringPi library provides support for SPI I/O.  The functions wiringPiSPISetup and  wiringPiSPIDataRW are all that is needed.  Here is the source code for a program that I used when testing and calibrating sensors.


#include <stdio.h> #include <stdint.h> #include <wiringPi.h> #include <gertboard.h> // read SPI data from MCP3008 chip, 8 possible adc's (0 thru 7) int readadc(adcnum) { uint8_t buff[3]; int adc; if ((adcnum > 7) || (adcnum < 0)) return -1; buff[0] = 1; buff[1] = (8+adcnum)<<4; buff[2] = 0; wiringPiSPIDataRW(0, buff, 3); adc = ((buff[1]&3) << 8) + buff[2]; return adc; } int main(int argc, char *argv[]) { int i, chan; uint32_t x1, tot ; printf ("SPI test program\n") ; // initialize the WiringPi API if (wiringPiSPISetup (0, 1000000) < 0) return -1 ; // get the channel to read, default to 0 if (argc>1) chan = atoi(argv[1]); else chan = 0; // run until killed with Ctrl-C while (1) { tot = 0; for (i=0; i<100; i++) { // read data and add to total x1 = readadc(chan); tot += x1; delay(10); } // display the average value printf("chan %d: %d \n", chan, (tot/100)) ; } return 0 ; }

Note: The SPI device is not loaded by default.  The easy way to get it loaded is to used the "gpio" utility that comes with the WiringPi library.  Just enter
      gpio load spi
and the drivers will be loaded and ready to use.

12 comments:

  1. Love your code
    Made SPI all the more easier for me to understand :D
    Why don't you host your code on GitHub ?

    ReplyDelete
  2. Glad that you found it helpful. That's the reason I do this blog.
    I will have my code on github eventually, I just have to catch up on a backlog of other stuff. I am working on another project that is hosted on google code (using a git interface) but it's not RasPi related.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. Hey, Thanks so much for this code. There are many phyton-adc-RPI-tutorials but i was searching for it in C. I would have adapt it myself but im very glas i found your code :)

    There is one question left:
    How to convert the other 7 analoug channels? (how to reads them into the RPI)
    Do I only have to change
    x1 = readadc(chan);
    the chan-parameter?

    Thanks :)

    ReplyDelete
  5. I am really glad to find your blog! :)I had exactly the same problem than "Anonymous" above ;). Since I am really a newbie in manipulating bits, your code really helped me and made me understanding things! Cool! thanks! :)

    ReplyDelete
  6. Hi Ted! Found it very useful for me!! I have the same MCP3008 but I always receive 1023 as result...I think I'm doing something wrong in the cabling...have you got some schematics?

    Thank you

    ReplyDelete
    Replies
    1. Adafruit has a good write up on the MCP3008
      Look here:
      http://learn.adafruit.com/reading-a-analog-in-and-controlling-audio-volume-with-the-raspberry-pi


      Delete
    2. It looks like the Adafruit tutorial is using bit banging, rather than true SPI. Therefore, the wiring does not match your wiring for SPI based readout.

      Delete
    3. Good point. I used WiringPi as my interface, so I had to use the designated SPI pins. Letting Gordon do all the hard work really does simplify these projects.

      http://wiringpi.com/

      Delete
  7. Hi Ted,
    I'm trying to read from a MCP3208 the 12 bit version of the ic you used above.
    I modified your line;
    adc = ((buff[1]&3) << 8) + buff[2]; to read
    adc = ((buff[1]&15) << 8) + buff[2]; so that I capture the 4 lower bits; bit 11, bit 10, bit9 and bit 8 and not only just bit9 and bit8 as your programme did. For some reason bit 10 and 11 never change from zero. If I do a printf on buff[1] immediately after the line; wiringPiSPIDataRW(0, buff, 3); while the analog in signal=vref I only get 3 instead of 15. For some reason I get returned a 10bit instead of a 12 bit result. Any ideas???

    Pat.


    int readadc(adcnum)
    {
    uint8_t buff[3];
    int adc;
    if ((adcnum > 7) || (adcnum < 0)) return -1; buff[0] = 1;
    buff[1] = (8+adcnum)<<4;
    buff[2] = 0;
    wiringPiSPIDataRW(0, buff, 3);
    adc = ((buff[1]&3) << 8) + buff[2];
    return adc;
    }

    ReplyDelete
  8. Hi there,
    long time ago Pat asked a question on integrating the mcp3208. Now i have the same problem. I have modified the code but i can only read 4 input ports, after readind port 0 to 3 the values are repeating. idea?
    buff[0] = 6;
    buff[1] = (8+adcnum)<<6;
    buff[2] = 0;
    buff[3] = 0;
    wiringPiSPIDataRW(0, buff, 3);
    adc = 0;
    adc = ((buff[1]& 0x0F) << 8) | buff[2];

    ReplyDelete
    Replies
    1. buff[1] overflows if adcnum>3. The hi order bit needs to be in buff[0]. Use this:

      buff[0] = 0x06 | ((adcnum & 0x07) >> 7);

      I also ran in to this once I tried reading the higher adc channels but forgot to update the blog. Thanks for the reminder.

      Delete