Interfacing an SPI ADC (MCP3008) chip to the Raspberry Pi using C++ (spidev)

来源:互联网 发布:json转字符串 编辑:程序博客网 时间:2024/05/21 17:35

In this entry I will demonstrate how to interface the MCP3008; an SPI-based analog to digital converter (ADC) integrated chip, to the Raspberry Pi. This enables  the Raspberry Pi to interpret analog voltages that are in turn typically emitted by analog-based sensors to reflect a measure of  a physical characteristic such as acceleration, light intensity or temperature. We will start by briefly examining the SPI interface.

Figure 1. 3 SPI slave devices connected to a single SPI master device. In this case a total of 3+n - > 3+3= 6 wires are required. Image acquired from here

Figure 1. 3 SPI slave devices connected to a single SPI master device. In this case a total of 3+n – > 3+3= 6 wires are required. Image acquired fromhere

A gentle introduction to the Serial Peripheral Interface (SPI)

The Serial Peripheral Interface (SPI) is a communication bus that is used to interface one or more slave peripheral integrated circuits (ICs) to a single master SPI device; usually a microcontroller or microprocessor of some sort. Many SPI  Peripheral ICs exist. They include, analog to digital converters (ADC), digital to analog converters (DAC), general purpose input/output (GPIO) expansion ICs, temperature sensing ICs, accelerometers and many more.  In this regards the SPI bus is similar to the I2C bus. the SPI’s main advantage over the I2C bus is that the SPI bus speeds can be very fast; 10Mbps is not unusual and maximum speeds can go as high as the hardware (master controller, slave peripheral, and printed circuit board traces connecting them) will go. Speeds that exceed 50Mbps are not unusual.

Having said that, the main disadvantage of the SPI bus w.r.t the I2C bus is the numbers of wires required by the bus. The I2C bus is a ’2-wire’ bus that theoretically can be used to connect up to 127 devices, and significantly more if a 9-bit addressing scheme is used instead of the classical 7-bit address. The SPI bus is a ’3+n wire’ bus. where ‘n’ is the number of slave devices attached to the master SPI device. For example in the example shown in Figure 1, a total of 6 wires are required by the SPI bus and six corresponding pins required on the master SPI device to interface the 3 slave SPI devices to the master SPI controller. This means that not only is the number of wires on an SPI bus larger than on an I2C bus, but that the number of wires continue to increase linearly as we add more slave devices on to the bus. Notice that slave SPI devices almost always require 4 pins to attach themselves to the SPI bus.

The 3 SPI wires shared by all devices on the SPI  bus are:

  • Master in slave out (MISO). Data is moved from slave to master on this wire.
  • Master out slave in (MOSI). Data is moved from master to slave on this wire.
  • Serial clock (SCLK). This clock is always generated by the master controller and is used to synchronize the transmission of data between devices on the bus.

In addition to these wires we have ‘n’ wires for ‘n’ slave devices on the bus. Each one of these wires carries the chip select signal (SS or CS) for its respective device. Only one slave device can have its chip select signal asserted by the master controller at a time.

Figure 2 SPI bus operation

Figure 2 SPI bus operation

The operation of the SPI bus is conceptually simple. Both the master controller and each slave device contain a shift register. When the chip select signal of a slave device is asserted (usually by being pulled low), the MISO, MOSI wires are used to connect its shift register with that of the master device. Clock pulses are then generated (by the master device)  to shift data between the two shift registers enabling communication. In this sense the read and write operation are combined. For example by shifting the contents of the master device shift register to  that of the slave device, we are  also shifting the data in the slave device shift register to that of the master.

Finally, there are 4 different SPI modes that can be used. Each mode defines a particular clock phase (CPHA) and polarity (CPOL) with respect to the data. For the purpose of this tutorial we will be utilizing SPI mode 0 which is also known as mode (0,0) or mode (CPHA=0,CPOL=0).

To learn more about the SPI bus, I refer the reader to these excellent resources:

  • Serial Peripheral Interface Bus (Wikipedia)
  • Understanding the SPI Bus with NI LabVIEW
  • Introduction to Serial Peripheral Interface

The MCP3008 SPI ADC chip

The MCP3008 chip is an SPI based analogue to digital converter (ADC). It has 8 analog input channels that can be configured for single ended and differential ADC conversions. The MCP3008 is a 10-bit ADC  that can convert  up to 200 kilo samples per second (200ksps) (@ 5V!!). The MCP3008 comes in 28 PDIP and SOIC packages. A pinout is provided in Figure 3. The datasheet for the MCP3008 can be can be downloaded fromhere.

