Arduino SPI communication – Serial Peripheral Interface

From the SPI bus

SPI (Serial Peripheral Interface) is a synchronous serial data transfer protocol used by microcontrollers to communicate quickly with each other or with peripheral devices over short distances.

The SPI bus is a high-speed one-way communication line, so two-way communication can happen simultaneously, so devices can send and receive data at the same time.

UNO R3 board ATmega 328 with usb cable
UNO R3 board ATmega 328 with usb cable from amazon(Ad)

In case of SPI data transfer, there is always a controller device, usually a microcontroller, which supervises the communication and controls the peripheral devices. Previously, the control device was known as “Master” while the peripheral devices were known as “Slave”, Arduino no longer supports the use of this terminology. More recently, they have been named “Controller” and “Peripheral”.

Let’s take a look at the wires of the SPI bus

Arduino UNO - Spi bus connector pins

“COPI” (Controller Out Peripheral In), the line used to send data from the Controller to peripherals, was previously called “MOSI” (Master Out Slave In).

“CIPO” (Controller In, Peripheral Out), the line for transferring data from the Peripheral to the controller, it used to be “MISO” (Master In Slave Out).

“SCK” Serial Clock (SCK), the clock pulses generated by the Controller that synchronize data transfer, the name of the serial clock has not changed.

“CS” (Chip Select pin), each peripheral has a line that is used by the Controller to enable and disable peripheral devices. By default, this pin is high. When the “CS” pin of a Peripheral goes low, it communicates with the controller. Formerly known as “SS” (Slave Select).

A simple SPI bus between a Controller device and a Peripheral device

Generally, there are three common lines on the SPI bus between the Controller device and the Pheriperal devices, and from the Controller side, a line is connected to each Pheriperal device, with which the controller selects which peripheral it wants to communicate with.

During the communication, the Controller keeps the CS line at a low signal level. The Controller sends data via the COPI (MOSI) line, and sends the clock signal used for communication via the SCK line. If the Controller (Master) device is waiting for a response from the Peripheral (Slave) device, it will continue to send clock signals until data is received on the CIPO (MISO) pin. This is the independent Slave/Peripheral configuration.

SPI bus - independent Slave/Peripheral configuration.

In a Daisy Chain configuration, the Controller device only needs one CS pin to communicate with all Peripheral devices. The Controller pulls the CS line low to initiate communication, signaling all peripherals to prepare to receive data on the COPI pins. The Controller keeps the CS line low for the duration of the communication.

The Controller device then sends data on the COPI pin to the first Peripheral device in the chain. The Controller sends the clock signal used for communication on the SCK line. The data sent from the Controller then flows from Peripheral device to Peripheral device. The Controller device must generate enough clock signals for the data to reach the last Peripheral device in the chain.

When waiting for a response from a Peripheral device, the Controller device continues to clock until all data is received on the CIPO pin.

SPI bus - Daisy Chain configuration

The SPI library does not need to be installed separately, it is part of the arduino IDE. Just paste it at the beginning of the sketch.

#include <SPI.h>

SPISettings

SPISettings is used to configure the SPI port of the SPI device. The SPISettings object has 3 parameters, these are the communication speed; the data order; and clock phase and polarity.

The first parameter is the transmission speed of the SPI, it must be specified in Hertz. Allows you to set the SPI baud rate used by the SPI class. Its value can be 4, 8, 16, 32, 64, 128 or 256 MHz for most Arduinos.

The second parameter, the bit order, determines which bit the transfer starts with (LSBFIRST or MSBFIRST).

The third parameter is one of four transmission modes: SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3

These transfer modes determine whether the data is stepped on the rising or falling edge of the clock (clock phase) and when the clock is idle, high or low (this is clock polarity). 

The polarity and phase of the clock signal according to the four transmission modes can be seen in the table:

ModeClock Polarity (CPOL)Clock Phase (CPHA)Output EdgeData Capture
SPI_MODE000falling edgerising edge
SPI_MODE101rising edgefalling edge
SPI_MODE210rising edgefalling edge
SPI_MODE311falling edgerising edge

CPOL determines the polarity of the clock signal.

CPOL=0 is the clock signal, which defaults to 0 and all pulses are 1. So the leading edge is a rising edge. If CPOL=1, the clock is idle at 1 and each cycle consists of 0 pulses. This means that the leading edge is a trailing edge.

The CPHA determines the timing (ie phase) of the data bits with respect to the clock pulses.

For CPHA=0, the output side switches the data on the falling edge of the previous clock cycle, while the receiving side captures the data on the rising edge of the clock cycle (or shortly after). On the output side, the data is valid until the trailing edge of the current clock cycle. In the first cycle, the first bit must be on the COPI line before the lead-in clock.

With CPHA=1, the output side steps data on the rising edge of the current clock cycle, while the receiving side latches data on (or shortly after) the falling edge of the clock cycle. On the output side, the data is valid until the rising edge of the next clock cycle. In the last cycle, the Peripheral device holds the CIPO queue until the Peripheral is deselected.

SPI bus clock phase and polarity

