SPI for microcontrollers

Table of Contents

In this article we will take a closer look at Serial Peripheral Interface, also called SPI. This is the third and last serial communication module in the PIC16F877A. Previously we have seen UART and I2C, and we discussed how those modules operate. Here we focus solely on SPI; what it is and how it works. This article starts by giving a short introduction into SPI to give you an overview of the basic understanding. After that we will look further into the hardware, and how to set it up with PIC microcontrollers. Next is the SPI protocol; even though it is not as elaborate as I2C, certain features of SPI require attention. With all this knowledge, we will explain how to configure the master and slave modes and we end with a simple point-to-point example on how two PIC16F877A MCUs can talk to each other. The tutorial is written for this particular microcontroller, but can easily be translate to other PIC MCUs.

1. Introduction to SPI

SPI is a widely used synchronous serial communication protocol employed in microcontroller systems. It allows multiple peripheral devices to communicate with a single microcontroller or microprocessor using a master-slave configuration. In SPI, data is transmitted in a full-duplex manner, meaning that data can be sent and received simultaneously. It utilizes a master device to control the communication process, while one or more slave devices respond to the master’s commands.

The basic configuration involves four signal lines: _MOSI_ (Master Out Slave In), _MISO_ (Master In Slave Out), _SCK_ (Serial Clock), and _SS_ (Slave Select). For the latter one, some people rather use the term CS (Chip Select). These lines facilitate data exchange and synchronization between devices. Throughout this article I will also use the terms _SDO_ (Serial Data Out) and _SDI_ (Serial Data In). These refer more to the I/O pins as to the actual line between them. As to not get any confusion with MISO and MOSI, I refer to Figure 1.

Fig 1. Master - Slave SPI pin configuration.
Fig 1. Master - Slave SPI pin configuration.

SPI is known for its high-speed data transfer capability, making it ideal for applications that demand rapid and reliable communication between peripherals and the microcontroller.Devices like sensors, displays, memory chips, and other integrated circuits often implement SPI for their ease of integration and efficiency in data transfer. Furthermore, SPI is versatile and is compatible with a wide range of microcontroller architectures and families.

2. Physical Layer

The MSSP (Master Synchronous Serial Port) module in the PIC16F877A is a serial interface and can operate in one of two modes, namely:

  • Serial Peripheral Interface (SPI) or,
  • Inter-Integrated Circuit (I2C)
    • Full Master mode
    • Slave mode

Hence, for this tutorial we have to configure this module to work only in SPI mode. The SPI mode allows a synchronous transmission and receiving of a single byte at the time, as that is the size of the shift register (SSPSR). To accomplish the communication, we typically need 3 pins for the master device and a 4th pin on the slave device. The pin layout on the PIC16F877A is as follows:

  • Serial Data Out (_SDO_) – RC5/SDO
  • Serial Data In (_SDI_) – RC4/SDI/SDA
  • Serial Clock (_SCK_) – RC3/SCK/SCL

Additionally, a fourth pin may be used when in a Slave mode of operation:

  • Slave Select (_SS_) – RA5/AN4/SS/C2OUT

In the case multiple slaves are connected to a single master, each slave is connected to the same SCK/SDO/SDI pins of the master, but they are selected individually by their respective SS pin on the master. On the slave this should be configured on the RA5 pin, while on the master it can be any free output pin. An example on how to connect it can be seen in Figure 2.

Fig 2. Master with multiple slaves - SPI configuration.
Fig 2. Master with multiple slaves - SPI configuration.

Note: When using multiple slaves, errors might occur due to floating lines. When the MCU intializes slave device #1, it sets its SS pin, however the SS on device number #2 and #3 are still unconfigured. If these pins are floating, at voltage below a logic 1, they can hear the communication to device #1 and perhaps even drive the MISO pins. Adding a pull-up resistor (or internal pull-ups) of roughly 10k Ohm will make sure that during the initialization, the SS line is at least at logic HIGH. To conclude, when using multiple slaves, these pull-up resistors are only to make sure that during power-up, the slave devices will ignore any garbage on the line. Of course, this will increase the power consumption ever so slightly, but at least it will not cause an error.