Figure 3. Pinout of the MCP3008 IC

Figure 3. Pinout of the MCP3008 IC Taken form the MCP 3008 datasheet

Typically the VDD pin is connected to  3.3V power. The AGND and DGND pins can be connected directly to the ground reference point. The VREF pin is the reference voltage which is the largest possible voltage that the ADC can interpret. In our scenario we will connect the VREF pin to 3.3V (same as VDD). So if 3.3V was sampled on any of the ADC’s channels it would be interpreted as the maximum digital value that can be represented by this 10-bit ADC i.e. 2^10 – 1 = 1023. Similarly the smallest analog voltage that the ADC can detect (also known as the ‘LSB size’) is VREF/1024. Which in our case is 3.3V/1024= 3.22mV and represents a digital value of 1. The equation that converts between the analog voltage and its digital interpretation is given by “Digital output code = 1024*VIN/VREF”; where VIN is the analog input voltage and VREF is the reference voltage.

A complete SPI transaction for the MCP3008 (SPI mode 0) is depicted in Figure 4. The complete transaction consists of 3 bytes being transmitted from master (Raspberry Pi) to slave (MCP3008) and 3 bytes transmitted from slave to master. Recall that due to the nature of the shift register  operation of the SPI bus, shifting 3 bytes into the slave device (writing to the slave MCP3008) will by default cause the 3 bytes in the slave device to be shifted out into the master device (Raspberry Pi).

Figure 4. Complete SPI transaction for the MCP3008. Diagram taken from MCP 3008 Datasheet.

Figure 4. Complete SPI transaction for the MCP3008. Diagram taken from MCP 3008 Datasheet.

  1. Raspberry Pi asserts the chip select signal connected to the MCP3008 (CS0 in our case) by setting it to 0V. This is typically taken care of internally by the spidev driver whenever the proper ioctl() function is called.
  2. Raspberry Pi sends a byte containing a value of ’1′ to the MCP3008. This is a start bit. At the same time the MCP3008 sends back a ‘don’t care’ byte to the Raspberry Pi.
  3. Raspberry Pi then sends a second byte whose most significant nibble (SGL/DIFF,D2,D1 & D0 bits) indicate the channel that we want to convert and whether we want  single-ended or differential conversion (See Figure 5). For example if this nibble is “1000″, the conversion will be single-ended and take place on channel 0 (CH0 pin). The least significant nibble is sent as ‘don’t care’. At the same time, the MCP3008 sends back the two most significant bits of the digital value (result) of the conversion (bits 8 and 9).
  4. Raspberry Pi sends another  ‘don’t care’ byte to the MCP3008. At the same time the MCP3008 send back a byte containing bits 7 through 0 0f the digital value (result) of the conversion.
  5. The Raspberry Pi then merges bits 8 & 9 from the second received byte with bits 7  through 0 from the third received byte to create the 10-bit digital value resulting from the conversion.

Figure 5. Configuring the ADC conversion

Figure 5. Configuring the ADC conversion. Table taken from MCP3008 datasheet.

Connecting the Raspberry Pi to the MCP3008.

So let’s connect  the Raspberry Pi to the MCP3008 ! This can be done as shown in Figure 6.

Figure 6. Connecting the Raspberry Pi to the MCP3008

Figure 6. Connecting the Raspberry Pi to the MCP3008

The connections between the Raspberry Pi and the other parts can be made via Male-to Female jumper wires or via one of Adafruit’s Pi cobbler kits (ver1  orver2).  Note that in this particular scenario, I opted for attaching the analog input coming from a potentiometer to channel 0 (CH0 pin). Also the SPI peripheral on the Raspberry Pi has only two chip selects (CS0,CS1 pins) and can therefore only be used to attach a maximum of two SPI slave devices to the Raspberry Pi. We chose the CS0 pin.

Enabling Spidev on the Raspberry Pi

The next step is to enable the spidev interface on the Raspberry Pi :

  • Turn on the Raspberry Pi and connect it to the network.
  • Log in to the Raspberry Pi remotely over SSH.
  • Open the raspi-black-list.conf  file using the following command : “sudo nano /etc/modprobe.d/raspi-blacklist.conf”
  • Comment out the “blacklist spi-bcm2708” entry by putting a hash # sign in front of it. So it looks like “#blacklist spi-bcm2708”.
  • Type “sudo reboot”. This should cause the Raspberry Pi to reboot. Which will terminate the SSH session.
  • Start a new SSH session and type “ls /dev/spidev*” If you see the output displayed in Figure 7, then the SPI driver (spidev) has been successfully enabled on the Raspberry Pi.

