Python Programming Tutorial: Getting Started with the Raspberry Pi
Experiment 3: SPI and Analog Input
Many sensors out there use an analog voltage to convey their measurement data. For example, photocells change their resistance depending on how much light is falling on the sensor. By using a voltage divider in our circuit, we can effectively measure the amount of ambient light by measuring a voltage.
The bad news is that our Raspberry Pi does not come with any way to measure an analog voltage. To do that, we'll need to rely on a separate piece of circuitry: an analog-to-digital converter (ADC). Specifically, we'll be using the Microchip MCP3002, which is a niftly little chip that can measure up to 2 analog voltages on separate channels and report their values over the Serial Peripheral Interface (SPI) interface.
We'll use the built-in spidev module in Python to send commands and read replies on the SPI bus.
- Analog vs. Digital - What is the difference, and why do we need to care about them for this example?
- Voltage Dividers - Explains how to set up, use, and calculate the values in a voltage divider
- Serial Peripheral Interface (SPI) - Read this if you would like to learn how SPI works on a low level
- Binary - We'll be working directly in binary in this section
Refer to the Raspberry Pi Pinout section in the previous example if you would like to see what pins and GPIO labels belong to each of these connections.
- Connect MOSI (GPIO10, pin 19) to Din on the MCP3002
- Connect MISO (GPIO9, pin 21) to Dout on the MCP3002
- Connect SCLK (GPIO11, pin 23) to CLK on the MCP3002
- Connect CE0 (GPIO8, pin 24) to CS/SHDN on the MCP3002
- Connect the photocell voltage divider to CH0 on the MCP3002
- Connect the potentiometer's middle pin to CH1 on the MCP3002
- Make the power (3.3 V) and ground (GND) connections as shown in the Fritzing diagram
Connecting through a Pi Wedge:
Connecting directly to the Raspberry Pi:
Code: Reading Analog Voltage
Depending on your version of Raspbian, you may or may not have to install the spidev package (e.g. Raspbian Lite does not come with some Python packages pre-installed). In a terminal, enter the following:
language:bash pip install spidev
In a new file, enter the following code:
language:python import time import spidev spi_ch = 0 # Enable SPI spi = spidev.SpiDev(0, spi_ch) spi.max_speed_hz = 1200000 def read_adc(adc_ch, vref = 3.3): # Make sure ADC channel is 0 or 1 if adc_ch != 0: adc_ch = 1 # Construct SPI message # First bit (Start): Logic high (1) # Second bit (SGL/DIFF): 1 to select single mode # Third bit (ODD/SIGN): Select channel (0 or 1) # Fourth bit (MSFB): 0 for LSB first # Next 12 bits: 0 (don't care) msg = 0b11 msg = ((msg << 1) + adc_ch) << 5 msg = [msg, 0b00000000] reply = spi.xfer2(msg) # Construct single integer out of the reply (2 bytes) adc = 0 for n in reply: adc = (adc << 8) + n # Last bit (0) is not part of ADC value, shift to remove it adc = adc >> 1 # Calculate voltage form ADC value voltage = (vref * adc) / 1024 return voltage # Report the channel 0 and channel 1 voltages to the terminal try: while True: adc_0 = read_adc(0) adc_1 = read_adc(1) print("Ch 0:", round(adc_0, 2), "V Ch 1:", round(adc_1, 2), "V") time.sleep(0.2) finally: spi.close() GPIO.cleanup()
Save the file (e.g. adc.py), and run it with Python:
language:bash python adc.py
You should be able to cover the photocell and see the Ch 0 voltage change. Adjust the knob on the potentiometer to see the Ch 1 voltage change.
Code to Note:
We are using SPI channel 0 on the Raspberry Pi when we initialize the SpiDev object:
language:python spi_ch = 0 spi = spidev.SpiDev(0, spi_ch)
Channel 0 corresponds to using CE0 (chip enable 0) on the Pi's pins. If you wanted to use another device on the SPI bus, you would need to connect it to CE1 and use SpiDev channel 1 as well.
We construct our SPI message by manipulating individual bits. We start with binary 11 (which is the decimal number 3) by using the prefix '0b':
language:python msg = 0b11
If you look at section 5 in the MCP3002 datasheet, you will see that we need to send a 1 to start transmission, followed by another 1 to denote that we want "single ended mode." After that, we select our channel with a 0 (for channel 0) or a 1 (for channel 1). Then, we send a 0 to show that we want data returned to us with the least significant bit (LSB) sent first.
language:python msg = ((msg << 1) + adc_ch) << 5
Finally, we send another twelve 0s. What we send here really doesn't matter, as we just need to send clock pulses to the MCP3002 so that it sends data back to us over the Dout (MISO) line. This data (4 setup bits followed by twelve 0s) is stored in a list.
language:python msg = [msg, 0b00000000]
We store the data returned to us in the
reply variable, and it comes to us as a list of 2 bytes (stored as 2 integers). Note that we send out data and read the reply at the same time when using SPI:
language:python reply = spi.xfer2(msg)
From there, we construct a single integer out of the two bytes (8 bits) by shifting the first over to the left by 8 bits and then adding the second byte to it. The last bit we read in is extraneous (not part of the ADC's return value) so we shift the answer to the right by one bit.
language:python adc = 0 for n in reply: adc = (adc << 8) + n adc = adc >> 1
The ADC value is given as a percentage of the maximum voltage (whatever the voltage is on the Vdd/Vref pin). That percentage is calculated by dividing the reply value by 1024. We get 1024 because we know that the MCP3002 is a 10-bit ADC, which means the maximum value of 10 bits (0b1111111111) is 1023. Many ADCs have some error, so we round up to 1024 to make the math easier (here's a discussion on max ADC values, if you're curious).
Once we get the percentage of Vref with val / 1024, we multiply that percentage by our Vref, which we know is 3.3V in the case of our Raspberry Pi.
language:python voltage = (vref * adc) / 1024
And that's how we get our analog voltage reading! If all this is confusing, you can simple copy the
Enable SPI portion and
read_adc() function into your own code. Then, just call
read_adc(0) to get the voltage at CH0 on the MCP3002.
One last interesting bit of code is the idea of default parameters. If you take a look at the
language:python def read_adc(adc_ch, vref = 3.3):
You'll see that there are actually two parameters:
vref. When you call this function, you are required to give it a channel number (0 or 1). However, you can optionally send it an argument with the Vref value. On most cases with the Raspberry Pi, the voltage will be 3.3V. If you use another voltage (e.g. 5V), then you can change the math so that the ADC gives you a more accurate reading.
You have the option of calling this function with another Vref (e.g. 5) using either
read_adc(0, 5) or by explicitly naming the
read_adc(0, vref=5). However, because we know that we've connected 3.3V to the MCP3002, we can simply call
read_adc(0) and know that the function will rely on its default parameter of
vref=3.3 when doing its calculations.
Challenge: Add an LED to your circuitry. Write a program to act as a variable nightlight. That is, the LED should turn on whenever the photocell sees dark (little ambient light) and should turn off whenever the photocell sees light (lots of ambient light). Have the potentiometer control the brightness of the LED when it is on. Hint: you might want to take some measurements to determine the threshold of light vs. dark. What is the voltage when you cover the photocell with your hand?