3. SPI Protocol

In contrast to the I2C protocol, SPI does not require any of the Start/Stop/ACK/etc bits to communicate between devices. This has its pros and cons, such as that there is no feedback from the slave that the data has been received. In fact, the master could send data and no one could be at the other end to receive it. You will find different self-made protocols throughout literature, however the basics of communication stays the same.

The MSSP consists of two key components:

  1. Transmit/Receive Shift Register (SSPSR): This component manages the data flow in and out of the device, starting with the Most Significant Bit (MSB).

  2. Buffer Register (SSPBUF): It holds the data written to the SSPSR until the received data is ready.

Once eight bits of data are received, the byte is transferred to the SSPBUF register. Subsequently, the Buffer Full detect bit (BF – SSPSTAT<0>) and the interrupt flag bit (SSPIF) are both set.

This double-buffering mechanism with SSPBUF ensures that the next byte of data can begin reception before the previously received data is even read.

If there’s an attempt to write to the SSPBUF register during data transmission/reception, it will be ignored, and the Write Collision Detect bit (WCOL – SSPCON<7>) will be set. Clearing the WCOL bit in user software is necessary to ascertain the success of subsequent writes to the SSPBUF register.

When expecting valid data, it’s important to read the SSPBUF before writing the next byte of data to transfer. The BF bit indicates when SSPBUF has received data (signaling completion of transmission). Reading SSPBUF clears the BF bit. Note that this data may not be relevant if the SPI is functioning only as a transmitter.

Typically, the MSSP interrupt is used to confirm the completion of transmission/reception, requiring a read and/or write operation on SSPBUF. If an interrupt method isn’t employed, software polling can be used to prevent write collisions.

3.1 Clock Signal - Phase and Polarity

SPI has no defined protocol, unlike I2C, this limits the overhead and increases its communication speed. As there is no need for Start or Stop bits, or waiting for acknowledgments. Most of it is done by the MOSI, MISO, SCK and SS lines. In addition to the clock frequency, the Master must also configure its clock Polarity (_CKP_) and clock Phase (_CKE_). You will also find the terms CPOL and CPHA for the polarity and phase, respectively. CKP and CKE tell the master and slave when to sample the data.

The clock polarity (_CKP_) determines the initial state of the click line. If CKP = 1, it means that SCK’s initial state is HIGH. If CKP = 0, it means that SCK’s initial state is LOW.

The clock phase (_CKE_) determines the edge. If CKE = 0, it means the leading edge (1st). If CKE = 1, it means the trailing edge (2nd).

A combination of CKE and CKP gives you 4 different modes,

Fig 3. Clock Polarity and Clock Phase.
Fig 3. Clock Polarity and Clock Phase.

4. Configuring Modes

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut elit tellus, luctus nec ullamcorper mattis, pulvinar dapibus leo.

Registers Description
SSPCON
Control register; enables SPI (master/slave), enables serial ports, SCK/SDO/SDI/SS, etc.
SSPSTAT
Status register; SPI Clock select, Buffer full bit, etc.
SSPSR
Receives/transmits the incoming data in a shift register
SSPBUF
This register can be interacted with to read from or to write to, and places it automatically onto SSPSR
SSPSTAT register 16F877A

SMP: Sample bit
SPI Master mode
1 = Input data sampled at end of data output time
0 = Input data sampled at middle of data output time
SPI Slave mode
SMP must be cleared when SPI is used in Slave mode.

CKE: SPI Clock Select bit
1 = Transmit occurs on transition from active to Idle clock state
0 = Transmit occurs on transition from Idle to active clock state
Note: Polarity of clock state is set by the CKP bit (SSPCON1<4>).

BF: Buffer Full Status bit (Receive mode only)
1 = Receive complete, SSPBUF is full
0 = Receive not complete, SSPBUF is empty

SSPCON Register PIC16F877A