Figure 7. spidev enabled

Figure 7. spidev enabled

Note how there are two spidev devices displayed in Figure 7. The first number refers to the SPI peripheral which in both cases is 0, as there is only one SPI device available to us on the Raspberry Pi. The second number represents the chip select i.e. spidev0.0 for CS0 and spidev0.1 for CS1 . In this scenario we are only interested in spidev0.0 since we will be using CS0.

C++ Program to control the MCP3008 from the Raspberry Pi

So the next step is to write C++ class / program that allows us to communicate with the MCP3008. We called this class “mcp3008Spi”. This c++ class relies heavily on the spidev user space interface. Although it has “mcp3008″ in its name, it was designed to be easily modified to work with other SPI chips as well. The “mcp3008Spi”‘s class definition is provided below. The class consists of four variables, two constructors, one destructor, spiOpen() that opens and configures the spidev interface, spiClose() that closes the spidev interface and a “spiWriteRead()” function that transfers data between master and slave devices.

/***********************************************************************
 * This header file contains the mcp3008Spi class definition. 
 * Its main purpose is to communicate with the MCP3008 chip using 
 * the userspace spidev facility. 
 * The class contains four variables:
 * mode        -> defines the SPI mode used. In our case it is SPI_MODE_0. 
 * bitsPerWord -> defines the bit width of the data transmitted. 
 *        This is normally 8. Experimentation with other values 
 *        didn't work for me
 * speed       -> Bus speed or SPI clock frequency. According to 
 *                https://projects.drogon.net/understanding-spi-on-the-raspberry-pi/
 *            It can be only 0.5, 1, 2, 4, 8, 16, 32 MHz. 
 *                Will use 1MHz for now and test it further.
 * spifd       -> file descriptor for the SPI device
 * 
 * The class contains two constructors that initialize the above 
 * variables and then open the appropriate spidev device using spiOpen().
 * The class contains one destructor that automatically closes the spidev
 * device when object is destroyed by calling spiClose().
 * The spiWriteRead() function sends the data "data" of length "length" 
 * to the spidevice and at the same time receives data of the same length. 
 * Resulting data is stored in the "data" variable after the function call.
 * ****************************************************************************/
#ifndef MCP3008SPI_H
    #define MCP3008SPI_H
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string>
#include <iostream>
 
class mcp3008Spi{
 
public:
    mcp3008Spi();
    mcp3008Spi(std::string devspi, unsigned char spiMode, unsigned int spiSpeed, unsigned char spibitsPerWord);
    ~mcp3008Spi();
    int spiWriteRead( unsigned char *data, int length);
 
private:
    unsigned char mode;
    unsigned char bitsPerWord;
    unsigned int speed;
    int spifd;
 
    int spiOpen(std::string devspi);
    int spiClose();
 
};
 
#endif

The constructors initialize the variables and then call the “spiOpen()” function to initialize the spidev interface. The destructor is typically called when the “mcp3008Spi” object is about to be destroyed and calls the spiClose() function to close the spidev interface just before the object is destroyed.

The spiWriteRead() function can be used to send/receive SPI transactions made of one or multiple bytes and should be compatible for use with other SPI devices. The member function definitions are provided below.

#include "mcp3008Spi.h"
using namespace std;
/**********************************************************
 * spiOpen() :function is called by the constructor.
 * It is responsible for opening the spidev device 
 * "devspi" and then setting up the spidev interface.
 * private member variables are used to configure spidev.
 * They must be set appropriately by constructor before calling
 * this function. 
 * *********************************************************/
int mcp3008Spi::spiOpen(std::string devspi){
    int statusVal = -1;
    this->spifd = open(devspi.c_str(), O_RDWR);
    if(this->spifd < 0){
        perror("could not open SPI device");
        exit(1);
    }
 
    statusVal = ioctl (this->spifd, SPI_IOC_WR_MODE, &(this->mode));
    if(statusVal < 0){
        perror("Could not set SPIMode (WR)...ioctl fail");
        exit(1);
    }
 
    statusVal = ioctl (this->spifd, SPI_IOC_RD_MODE, &(this->mode)); 
    if(statusVal < 0) {
      perror("Could not set SPIMode (RD)...ioctl fail");
      exit(1);
    }
 
    statusVal = ioctl (this->spifd, SPI_IOC_WR_BITS_PER_WORD, &(this->bitsPerWord));
    if(statusVal < 0) {
      perror("Could not set SPI bitsPerWord (WR)...ioctl fail");
      exit(1);
    }
 
    statusVal = ioctl (this->spifd, SPI_IOC_RD_BITS_PER_WORD, &(this->bitsPerWord));
    if(statusVal < 0) {
      perror("Could not set SPI bitsPerWord(RD)...ioctl fail");
      exit(1);
    }   
 
    statusVal = ioctl (this->spifd, SPI_IOC_WR_MAX_SPEED_HZ, &(this->speed));     
    if(statusVal < 0) {
      perror("Could not set SPI speed (WR)...ioctl fail");
      exit(1);
    } 
 
    statusVal = ioctl (this->spifd, SPI_IOC_RD_MAX_SPEED_HZ, &(this->speed));     
    if(statusVal < 0) {
      perror("Could not set SPI speed (RD)...ioctl fail");
      exit(1);
    }
    return statusVal;
}
 