The SPISettings object must be used as a parameter to SPI.beginTransaction() .

Back to table of contents.

begin()

begin() initializes the SPI bus, this must be done in setup() before any other SPI functions are used. The begin() function has no return value.

void setup()
{
  SPI.begin();

  .....
}

Back to table of contents.

beginTransaction()

The beginTransaction() function initializes the SPI bus using the SPISettings object.

The beginTransaction() function is usually called before SPI communication to set the communication parameters. These parameters include SPI bus speed, bit order, and communication modes.

Using a constant value as a parameter results in smaller and faster code.

SPI.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));

If any of the parameters are variable, we can create an instance of the SPISettings object to hold the variables for the 3 settings.

We can then pass the object instance to the SPI.beginTransaction() function. This method may be more efficient if the settings are not constant.

SPISettings mySetting(speedMaximum, dataOrder, dataMode);

SPI.beginTransaction(mySettings);

The beginTransaction() function has no return value.

Back to table of contents.

endTransaction()

To end SPI communication, use the SPI.endTransaction() function in your Arduino code. This function closes the current SPI transaction and frees the SPI hardware for new transactions.

Using the SPI.endTransaction() function is optional, as SPI communication is automatically terminated when the Arduino code is executed. However, if the SPI communication takes a long time, or if an error occurs during the SPI communication, it is recommended to use the SPI.endTransaction() function to end the communication and release the hardware.

It has no return value.

SPI.endTransaction();

Back to table of contents.

end()

The SPI.end() function disables the SPI bus (leaving the pin modes unchanged). It has no return value. The Controller can no longer communicate with that device until it re-initializes the SPI communication.

SPI.end();

Back to table of contents.

setBitOrder()

This feature is deprecated! In new projects, we use the SPI.beginTransaction() function with the SPISettings object instead to configure the parameters.

In the Arduino SPI library, this setBitOrder() function determines which bit to start the transfer with. Its parameter can be LSBFIRST or MSBFIRST. The setBitOrder() function has no return value.

SPI.setBitOrder(order);

Back to table of contents.

setClockDivider()

This feature is deprecated! In new projects, we use the SPI.beginTransaction() function with the SPISettings object instead to configure the parameters.

The SPI.setClockDivider() function can be used to set the SPI communication speed.

The SPI.setClockDivider() function has a single parameter that sets the SPI clock divider relative to the system clock. Available dividers on AVR based cards are 2, 4, 8, 16, 32, 64 or 128.

The default setting is SPI_CLOCK_DIV4, which sets the SPI clock to one quarter of the system clock frequency. So, for a motherboard with a 16MHz clock, the SPI bus clock will be 4 MHz.

SPI.setClockDivider(divider);

The parameters of the function can be:

SPI_CLOCK_DIV2
SPI_CLOCK_DIV4
SPI_CLOCK_DIV8
SPI_CLOCK_DIV16
SPI_CLOCK_DIV32
SPI_CLOCK_DIV64
SPI_CLOCK_DIV128

The setClockDivider() function has no return value.

Back to table of contents.

setDataMode()

This feature is deprecated! In new projects, we use the SPI.beginTransaction() function with the SPISettings object instead to configure the parameters.

The setDateMode() function sets the SPI data mode, clock polarity and phase. Its parameter is one of four transmission modes: SPI_MODE0, SPI_MODE1, SPI_MODE2, SPI_MODE3

SPI.setDataMode(mode);

Back to table of contents.

transfer()

Using the SPI.transfer() function, the Controller (master) can send data to the Peripheral (slave) device and receive data from the Peripheral device.

Using the SPI.transfer() function is very simple. The function sends or receives a single byte on the SPI bus. The function syntax is as follows:

byte SPI.transfer(byte data);

The function’s only input parameter is the data to send, and it returns a byte containing the received data.

// Send data to Peripheral device
SPI.transfer(byte data);

// Receive data from Peripheral device
byte response = SPI.transfer(byte data);

In case of SPI.transfer(buffer, size) transfer, the received data is stored locally in the buffer. During the SPI transaction, the data in the buffer is overwritten with the received data, so the sent data is replaced by the received data.

The function expects two parameters, the buffer, in which the data is stored, and the size, which specifies the size of the data.

// Sending data on the SPI bus
byte buffer[] = {0x01, 0x02, 0x03, 0x04};
int size = sizeof(buffer);
SPI.transfer(buffer, size);
  
// Receiving data on the SPI bus
byte receivedData[size];
SPI.transfer(receivedData, size);

Back to table of contents.

usingInterrupt()

Using the SPI.usingInterrupt() function can be beneficial if the Arduino needs to perform other tasks during SPI communication. Handling SPI communication interrupts allows the Arduino to handle data transfers and errors more efficiently.

The SPI.usingInterrupt() function allows the Arduino to use interrupts during SPI communication. The interruptNumber parameter specifies the sequence number of the interrupt, usually 0 or 1, depending on which interrupt line is used.

The interrupt specified in the usingInterrupt() function parameter will be disabled when beginTransaction() is called and re-enabled after endTransaction() is called.

