Since the Raspberry Pi is fundamentally a digital device, any I/O that is done through it’s GPIO pins will happen through high (one) and low (zero) states. When it comes to input signals that are analog, most likely from a transducer, they need to be converted to the digital domain so the Raspberry Pi can understand them. The MCP3008 is a 10-bit 8-channel analog-to-digital converter chip that has a very straightforward API implementation in GPIO Zero. The MCP3008 device uses an SPI (Serial Peripheral Interface) communication protocol that is fully taken care by the API.
SPI requires four pins for the communication:
- A “SCLK” pin which provides the clock (pulse train) for the communication.
- A “MOSI” pin (Master Out, Slave In) which the Pi uses to send information to the device.
- A “MISO” pin (Master In, Slave Out) which the Pi uses to receive information from the device.
- A “CEX” pin which is used to start and end the communication with the device, usually for one data sample at a time. Because the Raspberry Pi can have more than one device sharing the same SCLK, MOSI, and MISO pins, the CE0 or CE1 pin will indicate which device to use.
Additionally, the MCP3008 requires two separate voltage inputs ranging from 2.7 to 5.5V. The supply voltage at 5V allows for a higher data sampling rate of 200 kSamples/s (which should be plenty for the Raspberry Pi). A reference voltage of 3.3V improves the absolute resolution to about 3 mV instead of 5 mV since, for a 10-bit device, 1LSB = VREF/1024. However, the choice of VREF also has to satisfy the analog input range requirement of the transducer. If the input is higher that 5V, a resistive voltage divider can be used. Finally, in the case of a Raspberry Pi, both the digital and analog grounds on the device can share the same GND pin.
Important! You must enable the SPI for the MCP3008 communication to work. To do so, go to the Raspberry Pi Configuration menu and select Enable for the SPI in the Interfaces tab.
Hardware vs. Software SPI implementation
The Raspberry Pi has both a hardware and a software implementation of the SPI protocol. It is worth investigating what kind of sampling rates can be achieved with those two types.
The figure on the left shows a comparison between software and hardware implementation results of the sampling period per channel as a function of the number of analog inputs. The code used to collect the data, as well as GPIO pin configurations, can be found on my GitHub page.
For each point on the graph, data was sampled as fast as possible, i.e., with no time step clock, where the only I/O occurring in the execution loop was reading the MCP3008 output. Computational time storing the data in an array was negligible. Not surprisingly, the sampling period increases linearly with the number of input channels and, on my Raspberry Pi 4B, the hardware implementation is in average 3.6 times faster than the software one.
The hardware sampling rate is approximately 6.25 kSamples/s and the software one is approximately 1.72 kSamples/s. Note that it’s common to use Samples (or kSamples) per second for a DAQ device, where the number usually gives the maximum throughput of the device. For example, 5 channels with the hardware implementation still gives 6.25 kSamples/s. However the rate is 1.25 kSamples/s per channel.
Still on the 5 channels example, you can very likely get away without an anti-aliasing filter. Noise above 625 Hz (half of the 1250 Hz sampling rate) should have negligible amplitude and shouldn’t show up as an alias in the actual signal. That doesn’t mean that the analog signal being collected with the MCP3008 doesn’t have (relatively) low frequency noise, which in turn can be filtered using a digital low-pass filter in the DAQ software.
Example with a Potentiometer
A potentiometer is basically a variable resistive voltage divider that will output a voltage between a high and a low voltage value, in our case VREF and GND. Because potentiometers convert a linear or angular position input to a voltage output, they do fall into the transducer category.
In this example, the analog output of the potentiometer will be sampled by the DAQ software. To add a bit of visual excitement, the sampled voltage will be used to change the PWM output that controls the intensity of the LED.
The wires on the right hand side of the figure are all connected to the appropriate pins on the Raspberry Pi. Since hardware SPI will be used:
- SCLK = GPIO11
- MISO = GPIO9
- MOSI = GPIO10
- CEO = GPIO8
PWM is connected to GPIO16. Note that the LED requires a series resistor, where R can be anywhere between 300 to 1000 Ohm.
Also, pay attention to the semi-circular notch on the MCP3008 chip, so it can be inserted with the right orientation into the breadboard.
The Python code for the example is shown below, where there are a few things worth mentioning:
- The sampling period is 0.02 s, which gives a sampling rate of 50 Samples/s. That is far below the limit for the hardware SPI implementation (6.25 kSamples/s) and even the software one (1.72 kSamples/s).
- The execution loop concept with a time step clock is used to ensure a fairly accurate sampling period. Both I/O and computation tasks are performed within a time step with plenty of time to spare.
- Due to the noise in the analog signal, most likely due to the questionable quality of the potentiometer, a digital low-pass filter is implemented in the code. The cutoff frequency of 2 Hz can be modified for some extra exploration of its effect.
- For the plotting of the sampled signal to work, a module named utils.py has to be accessible for importing as discussed in my previous post A useful Python plotting module.
import time import numpy as np from utils import plot_line from gpiozero import PWMOutputDevice, MCP3008 # Creating LED PWM object led = PWMOutputDevice(16) # Creating ADC channel object pot = MCP3008(channel=0, clock_pin=11, mosi_pin=10, miso_pin=9, select_pin=8) # Assining some parameters tsample = 0.02 # Sampling period for code execution (s) tstop = 10 # Total execution time (s) vref = 3.3 # Reference voltage for MCP3008 # Preallocating output arrays for plotting t =  # Time (s) v =  # Potentiometer voltage output value (V) vfilt =  # Fitlered voltage output value (V) # First order digital low-pass filter parameters fc = 2 # Filter cutoff frequency (Hz) wc = 2*np.pi*fc # Cutoff frequency (rad/s) tau = 1/wc # Filter time constant (s) c0 = tsample/(tsample+tau) # Digital filter coefficient c1 = tau/(tsample+tau) # Digital filter coefficient # Initializing filter previous value valueprev = pot.value time.sleep(tsample) # Initializing variables and starting main clock tprev = 0 tcurr = 0 tstart = time.perf_counter() # Execution loop print('Running code for', tstop, 'seconds ...') while tcurr <= tstop: # Getting current time (s) tcurr = time.perf_counter() - tstart # Doing I/O and computations every `tsample` seconds if (np.floor(tcurr/tsample) - np.floor(tprev/tsample)) == 1: # Getting potentiometer normalized voltage output valuecurr = pot.value # Filtering value valuefilt = c0*valuecurr + c1*valueprev # Calculating current raw and filtered voltage vcurr = vref*valuecurr vcurrfilt = vref*valuefilt # Updating LED PWM output led.value = valuefilt # Updating output arrays t.append(tcurr) v.append(vcurr) vfilt.append(vcurrfilt) # Updating previous filtered value valueprev = valuefilt # Updating previous time value tprev = tcurr print('Done.') # Releasing pins pot.close() led.close() # Plotting results plot_line([t]*2, [v, vfilt], yname='Pot Output (V)', legend=['Raw', 'Filtered']) plot_line([t[1::]], [1000*np.diff(t)], yname='Sampling Period (ms)')
The graphs below show the results of one run on my Raspberry Pi 4B. Observe how the low-pass filter removes the noise from the raw signal as I twirl around the potentiometer knob. That of course comes with a price: the filtered signal now lags the original signal. In DAQ applications where the signal doesn’t need to be filtered in real-time, there are “zero-lag” filtering techniques that can be applied after the signal was acquired. Also note the consistent sampling time step of 20 ms.
A note on Quantization
What would happen if I never touched the potentiometer during the program execution, leaving its output at a constant value? The result is shown in the next graph, where the quantization effect is quite clear on the (very low amplitude) signal noise riding on top of the output voltage. The discrete nature of the output can now be observed, and the values are exactly VREF/210 = 3.3/1024 = 0.0032 V apart!
4 thoughts on “MCP3008 with Raspberry Pi”
Well thank you, Sean.