/***********************************************************
 * spiClose(): Responsible for closing the spidev interface.
 * Called in destructor
 * *********************************************************/
 
int mcp3008Spi::spiClose(){
    int statusVal = -1;
    statusVal = close(this->spifd);
        if(statusVal < 0) {
      perror("Could not close SPI device");
      exit(1);
    }
    return statusVal;
}
 
/********************************************************************
 * This function writes data "data" of length "length" to the spidev
 * device. Data shifted in from the spidev device is saved back into 
 * "data". 
 * ******************************************************************/
int mcp3008Spi::spiWriteRead( unsigned char *data, int length){
 
  struct spi_ioc_transfer spi[length];
  int i = 0; 
  int retVal = -1;  
 
// one spi transfer for each byte
 
  for (i = 0 ; i < length ; i++){
 
    spi[i].tx_buf        = (unsigned long)(data + i); // transmit from "data"
    spi[i].rx_buf        = (unsigned long)(data + i) ; // receive into "data"
    spi[i].len           = sizeof(*(data + i)) ;
    spi[i].delay_usecs   = 0 ; 
    spi[i].speed_hz      = this->speed ;
    spi[i].bits_per_word = this->bitsPerWord ;
    spi[i].cs_change = 0;
}
 
 retVal = ioctl (this->spifd, SPI_IOC_MESSAGE(length), &spi) ;
 
 if(retVal < 0){
    perror("Problem transmitting spi data..ioctl");
    exit(1);
 }
 
return retVal;
 
}
 
/*************************************************
 * Default constructor. Set member variables to
 * default values and then call spiOpen()
 * ***********************************************/
 
mcp3008Spi::mcp3008Spi(){
    this->mode = SPI_MODE_0 ; 
    this->bitsPerWord = 8;
    this->speed = 1000000;
    this->spifd = -1;
 
    this->spiOpen(std::string("/dev/spidev0.0"));
 
    }
 
/*************************************************
 * overloaded constructor. let user set member variables to
 * and then call spiOpen()
 * ***********************************************/
mcp3008Spi::mcp3008Spi(std::string devspi, unsigned char spiMode, unsigned int spiSpeed, unsigned char spibitsPerWord){
    this->mode = spiMode ; 
    this->bitsPerWord = spibitsPerWord;
    this->speed = spiSpeed;
    this->spifd = -1;
 
    this->spiOpen(devspi);
 
}
 
/**********************************************
 * Destructor: calls spiClose()
 * ********************************************/
mcp3008Spi::~mcp3008Spi(){
    this->spiClose();
}

Finally  we write an application to test the “mcp3008Spi” class. The application initializes an “mcp3008Spi” object. sends  3 bytes through the spidev interface to configure the conversion for single ended conversion on channel 0. 3 bytes are transmitted back at the same time. The last two contain the digital result of the conversion.

/***********************************************************************
 * mcp3008SpiTest.cpp. Sample program that tests the mcp3008Spi class.
 * an mcp3008Spi class object (a2d) is created. the a2d object is instantiated
 * using the overloaded constructor. which opens the spidev0.0 device with 
 * SPI_MODE_0 (MODE 0) (defined in linux/spi/spidev.h), speed = 1MHz &
 * bitsPerWord=8.
 * 
 * call the spiWriteRead function on the a2d object 20 times. Each time make sure
 * that conversion is configured for single ended conversion on CH0
 * i.e. transmit ->  byte1 = 0b00000001 (start bit)
 *                   byte2 = 0b1000000  (SGL/DIF = 1, D2=D1=D0=0)
 *                   byte3 = 0b00000000  (Don't care)
 *      receive  ->  byte1 = junk
 *                   byte2 = junk + b8 + b9
 *                   byte3 = b7 - b0
 *     
 * after conversion must merge data[1] and data[2] to get final result 
 * 
 * 
 * 
 * *********************************************************************/