SPI.usingInterrupt(interruptNumber)

The usingInterrupt() function has no return value.

Back to table of contents.

ARDUINO MEGA 2560 REV3
ARDUINO MEGA 2560 REV3 – advertising

SPI Sample Codes

First, let’s see an example of implementing SPI communication between two Arduino UNOs. The SPI port pins are SCK 13, CIPO 12, and COPI 11. In the example, pin 10 will be the CS line, we can choose it freely. It is important to connect the GND pins as well!

Connect the two devices based on the image below.

SPI communication between two arduino unos

The Controller device controls the communication and sends the data to the Peripheral device. The Perpheral device can only send data if the Controller device has requested it. During SPI communication, both devices must use the same settings, such as baud rate and communication modes.

In the following simple example, the Controller sends a byte to the Perpheral and then waits for 1 second. Perpheral receives the value and then outputs it to the Soros monitor.

Example code for the Controller device, upload it to one of the Arduino UNOs.

#include <SPI.h>

const char chipSelectPin = 10;

void setup()
{
  pinMode(chipSelectPin, OUTPUT);
  digitalWrite(chipSelectPin, HIGH);
  SPI.begin();
  Serial.begin(9600);
}

void loop()
{
  digitalWrite(chipSelectPin, LOW);
  SPI.transfer(0x01);
  digitalWrite(chipSelectPin, HIGH);
  delay(1000);
}

And this is the code for the Peripheral device, which goes on the second Arduino UNO.

#include <SPI.h>

const char chipSelectPin = 10;
int data = 0;

void setup()
{
  pinMode(chipSelectPin, INPUT);
  SPI.begin();
  Serial.begin(9600);
}

void loop()
{
  if (digitalRead(chipSelectPin) == LOW)
  {
    data = SPI.transfer(0x00);
    Serial.println(data);
  }
}

The following example shows SPI communication between an Arduino Uno and a 74HC595 Shift Register IC. The 74HC595 IC is one of the simplest, cheapest port expansion solutions.

In the setup() function, we set the SPI communication and set the Arduino pin 10 as an output. This loads the CS (Chip Select) line used for SPI communication. This is connected to the latch pin of the 74HC595 IC.

In the loop() function we create a byte variable, then in the for() cycle we send the byte to the 74HC595 IC, then shift the bits of the byte to the left by 1 bit, and send it again in the next cycle. The for() loop runs 8 times and starts over.

To start communication, the latch pin of the 74HC595 IC is set to low so that the IC can receive data, and then the data is transferred using the SPI.transfer() function. Finally, the latch pin is set high so that the transferred data can be displayed on the output of the 74HC595 IC.

SPI communication between Arduino Uno and a 74HC595 Shift Register IC

Let’s see the example code:

#include <SPI.h>

const char latchPin = 10;            // 74HC595 IC latch pin

void setup()
{
  SPI.begin();                       // SPI initialization
  pinMode(latchPin, OUTPUT);         // set latch pin as output
}

void loop()
{
  byte data = 0b00000001;            // initialize output byte
  for (int i = 0; i < 8; i++)
  {
    digitalWrite(latchPin, LOW);     // setting latch pin low
    SPI.transfer(data);              // data transfer via SPI bus
    digitalWrite(latchPin, HIGH);    // setting latch pin high
    data = data << 1;                // Shift left by 1 bit
    delay(300);                      // 300ms wait
  }
}

Relays can also be connected to the LEDs connected to the 74HC595 IC, so with a suitable arduino diagram, we can connect many devices using a couple of arduino pins.

In the third example, we connect a common cathode 7-segment display to the SPI bus via the 74HC595 IC.

Connect the 74HC595 IC and the Arduino UNO based on the previous example. The LEDs are now replaced by a 7-segment display.

Connect the Q0-Q6 pins of 74HC595 IC to the ag pins of the 7-segment display via a 330-ohm resistor, and the Q7 pin to the dp pin of the 7-segment display via a 330-ohm resistor.

We connect a 7-segment display via the 74HC595 IC on the SPI bus to an arduino uno

The following sketch prints the digits 0 to 9 on the 7-segment display. For visibility, we put a one-second delay in the for() loop between displaying the digits.

#include <SPI.h>


const char latchPin = 10;


byte digits[] = {
  B11111100,      // 0
  B01100000,      // 1
  B11011010,      // 2
  B11110010,      // 3
  B01100110,      // 4
  B10110110,      // 5
  B10111110,      // 6
  B11100000,      // 7
  B11111110,      // 8
  B11110110       // 9
};

void setup()
{
  pinMode(latchPin, OUTPUT);
  SPI.begin();
}

void loop()
{
  for (int i = 0; i < 10; i++)
  {
    digitalWrite(latchPin, LOW);

    SPI.transfer(digits[i]);
    digitalWrite(latchPin, HIGH);
    delay(1000);
  }
}

From these few examples, we can see how easily we can use the possibilities of the Arduino SPI bus.

Back to table of contents.

advertising – Amazon.com