Skip to content

Arduino - Serial Peripheral Interface

Arduino - Serial Peripheral Interface (SPI)

Section titled “Arduino - Serial Peripheral Interface (SPI)”

The Serial Peripheral Interface (SPI) is a synchronous serial communication protocol used for short-distance communication, primarily in embedded systems. It’s known for being faster than I2C and supporting full-duplex (simultaneous send and receive) communication.

SPI communication involves a Master device that controls the communication and one or more Slave devices that respond to the master. It operates using a set of four signal lines.

SignalAlternative NameDescription
SCKSCLKSerial Clock - The clock signal sent by the Master to synchronize data transfer.
MOSICOPIMaster Out, Slave In - The data line for sending data from the Master to the Slave.
MISOCIPOMaster In, Slave Out - The data line for sending data from the Slave to the Master.
SSCSSlave Select - A line controlled by the Master to select which Slave it wants to communicate with. Each slave needs a dedicated SS line.

The SPI pins are located on specific pins for different Arduino boards. On an Arduino UNO, they are on the ICSP header and also mirrored on digital pins 10-13. Here is the standard wiring for connecting two Arduino UNOs:

  • Common Ground: Connect a GND pin on the Master to a GND pin on the Slave. This is essential!
  • SCK: Master Pin 13 → Slave Pin 13
  • MOSI: Master Pin 11 → Slave Pin 11
  • MISO: Master Pin 12 → Slave Pin 12
  • SS: Master Pin 10 → Slave Pin 10

Note: For other boards like the Mega or MKR series, consult their official pinout diagrams to locate the SPI pins.

The modern approach to SPI communication uses a ‘transaction’. This ensures that the SPI settings (like clock speed and data mode) are correctly configured for a specific device before communication begins and are not interfered with by other devices on the same bus. This is the best practice.

  • SPI.beginTransaction(SPISettings(speed, dataOrder, dataMode)): Begins an SPI transaction. SPISettings is where you configure the communication parameters.
  • speed: The clock speed in Hz (e.g., 4000000 for 4MHz).
  • dataOrder: MSBFIRST (Most Significant Bit first) or LSBFIRST.
  • dataMode: SPI_MODE0, SPI_MODE1, SPI_MODE2, or SPI_MODE3. This defines the clock polarity and phase. The device’s datasheet will specify which mode to use.
  • SPI.transfer(data): Sends a byte of data and simultaneously receives a byte back, which is the return value of the function.
  • SPI.endTransaction(): Releases the SPI bus for other devices.

In this example, the Master will send a command character to the Slave. The Slave will process the command and prepare a response. The Master will then read this response.

/*
SPI Master Example - Modernized
This sketch sends a command to an SPI slave and reads the response.
*/
#include <SPI.h>
const int slaveSelectPin = 10;
// Define the settings for our SPI communication
SPISettings slaveSettings(4000000, MSBFIRST, SPI_MODE0);
void setup() {
Serial.begin(115200);
while (!Serial);
// Set the Slave Select pin as an output
pinMode(slaveSelectPin, OUTPUT);
digitalWrite(slaveSelectPin, HIGH); // Deselect slave initially
// Initialize SPI
SPI.begin();
Serial.println("Master Initialized");
}
void loop() {
// Send command 'a' and get a response
char response = sendCommand('a');
Serial.print("Received from slave: ");
Serial.println(response);
delay(1000);
// Send command 'b' and get a response
response = sendCommand('b');
Serial.print("Received from slave: ");
Serial.println(response);
delay(1000);
}
char sendCommand(char command) {
// Start the transaction with the slave's settings
SPI.beginTransaction(slaveSettings);
// Select the slave
digitalWrite(slaveSelectPin, LOW);
// Send the command and receive a byte at the same time
char result = SPI.transfer(command);
// Deselect the slave
digitalWrite(slaveSelectPin, HIGH);
// End the transaction
SPI.endTransaction();
return result;
}
/*
SPI Slave Example - Modernized
This sketch listens for commands from an SPI master and replies.
It uses an interrupt to handle incoming data efficiently.
*/
#include <SPI.h>
volatile byte receivedData;
volatile byte dataToSend;
volatile bool newData = false;
void setup() {
Serial.begin(115200);
while (!Serial);
// Set MISO as an output
pinMode(MISO, OUTPUT);
// Configure SPI for Slave mode
SPCR |= _BV(SPE); // Enable SPI
SPCR |= _BV(SPIE); // Enable SPI Interrupt
receivedData = 0;
dataToSend = 'x'; // Default initial response
Serial.println("Slave Initialized");
}
// SPI Interrupt Service Routine
ISR(SPI_STC_vect) {
receivedData = SPDR; // Read received data
SPDR = dataToSend; // Pre-load the data to be sent next time
newData = true; // Set flag for the main loop
}
void loop() {
if (newData) {
newData = false; // Clear the flag
Serial.print("Received command: ");
Serial.println((char)receivedData);
// Process the command and prepare the next response
if (receivedData == 'a') {
dataToSend = 'A'; // Prepare 'A' as the response for the next transfer
} else if (receivedData == 'b') {
dataToSend = 'B'; // Prepare 'B'
} else {
dataToSend = '?'; // Unknown command
}
}
}
  • No Communication: The most common issue is wiring. Double-check all connections: MOSI-to-MOSI, MISO-to-MISO, SCK-to-SCK, and especially the common ground.
  • Garbage Data: If you receive incorrect data, there’s likely a mismatch in SPI settings. Ensure both Master and Slave are using the same dataMode and dataOrder.
  • Multi-Slave Issues: When using multiple slaves, ensure only one slave is selected (its SS pin is LOW) at any given time. All others must be deselected (SS pin HIGH).
  • The Best Tool: For complex SPI issues, a logic analyzer is invaluable. It allows you to see the actual signals on the wires and quickly diagnose timing, mode, or data problems.