#include "mcp3008Spi.h"
 
using namespace std;
 
int main(void)
{
    mcp3008Spi a2d("/dev/spidev0.0", SPI_MODE_0, 1000000, 8);
    int i = 20;
        int a2dVal = 0; 
    int a2dChannel = 0;
        unsigned char data[3];
 
    while(i > 0)
    {
        data[0] = 1;  //  first byte transmitted -> start bit
        data[1] = 0b10000000 |( ((a2dChannel & 7) << 4)); // second byte transmitted -> (SGL/DIF = 1, D2=D1=D0=0)
        data[2] = 0; // third byte transmitted....don't care
 
        a2d.spiWriteRead(data, sizeof(data) );
 
        a2dVal = 0;
                a2dVal = (data[1]<< 8) & 0b1100000000; //merge data[1] & data[2] to get result
                a2dVal |=  (data[2] & 0xff);
        sleep(1);
        cout << "The Result is: " << a2dVal << endl;
        i--;
    }
    return 0;
}

Save the above three files as mcp3008Spi.h, mcp3008Spi.cpp & mcp3008SpiTest.cpp respectively and put them in the same directory on the Raspberry Pi; say a directory called spitest. Then build the application natively on the Raspberry Pi using the following command “ g++ -Wall -o OutBin  mcp3008Spi.cpp mcp3008SpiTest.cpp”. Run the binary “OutBin” as root i.e. “sudo ./OutBin“. Start rotating the potentiometer and the digital values printed should vary within a range of 0-1023 as shown in Figure 8. Congratulations! You got the Raspberry Pi and MCP3008 to communicate! The Raspberry Pi can now be used to read/interpret analog data.

Figure 8. Digital result of MCP3008 conversion printed to console as potentiometer needle rotated from one extreme to the other

Figure 8. Digital result of MCP3008 conversion printed to console as potentiometer needle rotated from one extreme to the other

The code shown in this blog is available for download (git) here.

A note about sampling speed:

The maximum sampling frequency for the MCP3008 Chip is 200ksps at 5V and 75Ksps at 2.7V. At 3.3V, I doubt that it would go above 100Ksps. Furthermore because the spidev interface used here is a Linux userspace interface with slow access to kernel space and non-deterministic timing, expect significantly lower sample rates.

There really is no way to directly control the sampling speed. You could speed up/slow down the speed of the spi clock (in the code example set to 1MHz…Maximum is 1.35MHz @ 2.7V) and call the spiWriteRead function at regular intervals using functions such as usleep or POSIX timers but I’m not sure what kind of sample rates you’ll get without doing some experimentation.

At maximum spi clock speed and calling spiWriteRead as fast as possible I doubt you’d be able to achieve anything near 20ksps. There will also be synchronicity issues i.e. the delay between consecutive samples could exhibit a large variance due to the non-deterministic nature of the Linux Kernel. Remember….Linux is NOT a real-time operating system!!!

This solution is meant for applications where real-time constraints such as sampling speed requirements are very modest or even non-existent.

If you have strict sampling requirements, consider one of the following approaches:

  • Access the SPI registers directly by mmaping into /dev/mem or via the bcm2835 library.
  • Bitbang SPI via GPIO or use a Parallel ADC connected to the RPI’s GPIOs. The RPI GPIOs can be controlled by many different methods, one of which can achieve  20MHz + toggling speed (also mmaping into /dev/mem  or via thebcm2835 library but for GPIOs)
  • Write your own custom/optimized ADC device driver. Have a look at the 24th (June 2014) edition of the MagPi Magazine. The first article explains how to write a Linux driver for a 6-bit 10MSPS ADC and use it in conjunction with GNUplot to plot the waveforms.
  • Use a microcontroller with a decent built-in ADC and generous amounts of memory for buffering….and maybe a DMA unit e.g. STM32F0/1/2/3/4, TIVA C , PIC32MX, PIC32MZ, e.t.c. The micro can then transmit ADC data to the RPI over UART, SPI or I2C.

For speech related tasks, I recommend using a USB microphone with the PortAudioAudio I/O library. Its the same library that the Audacity software uses. Unfortunately I’ve never used PortAudio..but it is well documented and the main PortAudio site hosts a nifty tutorial.


http://hertaville.com/2013/07/24/interfacing-an-spi-adc-mcp3008-chip-to-the-raspberry-pi-using-c/

0 0
原创粉丝点击