WCOL: Write Collision Detect bit (Transmit mode only)
1 = The SSPBUF register is written while it is still transmitting the previous word. (Must be cleared in software.)
0 = No collision

SSPOV: Receive Overflow Indicator bit
SPI Slave mode:
1 = A new byte is received while the SSPBUF register is still holding the previous data. In case of overflow, the data in SSPSR is lost. Overflow can only occur in Slave mode. The user must read the SSPBUF, even if only transmitting data, to avoid setting overflow. (Must be cleared in software.)
0 = No overflow
Note: In Master mode, the overflow bit is not set, since each new reception (and transmission) is initiated by writing to the SSPBUF register

SSPEN: Synchronous Serial Port Enable bit
1 = Enables serial port and configures SCK, SDO, SDI, and SS as serial port pins
0 = Disables serial port and configures these pins as I/O port pins
Note: When enabled, these pins must be properly configured as input or output.

CKP: Clock Polarity Select bit
1 = Idle state for clock is a high level
0 = Idle state for clock is a low level

SSPM3:SSPM0: Synchronous Serial Port Mode Select bits
0101 = SPI Slave mode, clock = SCK pin. SS pin control disabled. SS can be used as I/O pin
0100 = SPI Slave mode, clock = SCK pin. SS pin control enabled
0011 = SPI Master mode, clock = TMR2 output/2
0010 = SPI Master mode, clock = FOSC/64
0001 = SPI Master mode, clock = FOSC/16
0000 = SPI Master mode, clock = FOSC/4
Note: Bit combinations not specifically listed here are either reserved or implemented in I2C mode only.

4.1 Master Mode

Due to the lack of overhead, configuring SPI does not require many registers as can be seen above. To configure the master, we need to:

  • enable SPI Master mode with the desired clock rate.
  • enable the Synchronous Serial Port.
  • configure CKP, CKE and SMP.
  • Configure the IO pins.
  • Turn on the interrupts (optional).
				
					void SPI_Init_Master() 
{
    SSPSTAT = 0b00000000; // SMP = 0, CKE = 0
    SSPCON = 0b00100010;  // SSPEN = 1, CKP = 0 SSPM3:SSPM0 = 0010 (SPI Master mode, clock = Fosc/64)
   
    TRISC5 = 0; // SDO -> Output
    TRISC4 = 1; // SDI -> Input
    TRISC3 = 0; // SCK -> output
    TRISD3 = 0; // SS (RD3) is an output.
   
    // Set up interrupts (optional)
    INTCON.GIE = 1;    // Enable global interrupts
    INTCON.PEIE = 1;   // Enable peripheral interrupts
    PIE1.SSPIE = 1;    // Enable SPI interrupt
}
				
			

In addition to initializing, the master also has to send data to the slave. This can be done by a simple function that sets data onto the SSP Buffer.

				
					void SPI_Write(unsigned char data)
{
    SSPBUF = data; //Write the data into the SSP Buffer
}
				
			

4.2 Slave Mode

To configure the slave, we need to:

  • Enable SPI Slave with SS enabled.
  • Enable the Synchronous Serial Port.
  • Configure CKP and CKE same as the Master, and clear SMP.
  • Configure the IO pins.
  • Turn on the interrupts (optional, but recommended).
				
					void SPI_Init_Slave() 
{
    SSPSTAT = 0b00000000; // SMP = 0, CKE = 0
    SSPCON = 0b00100100;  // SSPEN = 1, CKP = 0, SSPM3:SSPM0 = 0010 (SPI Master mode, clock = Fosc/64)
   
    TRISC5 = 0; // SDO -> Output
    TRISC4 = 1; // SDI -> Input
    TRISC3 = 1; // SCK -> Input
    TRISD3 = 1; // SS (RD3) is an input.
   
    // Set up interrupts (recommended)
    INTCON.GIE = 1;    // Enable global interrupts
    INTCON.PEIE = 1;   // Enable peripheral interrupts
    PIE1.SSPIE = 1;    // Enable SPI interrupt
}
				
			

