Intro to Arduino: SPI Serial Communication

< All Topics

Serial Peripheral Interface, or SPI, was developed in the late 1980’s and was quickly adopted as the standard communication protocol for embedded systems. SPI is intended for communicating over short distances at relatively high speeds. The protocol allows one master per bus and many slave devices. The master differentiates between the slaves by activating the appropriate slave device with a slave select (SS) or chip select (CS) line. The SS line is active low and usually, has a pull-up to ensure that the line returns high in an idle state. In addition to the SS line, SPI uses three lines for communication; Master In Slave Out (MISO), Master Out Slave In (MOSI), and the serial clock line (SCK). SPI operates in a synchronous, full-duplex mode. This means that data travels in both directions at the same time on every clock pulse. The master runs the serial clock (SCK) line and with every pulse, the master sends and receives a bit and the slave sends and receives a bit.

Hardware

All of the devices on an SPI bus share the MOSI, MISO, and SCK lines. In addition to these three lines, each slave also has its own slave select line connected directly to the master. This means that SPI requires 3 + N wires, where N is the number of slaves on the bus. The communication lines are driven high and low by the devices, while the SS lines require pull-ups to ensure that they return to the inactive state. A 4.7k to 10k resistor from each SS to Vcc should be adequate.

In the Arduino IDE

To begin, you must include the SPI library in your sketch. Go to the Sketch Menu -> Include Library -> SPI.

Initialization

To initialize the SPI library, use the SPI.begin() function. This must be done before using any other SPI functions; usually in setup(). The SPI.begin() function does not require any arguments. The default SPI configuration is a baud rate of 4MHz in Mode 0.

SPI.begin(); // Initialize SPI - 4000000 Baud, MODE0 (default)

Transmitting

When we use SPI, it is a pretty manual operation. We have to activate the chip select (CS) line to activate the slave that we want to communicate with, then we can transmit data. We can send as many bytes as we want. Finally, we release the CS line to end the transmission and deactivate the slave.

const int CS_Pin = 7;

pinMode(CS_Pin, OUTPUT); // The CS_Pin should remain in a high impedance state (INPUT) when it is not in use.
digitalWrite(CS_PIN, LOW); // Activate the CS line (CS is active LOW)

SPI.transfer(36); // Send first byte // DEC 36
SPI.transfer(0x12); // Send second byte // HEX 0x12 = DEC 18
SPI.transfer(0b01101101); // Send third byte // BIN 0b01101101 = DEC 109

pinMode(CS_Pin, INPUT); // Set CS_Pin to high impedance to allow pull-up to reset CS to inactive.
digitalWrite(CS_Pin, HIGH); // Enable internal pull-up

Receiving

When communicating over SPI, data does not move unless we are transmitting. Because of this, the master must transmit “dummy” data when the slave is ready to transmit to the master.

const int CS_Pin = 7;
byte firstByte, secondByte;

pinMode(CS_Pin, OUTPUT); // The CS_Pin should remain in a high impedance state (INPUT) when it is not in use.
digitalWrite(CS_PIN, LOW); // Activate the CS line (CS is active LOW)

SPI.transfer(0x42); // In this case, 0x42 commands the slave to transmit 2 bytes of data
firstByte = SPI.transfer(0x00); // Send dummy data to receive first byte
secondByte = SPI.transfer(0x00); // Send dummy data to receive second byte

pinMode(CS_Pin, INPUT); // Set CS_Pin to high impedance to allow pull-up to reset CS to inactive
digitalWrite(CS_Pin, HIGH); // Enable internal pull-up

Transactions with a Unique SPI Configuration

Sometimes we need to use different SPI setups for different devices. This can happen when one device uses SPI_MODE0, while another device uses SPI_MODE3. Or perhaps one device is slow and can only operate at 1MHz, while a memory chip on the same bus will operate at 12MHz. We might not want to limit all of our transactions to the 1MHz limit because the amount of data to and from the memory chip might be very large. In these cases, we can run each transaction with its own setup using the SPI.beginTransaction() and SPI.endTransaction commands.

The SPI.beginTransaction() is called immediately before an SPI transaction. The argument of SPI.transaction is the output of SPISettings function. So SPISettings is usually called with the SPI.beginTransaction() function. SPISettings() requires the same arguments as SPI.begin: baud rate, bit order (MSBFIRST or LSBFIRST), and SPI mode (SPI_MODE0, SPI_MODE1, SPI_MODE2, or SPI_MODE3).

