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
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:
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
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:
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 from here.
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 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
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 :
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
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!!!
If you have strict sampling requirements, consider one of the following approaches:
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/