SPI Protocol
The SPI protocol, or serial peripheral interface, is another example of a serial protocol for two devices to send and receive data. The big difference between SPI and I2C is that SPI uses a few more wires, in particular an explicit data input and data output wire instead of sharing a single data wire like with I2C. There’s also a clock wire like in I2C, but with SPI it has the freedom to use almost any speed it desires from a few kilohertz up to hundreds of megahertz (if the hardware supports it!). This makes the SPI protocol great for devices like TFT displays that need to be sent very large amounts of data–with control over the clock speed it’s possible to very quickly send entire screen images to the display.
Compared to I2C the SPI protocol has some interesting properties:
- SPI uses 3 to 4 wires for sending and receiving data. One wire is a clock line that toggles up and down to drive bits being sent and received. Like with I2C only the main device can drive the clock. Another wire is MOSI, or ‘main output, secondary input’ which is the data output from your board and sent to a connected device. Likewise a MISO wire, or ‘main input, secondary output’, is for sending data from the device to the board receiving it. Finally most chips have a CS, or chip select, wire which is toggled to tell the chip that it should listen and respond to requests on the SPI bus.
- Like I2C multiple devices can share the same SPI bus, however a big difference is that each device typically requires its own unique CS line. Remember the CS/chip select line is what tells a chip that it should listen for SPI traffic. As a result for each SPI device you connect to your board it can share the clock, MOSI, MISO lines but must have its own CS line (typically connected to any free digital I/O pin).
- SPI devices have different requirements for speed (sometimes called baudrate), polarity, and phase. The SPI page on Wikipedia has a good description of what polarity and phase mean–they control how the data is sent and received over the MISO and MOSI lines. Different polarity values control if a digital high or low logic level means a bit is a one or zero. Similarly different phase values control when data is read and written by devices–either with the rising or falling edge of the clock line. The important thing to know about phase and polarity is that each device has its own requirement for setting them so be sure to check your device’s datasheet. Many devices are ‘mode 0’ which means a polarity and phase of 0 but watch out because some devices use different modes.
- Like with I2C the basic operations are reading and writing bits and bytes of data over the data lines. However unlike I2C there is no guarantee or check that a connected device received or sent data successfully. Sometimes chips have extra lines to watch for an acknowledgment, but sometimes they don’t and the SPI requests are ‘fire and forget’ with no guarantee they were received.
MAX31855 SPI Thermocouple Temperature Sensor
To demonstrate interacting with a SPI device this guide will show you how to query the temperature from a MAX31855 thermocouple temperature sensor. This sensor is a good example of an SPI device because it has a very simple interface, you just connect a MISO line and read bytes of temperature data. There are no registers or other complex structures to configure and process on the chip. You’ll need these parts to follow this section:
- MAX31855 thermocouple temperature sensor.
- If you don’t have one a simple K-type thermocouple is also required to connect to the MAX31855.
- A breadboard and wires to connect the components and board together.
Connect the components together as follows:
- Board 5V or 3.3V output to MAX31855 VIN.
- Board ground/GND to MAX31855 GND.
- Board SCK (SPI clock line) to MAX31855 CLK/clock. Note this is on the small 2x3 header on a Metro M0 Express or other Arduino form-factor boards.
- Board MISO to MAX31855 DO (data output, AKA MISO). Note this is also on the small 2x3 header on a Metro M0 Express or other Arduino form-factor board.
- Board D2 (or any free digital I/O pin) to MAX31855 CS/chip select.
The wiring above will configure hardware-based SPI communication. Like with I2C you can choose to use your microprocessor’s built-in SPI communication hardware, or you might use software ‘bit banging’ to talk SPI much more slowly over any digital I/O lines. You’ll see how to switch to software SPI further in this guide.
Once the board is wired up connect to the REPL. You’ll need to import the board
, busio
, and digitalio
modules:
>>> import board >>> import busio >>> import digitalio
Remember the CS line is just a simple digital I/O line so we need to use the digitalio module to control it. Let’s setup a digital output to drive this line:
>>> cs = digitalio.DigitalInOut(board.D2) >>> cs.direction = digitalio.Direction.OUTPUT >>> cs.value = True
For most chips they expect the CS line to be held high when they aren’t in use and then pulled low when the processor is talking to them. However check your device’s datasheet as the polarity and phase (or mode) can change how the chip expects CS to work! In this case the MAX31855 expects CS to be high when not in use and pulled low when talking to it. We’ll start the CS line in a high or true value so that it isn’t yet listening.
Now we need to create an interface to the SPI hardware bus. Do so with this line to create an instance of the busio.SPI
class:
>>> spi = busio.SPI(board.SCK, MISO=board.MISO)
To create the SPI class you must pass at least a clock pin and then optionally the MISO and MOSI pins. In this case the MAX31855 doesn’t use the MOSI pin so we only provide MISO.
Now we’re almost ready to read data from the sensor. However just like with I2C you must lock the SPI bus before you send and receive data. The busio.SPI.try_lock()
and busio.SPI.unlock()
functions can do this like with I2C. Let’s read 4 bytes of data from the chip:
>>> while not spi.try_lock(): ... pass ... >>> spi.configure(baudrate=5000000, phase=0, polarity=0) >>> cs.value = False >>> result = bytearray(4) >>> spi.readinto(result) >>> cs.value = True >>> result bytearray(b'\x01\xa8\x1a\xf0')
Before digging into the results let’s break down what just happened:
- The while loop at the start will attempt to lock the SPI bus so your code can access SPI devices. Just like with I2C you need to call try_lock (and later unlock) to ensure you are the only user of the SPI bus.
- The
busio.SPI.configure()
function is called to configure the speed, phase, and polarity of the SPI bus. It’s important to always call configure after locking the bus and before talking to your device as communication with other devices might have changed the speed, polarity, etc. You’ll need to look up the exact speed and other values from your device’s datasheet. For the MAX31855 we’ll use a speed of 5mhz and a polarity and phase of 0 (sometimes called mode 0). - Next we toggle the CS line down to a low logic level. Remember with SPI each device needs a chip select line to tell it when it’s ready to send and receive data.
- A 4 byte buffer is created to hold the result of the SPI read. Just like with I2C reads you need to pass a buffer that will be filled with response data, and the size of the buffer determines how many bytes are read.
- The
busio.SPI.readinto()
function is called to read 4 bytes of data from the MAX31855. Remember the size of the passed in buffer determines how many bytes of data are read. - Finally the CS line is toggled back to a high digital logic level. This tells the MAX31855 we’re done talking to it and it can stop listening or sending data.
Notice the returned data has the hex value 0x01A81AF0. Just like with the MCP9808 you’ll need to check your device’s datasheet to see how to interpret the data. In this case you can again make a little Python function to convert the raw bytes into temperature data:
>>> def temp_c(data): ... temp = data[0] << 8 | data[1] ... if temp & 0x0001: ... return float('NaN') # Fault reading data. ... temp >>= 2 ... if temp & 0x2000: ... temp -= 16384 # Sign bit set, take 2's compliment. ... return temp * 0.25 ... >>> temp_c(result) 26.5
Awesome, a value of 26.5 degrees Celsius was read from the sensor! Try touching the thermocouple with your finger and running the SPI read code again to get another temperature reading to compare. Remember to toggle the CS pin low and then back high in between reading SPI data!
Although the MAX31855 doesn’t require it and it’s not shown above, you can also use the busio.SPI.write()
function to send data over the MOSI line. For example to send the bytes 0x01, 0xFF you would run:
>>> spi.configure(baudrate=5000000, phase=0, polarity=0) >>> cs.value = False >>> spi.write(bytes([0x01, 0xFF])) >>> cs.value = True
Just like with reading data you want to make sure the bus is configured for the right speed, phase, and polarity. Then you toggle the CS line low to tell the device you’re about to talk to it, send data with the busio.SPI.write()
function, and toggle the CS line back high again.
Finally, don’t forget to call busio.SPI.unlock()
to unlock the SPI bus and let other code use it:
>>> spi.unlock()
Again you might want to put this all in a try-finally block to make sure unlock is always called, even if something fails and throws an exception. Here’s an example of a complete sensor read with the try-finally syntax:
>>> while not spi.try_lock(): ... pass ... >>> try: ... spi.configure(baudrate=5000000, phase=0, polarity=0) ... cs.value = False ... result = bytearray(4) ... spi.readinto(result) ... cs.value = True ... finally: ... spi.unlock() ... >>> result bytearray(b'\x01\xa8\x1a\xf0')
That’s all there is to the basics of reading and writing data with the SPI protocol and the built-in SPI APIs of CircuitPython. However just like with I2C there’s a handy SPIDevice library that can simplify talking to SPI devices.
SPIDevice Library
You saw above how to interact with a SPI device using the API built-in to CircuitPython. Remember using the built-in API requires careful management of the lock and unlock functions to access the SPI bus, and explicit manipulation of the chip select line for a device. If you’re writing code to talk to a SPI device you might find using the CircuitPython bus device library a bit easier to manage as it controls locking & unlocking, and the chip select line automatically (using Python’s context manager with statement).
To use the bus device library you’ll first need to install the library on your board.
First make sure you are running the latest version of Adafruit CircuitPython for your board.
Next you'll need to install the necessary libraries to use the hardware--carefully follow the steps to find and install these libraries from Adafruit's CircuitPython library bundle. Our introduction guide has a great page on how to install the library bundle for both express and non-express boards.
For Express boards, install the entire bundle. For non-express boards, with limited space, you'll need to grab just the adafruit_bus_device folder from inside lib to the lib folder of your board’s CIRCUITPY drive.
Once you have the bus device library installed you can use the SPIDevice class to simplify access to a device on the SPI bus. First setup the SPI bus and CS line exactly as you did before:
>>> import board >>> import busio >>> import digitalio >>> spi = busio.SPI(board.SCK, MISO=board.MISO) >>> cs = digitalio.DigitalInOut(board.D2)
Now import the bus device module and create an instance of the SPIDevice class. Notice the SPIDevice class needs to be told the SPI bus, chip select line, baudrate, polarity, and phase of the SPI connection. These details will be remembered by the SPIDevice class so it can automatically lock and configure the bus appropriately (again using Python’s with statement and a context manager):
>>> from adafruit_bus_device.spi_device import SPIDevice >>> device = SPIDevice(spi, cs, baudrate=5000000, polarity=0, phase=0)
Now you’re ready to interact with the SPI device instance using the same read and write functions as before. However this time you’ll put your code in a with statement context manager and it will automatically lock the bus, assert the CS line, configure the SPI bus, and unlock the bus when done:
>>> with device: ... result = bytearray(4) ... spi.readinto(result) ... >>> result bytearray(b'\x01\xa8\x1a\xf0') >>> temp_c(result) 26.5
Notice you didn’t need to call configure or even change the CS line from high to low and back. The SPIDevice class takes care of all these details for you automatically!
You can even call the write function just like on the SPI bus directly and data will be written out the MOSI line. One important thing to note is that the CS line is asserted for the entire with statement block, so if you need to make two different transactions be sure to put them in their own with statement blocks. Another thing to note with the SPI device class is that it currently only supports devices with a chip select (it is not optional) and whose chip select is asserted with a low logic signal. Devices asserted with a high logic level are rare and uncommon so the SPI device class should cover most needs.
Just like with I2C you typically don’t need to go straight to these low-level SPI protocol requests (using built-in APIs or the SPIDevice class), instead look for a library to interface with your hardware. In this case the CircuitPython MAX31855 library is exactly what you want to use to talk to this thermocouple sensor. Using a library simplifies access to the sensor data and saves you from writing all the complex SPI transaction code. However if your device doesn’t have a library you might need to interface with it directly using code like the above!
Software SPI & I2C
As mentioned above there are some cases where using the hardware’s SPI (or even I2C) support isn’t possible. Perhaps you have so many devices you’ve exceeded the available pins or resources, or maybe the hardware bus pins aren’t accessible. In these cases you can fall back to a software-driven, or sometimes called ‘bit-banged’, approach to driving the SPI protocol. This approach uses simple digital I/O lines to read and write SPI protocol data. The big difference between hardware and software SPI is speed–with software SPI it will run much slower than hardware SPI because toggling digital I/O is slower than dedicated hardware SPI. However in many cases like reading this temperature sensor the speed of the bus doesn’t matter and you can use software SPI.
To try software SPI re-wire the MAX31855 as follows:
- Board 5V or 3.3V output to MAX31855 VIN.
- Board ground/GND to MAX31855 GND.
- Board D4 to MAX31855 CLK/clock.
- Board D3 to MAX31855 DO (data output, AKA MISO).
- Board D2 to MAX31855 CS/chip select.
Notice all of the SPI lines are connected to digital I/O lines. You can actually change these to any other digital I/O lines (but you’ll need to modify the code to match!).
Now import and configure the CS line exactly as before:
>>> import board >>> import digitalio >>> cs = digitalio.DigitalInOut(board.D2) >>> cs.direction = digitalio.Direction.OUTPUT >>> cs.value = True
At this point you’re ready to configure the software SPI bus by using the bitbangio
module. The bitbangio
modules provides all of the software-based protocol support, like SPI and I2C. Luckily the interface to the bitbangio classes is exactly the same as for the busio hardware-based interfaces so your code doesn’t change much beyond what library it imports and how it creates the SPI class:
>>> import bitbangio >>> spi = bitbangio.SPI(board.D4, MISO=board.D3)
Just like with the busio SPI class the bitbangio.SPI
class is created and told the clock line and MISO line (it can also optionally be told the MOSI line). Notice all these lines are just simple digital I/O pins that you wired above.
Now using the software-based SPI bus is exactly the same as with hardware like above! Try the same code:
>>> while not spi.try_lock(): ... pass ... >>> spi.configure(baudrate=5000000, phase=0, polarity=0) >>> cs.value = False >>> result = bytearray(4) >>> spi.readinto(result) >>> cs.value = True >>> result bytearray(b'\x01\xa8\x1a\xf0')
Awesome you received 4 bytes of temperature data just like with hardware SPI! The big difference here is that code in CircuitPython is driving the digital pins to run the SPI protocol instead of hardware built-in to the microprocessor. This means the call to read data is a little slower (it’s certainly not running at 5mhz like requested) but for most devices they don’t care about the slower speed.
Again remember to call bitbangio.SPI.unlock()
to unlock the software SPI bus!
>>> spi.unlock()
Don’t forget you can even use the SPIDevice library with the bit-bang I2C bus!
You can do the exact same software ‘bit-bang’ trick with the I2C protocol too (even using the I2CDevice class). Use the bitbangio.I2C
class from the bitbangio
module in place of the busio.I2C
class. The interface between the two classes is the same so you just change how you import and create the I2C interface, for example:
>>> import board >>> import bitbangio >>> i2c = bitbangio.I2C(board.D3, board.D2)
The above would create a software I2C interface using D3 as the clock and D2 as the data line. This is handy for adding more I2C peripherals or using pins other than SCL and SDA. Again the speed is slower, but most devices don’t care about speed. Also note on some boards like the ESP8266 software I2C is required!
Page last edited March 08, 2024
Text editor powered by tinymce.