SPI.beginTransaction(SPISettings(12000000, MSBFIRST, SPIMODE3)); // Example implimentation

After calling SPI.beginTransaction, the SPI communication is performed and usual and the transaction is closed by calling SPI.endTransaction. Below is an example of two SPI transactions with different settings.

// First Transaction at 1MHz and SPI mode 0
SPI.beginTransaction( SPISettings(1000000, MSBFIRST, SPI_MODE0) );
SPI.transfer(0x24);
SPI.transfer(0x12);
SPI.endTransmission();

// Second Transaction at 12MHz and SPI mode 3
SPI.beginTransmission( SPISettings(12000000, LSBFIRST, SPI_MODE3) );
SPI.transfer(0x42);  // Initaite transfer from slave
firstByte = SPI.transfer(0x00); // Receive first byte
secondByte = SPI.transfer(0x00); // Receive second byte
SPI.endTransaction();

Protocol

SPI Modes

There are only two parameters that change in the SPI protocol. The first is clock line polarity (CPOL); is the clock active high or active low? Similarly, the second is the clock phase (CPHA); is data captured (read) when the clock line goes from low to high or from high to low? This results in four distinct SPI modes.

Mode 0

The clock is active high and data is captured on the rising edge of the clock. (CPOL = 0, CPHA = 0)

Mode 1

The clock is active high and data is captured on the falling edge of the clock. (CPOL = 0, CPHA = 1)

Mode 2

The clock is active low and data is captured on the rising edge of the clock. (CPOL = 1, CPHA = 0)

Mode 3

The clock is active low and data is captured on the falling edge of the clock. (CPOL = 1, CPHA = 1)

Clock Speed

The speed of the SPI protocol will usually be limited by the capabilities of the devices on the SPI bus. You will need to check the specifications of your devices to find the maximum speed that they can all handle.

Data Transfer

SPI operates in full duplex mode, meaning that data is always being sent and received. While this can result in very fast, bi-directional data transfer, it is rare to have useful data going in both directions at the same time.

Transmitting Data

When the master sends a byte of data, the clock is pulsed eight times and the MOSI line is driven to the appropriate state for each clock pulse.

Receiving data

Even when a slave is ready to transmit data to the master, it is powerless to do so without the help of the master. In order for the slave to send data to the master, the master MUST be sending data to the slave, so that the clock is running (SCK). This is where we use dummy data. When the slave is ready to transmit a byte of data, the master transmits a byte of worthless (dummy) data to the slave (MOSI). And as each bit of dummy data is sent to the slave, the slave clocks out a bit of real data for the master (MISO).

Example

#include <SPI.h>

// using two incompatible SPI devices, A and B. Incompatible means that they need different SPI_MODE
const int slaveAPin = 20;
const int slaveBPin = 21;

// set up the speed, data order and data mode
SPISettings settingsA(2000000, MSBFIRST, SPI_MODE1); 
SPISettings settingsB(16000000, LSBFIRST, SPI_MODE3); 

void setup() {
  // set the Slave Select Pins as outputs:
  pinMode (slaveAPin, OUTPUT);
  pinMode (slaveBPin, OUTPUT);
  // initialize SPI:
  SPI.begin(); 
}

uint8_t stat, val1, val2, result;

void loop() {
  // read three bytes from device A
  SPI.beginTransaction(settingsA);
  digitalWrite (slaveAPin, LOW);
  // reading only, so data sent does not matter
  stat = SPI.transfer(0);
  val1 = SPI.transfer(0);
  val2 = SPI.transfer(0);
  digitalWrite (slaveAPin, HIGH);
  SPI.endTransaction();
  // if stat is 1 or 2, send val1 or val2 else zero
  if (stat == 1) { 
   result = val1;
  } else if (stat == 2) { 
   result = val2;
  } else {
   result = 0;
  }
  // send result to device B
  SPI.beginTransaction(settingsB);
  digitalWrite (slaveBPin, LOW);
  SPI.transfer(result);
  digitalWrite (slaveBPin, HIGH);
  SPI.endTransaction();
}

To learn more about Arduino SPI, visit the Arduino SPI Library Reference page.

Previous Intro to Arduino: UART Serial Communication
Next Intro to Arduino: Installing a Library
Table of Contents