The slave requires a function to read the incoming data. I will use a simple ISR that will read the SSP Buffer (and by doing so clear the BF bit), and clear the SSPIF bit manually. That is all that is required during the ISR.

				
					void interrupt ISR() {
    if (PIR1.SSPIF) {
        received_data = SSPBUF; // Reads data, and clears BF bit
        PIR1.SSPIF = 0;                 // Clear the SPI interrupt flag
    }
}
				
			

5. SPI Example

Master SPI example

This is an example of an SPI Master device that transmit a single byte to a slave. As such, the functions used in this example is very limited.

				
					#include <pic16f877a.h>
#include <xc.h>

// Define the frequency of the external crystal oscillator
#define _XTAL_FREQ 20000000  // 20MHz external crystal oscillator frequency

void SPI_Init_Master();
void SPI_Write(unsigned char data);

void main()
{
    SPI_Init_Master();
    while(1)
    {
        PORTD3 = 0;  // Pull SS low to select the slave
        SPI_Write(0x8F);
        PORTD3 = 1;  // Pull SS high to deselect the slave
        __delay_ms(1000);
        
        PORTD3 = 0;  // Pull SS low again to select the slave
        SPI_Write(0x00);
        PORTD3 = 1;  // Pull SS high to deselect the slave
        __delay_ms(1000);
    }
}

// end main ------------------------------------------------

void SPI_Init_Master() 
{
    SSPSTAT = 0b00000000; // SMP = 0, CKE = 0
    SSPCON = 0b00100010;  // SSPEN = 1, CKP = 0 SSPM3:SSPM0 = 0010 (SPI Master mode, clock = Fosc/64)
   
    TRISC5 = 0; // SDO -> Output
    TRISC4 = 1; // SDI -> Input
    TRISC3 = 0; // SCK -> output
    TRISD3 = 0; // SS (RD3) is an output.
   
    // Set up interrupts (optional)
    INTCON.GIE = 1;    // Enable global interrupts
    INTCON.PEIE = 1;   // Enable peripheral interrupts
    PIE1.SSPIE = 1;    // Enable SPI interrupt
}

void SPI_Write(unsigned char data)
{
    SSPBUF = data; //Write the data into the SSP Buffer
}
				
			
Slave SPI example

This is the example of an SPI Slave device that receives a single byte of data. Again, same as before, not all possible functions that have been described are used, such as both writing and reading. However, with some combining, you can incorporate both.

				
					#include <pic16f877a.h>
#include <xc.h>

// Define the frequency of the external crystal oscillator
#define _XTAL_FREQ 20000000  // 20MHz external crystal oscillator frequency

volatile uint8_t received_data;

void SPI_Init_Slave();

void main()
{
    SPI_Init_Slave(); 
    
    while(1)
    {
        // Do something with incoming received_data here
    }
}

// end main ------------------------------------------------

void SPI_Init_Slave() 
{
    SSPSTAT = 0b00000000; // SMP = 0, CKE = 0
    SSPCON = 0b00100100;  // SSPEN = 1, CKP = 0, SSPM3:SSPM0 = 0010 (SPI Master mode, clock = Fosc/64)
   
    TRISC5 = 0; // SDO -> Output
    TRISC4 = 1; // SDI -> Input
    TRISC3 = 1; // SCK -> Input
    TRISD3 = 1; // SS (RD3) is an input.
   
    // Set up interrupts (recommended)
    INTCON.GIE = 1;    // Enable global interrupts
    INTCON.PEIE = 1;   // Enable peripheral interrupts
    PIE1.SSPIE = 1;    // Enable SPI interrupt
}

void interrupt ISR() {
    if (PIR1.SSPIF) {
        received_data = SSPBUF; // Reads data, and clears BF bit
        PIR1.SSPIF = 0;         // Clear the SPI interrupt flag
    }
}
				
			
Share
Tweet
Share
Pin
Email
5 2 votes
Article Rating
Subscribe
Notify of
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments