12 Getting Started with SPI

12.1 Introduction

Author: Alin Stoicescu, Microchip Technology Inc.

This technical brief provides information about Serial Peripheral Interface (SPI) on tinyAVR® 0- and 1-series, megaAVR® 0-series, and AVR® DA devices, and intends to familiarize the user with AVR microcontrollers. The document describes the application area, the modes of operation, and the hardware and software requirements of the SPI.

Throughout the document, the configuration of the peripheral will be described in detail, starting with the location of the SPI pins, the direction of the pins, how to initialize the device as a host or a client, and how to exchange data inside the system. This document covers the following use cases:

Note: For each use case described in this document, there are two code examples: One bare metal developed on ATmega4809, and one generated with MPLAB® Code Configurator (MCC) developed on AVR128DA48.

12.2 Overview

The SPI bus is a synchronous serial communication interface based on four types of logic signals:

  • SCK: Serial Clock (output from the host)
  • MOSI: Host Output Client Input (data output from the host)
  • MISO: Host Input Client Output (data output from the client)
  • SS: Client Select (active-low, data output from the host)

This peripheral is used for short distance and high-speed communication, primarily in embedded systems. The SPI devices communicate in Full-Duplex mode, using a channel for transmitting and one for receiving data. The SPI is based on a host-client architecture with a single host at a time and one or more clients. The host device is the only one that can generate a clock, thus it is the initiator of the data exchange. The SPI host device uses the same SCK, MOSI and MISO channels for all the clients, but usually individual lines of SS for each of the clients. The host device selects the desired client by pulling the SS signal low.

The data to be sent will be stored in either a data register or, if a transmission is already in progress and the Buffer mode was activated, in a buffer register. The data are sent out serially on the MOSI channel, using a shift register, and every bit is synchronized using the SPI clock generator. While every bit is shifted out, new data are received on the MISO channel from the client and are shifted in a receiver buffer and further in the receive DATA register. If the receiver is busy, meaning there are already data in the receive DATA register and the Buffer mode was activated, the data will be temporarily stored in a second receiver buffer. The Buffer mode is activated by setting high the BUFEN bit of the CTRLB register.

Figure 12-143. SPI Block Diagram

The SPI module has five registers. One register is used for data transfer and storage, two registers are used for Interrupt flags, and the last two registers (CTRLA and CTRLB) are for initializations. All the configurations required to make the peripheral work correctly are reduced to changing some bits in the CTRLA register, while the CTRLB register is focused on different modes of operation that are optional. More details regarding the registers can be found in the family data sheet of the device, on the register summary of the peripheral section.

Figure 12-144. Register Summary - SPIn

12.3 Sending Data as a Master SPI Device

The master is the device that decides when to trigger communication and which slave is the recipient of the message. SPI master devices are generally used in high-speed communication and the focus is to exchange data with other devices acting as slaves (e.g. sensors, memories or other MCUs).

This use case follows the steps:

  • Configure the location of the SPI pins
  • Initialize the peripheral
  • Configure the direction of the pins
  • Control slave devices
  • Send data as a master device

How to Configure the Location of the SPI Pins

The way to configure the location of the pins is independent of the application purpose and the SPI mode. Each microcontroller has its own default physical pin position for peripherals. These locations can be found on PORTMUX peripheral chapter from the family data sheet of the megaAVR 0-series. For ATmega4809, the SPI pins are located on PA[7:4] and can be changed on PC[3:0] or PE[3:0] using the TWISPIROUTEA register of the PORTMUX module.

Figure 12-145. TWISPIROUTEA Register

The order of the pins is the following: MOSI, MISO, SCK, SS; MOSI representing the lowest pin number from the group. This is how a user can change the location of the SPI pins for option 1 with port C:

This translates into the following code:

PORTMUX.TWISPIROUTEA |= PORTMUX_SPI00_bm;

Or option 2 with port E:

This translates into the following code:

PORTMUX.TWISPIROUTEA |= PORTMUX_SPI01_bm;

How to Initialize the Peripheral

The clock frequency is derived from the main clock of the microcontroller and is reduced using a prescaler or divider circuit present in the SPI hardware. By default, the source of the main clock is a 20 MHz internal oscillator, which is divided by a prescaler whose default value is 6. Thus, resulting in a main clock frequency of approximately 3.33 MHz. More information about the clock can be found in Clock Controller chapter of the family data sheet of the megaAVR 0-series.

The clock frequency of the SPI can also be increased using the Double Clock mode, which works only in Master mode. The Data Order bit represents the endianness (Most Significant bit or Least Significant bit) of the data, the order in which the bits are transmitted on the channel (starting with the last or the first bit from a register). All the configurations are related to CTRLA register.

Figure 12-146. CTRLA Register

Next is an example on how to configure a master SPI device with the default main clock source and with the default pin location presented in the previous topic. A 416 kHz frequency will result by configuring the device in Double-Speed mode and with a 16 times divider. The data will be shifted out starting with the Most Significant bit (MSb):

This translates into the following code:

SPI0.CTRLA = SPI_CLK2X_bm 
           | SPI_DORD_bm
           | SPI_ENABLE_bm
           | SPI_MASTER_bm
           | SPI_PRESC_DIV16_gc;

How to Configure the Direction of the Pins

Since the master devices control and initiate transmissions, the MOSI, SCK and SS pins must be configured as output, while the MISO channel will keep its default direction as input. The default values, directions and configurations explained above are still applicable here. The following example is based on the default position of the SPI pins:

PORTA.DIR |= PIN4_bm;  // MOSI channel
PORTA.DIR &= ~PIN5_bm; // MISO channel
PORTA.DIR |= PIN6_bm;  // SCK channel
PORTA.DIR |= PIN7_bm;  // SS channel

An SPI master device can control more than one slave, thus requiring more SS pins. The additional SS channels can be configured just like the one in the example above. The user must choose an unused pin and configure its direction as output.

How to Control Slave Devices

A master will control a slave by pulling low the SS pin. If the slave has set the direction of the MISO pin to output, when the SS pin is low, the SPI driver of the slave will take control of the MISO pin, shifting data out from its transmit DATA register. All slave devices can receive a message, but only those with SS pin pulled low can send data back. Though, it is not recommended to enable more than one slave in a typical connection (like the one below) because all of them will try to respond to the message and there is only one MISO channel, thus the transmission will result in a write collision. The user can check the appearance of collisions by reading the value of WRCOL bit in INTFLAGS register.

Figure 12-147. Typical SPI Bus

How to Send Data as a Master Device

All the settings configured before are considered in the following example and the polling method is used for flag checking. Before sending data, the user must pull low an SS signal to let the slave device know it is the recipient of the message.

PORTA.OUT &= ~PIN7_bm;

Once the user writes new data into the DATA register the hardware starts a new transfer, generating the clock on the line and shifting out the bits.

Figure 12-148. DATA Register
SPI0.DATA = data;

When the hardware finishes shifting all the bits, it activates a receive Interrupt flag, which can be found in the INTFLAGS register.

Figure 12-149. INTFLAGS Register

The user must check the state of the flag, before writing new data in the register, by either activating the interrupts or by constantly reading the value of the flag (method called polling), else a write collision interrupt will occur.

while (!(SPI0.INTFLAGS & SPI_IF_bm))
{
    ;
}

The user can pull the SS channel high if there is nothing left to transmit.

PORTA.OUT |= PIN7_bm;

Full Code Example

Tip: The full code example is also available in the Appendix section.

12.4 Receiving Data as a Client SPI Device

The client devices are usually actuators. Clients do no initiate any action, they only act whenever the host initiates. A client must be always available and has to wait until the host pulls low its SS channel.

This use case follows the steps:

  • Initialize the peripheral
  • SPI client direction pin configuration
  • Receive data as a client SPI

How to Initialize the Peripheral

The client gets its clock signal from the host device so there is no point changing the clock divider of the peripheral, a change that does not affect when in SPI Client mode. Though, the hardware peripheral has to sample the data received on the MOSI channel. For the data signal to be correctly reconstructed, the main clock frequency of the device must be at least double the clock received on the SPI SCK channel.

If the client device is a microcontroller, the user has to consider the frequency request and configure a powerful clock source. If the user does not have access to the clock generator of the client, it has to make sure the host does not exceed the limitations of the client. A host is part of the same system or application and is mainly represented by a microcontroller whose frequency can be easily changed - either SPI or main clock frequency.

To make the example easier to understand, some of the information presented in Sending Data as a Master SPI Device is also applied here. The device is configured as a client, with a main clock frequency of 3.33 MHz, and the data are shifted out starting with the LSb. Configuring the device as a client resumes mainly to enabling the module and deactivating the Host (MASTER) bit from the CTRLA register:

Figure 12-150. CTRLA Register

SPI0.CTRLA = SPI_DORD_bm
           | SPI_ENABLE_bm
           & (~SPI_MASTER_bm);

SPI Client Direction Pin Configuration

When the device is in SPI Client mode, the MOSI, SCK and SS pins require to be configured as input channels. By default, all Input/Output (I/O) pins are configured as input, so there is nothing that needs to be modified for these pins. Thus, the hardware circuit of the SPI will take control of these channels during transmission if the peripheral is enabled. Since it is not mandatory to send data back, the MISO channel can be configured either as output or input.

The normal mode is to configure the pin as an output, and the hardware circuit will control its behavior during data exchanges. If the pin is configured as input, it will act as an ordinary I/O pin and will not be used by the SPI.

When the pin value of the DIR register has the value 0, the pin acts as an input digital pin, respective output digital pin for value 1. The default location of the SPI pins will be considered. To be sure that the default direction value of the pins was not changed, all the required pins will be configured as follows:

PORTA.DIR &= ~PIN4_bm; // MOSI channel 
PORTA.DIR |= PIN5_bm;  // MISO channel
PORTA.DIR &= ~PIN6_bm; // SCK channel
PORTA.DIR &= ~PIN7_bm; // SS channel

How to Receive Data as a Client SPI

All the client devices connected to the SPI bus will receive the message sent on the MOSI channel by the host device. A client cannot respond to a message unless the SS channel is pulled low. When the host device pulls the SS pin low, the SPI peripheral of the client device will take control of the MISO pin and will shift data out. If the user does not write into the DATA register, the client will not send data out and the peripheral will shift out a byte full of zeros.

The peripheral will signal the reception of new data by activating the IF flag of the INTFLAGS register. The user has to check the value of the bit, either by the polling method as presented in the host example or by interrupts. The following example uses interrupts to establish the value of the bit since there is no way to tell when the host will send new data and interrupts are non-blocking. Therefore, the device can do whatever it has to do during idle SPI time.

When using interrupts, three important things must be taken into consideration:

  1. Activating the interrupts for the microcontroller. The macro can be used by including the <avr/interrupt.h> file:
    sei();
    
  2. Activating the interrupts for the peripheral can be done by activating the IE flag from the INTCTRL register:
    Figure 12-151. INTCTRL Register
    SPI0.INTCTRL = SPI_IE_bm;
  3. Clearing the Interrupt flag, if it is not cleared automatically by the hardware. After receiving new data, the receive complete Interrupt flag will be activated. This one can be found in the INTFLAGS register.
    Figure 12-152. INTFLAGS Register

Clearing the Interrupt flag is done by writing ‘1’ to the bit inside the interrupt function, where the user may also insert its interrupt routine based on its application purpose.

In the example below, it is shown how to read the received data, clear the interrupt and write to the DATA register (it is the user’s choice what to do with the received data and what to write back to the host).

ISR(SPI0_INT_vect)
{
	receiveData = SPI0.DATA;

	SPI0.DATA = writeData;

	SPI0.INTFLAGS = SPI_IF_bm;
}
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

12.5 Changing Data Transfer Type

It represents how data are transmitted concerning clock generation. The clock polarity and the clock phase are the ones important for data modes. By clock polarity, one can understand the level of the signal which can be low while in the Idle state and will start with a rising edge when transmitting data, or it can be high while in Idle state and will start with a falling when exchanging data. Depending on the phase, the data are generated or sampled concerning the clock on the channel: On a rising or a falling edge. See the figure below.

Figure 12-153. SPI Data Transfer Modes

Both the host and the client devices must be configured in the same way, so one can decode correctly what the other encoded. Data modes can be selected by changing the value of the MODE[1:0] bit field from the CTRLB register.

Figure 12-154. CTRLB Register

Until now, the examples were based on SPI Mode 0 because there was no change made to these bits and that is the default value of the bits.

Below is an example of how to configure the SPI in Data Mode 3, based on the normal/basic host SPI Initialization mode presented in Sending Data as a Master SPI Device. The only difference is the change of the data transmission type:

SPI0.CTRLB |= SPI_MODE_3_gc;
Tip: The full code example is also available in the Appendix section.

An MCC generated code example for AVR128DA48, with the same functionality as the one described in this section, can be found here:

12.6 References

12.7 Appendix

Note: The following code was developed for ATmega4809 Curiosity Nano platform.

Sending Data as a Host SPI Device Full Code Example

#include <avr/io.h>

void SPI0_init(void);
void clientSelect(void);
void clientDeselect(void);
uint8_t SPI0_exchangeData(uint8_t data);

void SPI0_init(void)
{
    PORTA.DIR |= PIN4_bm;  /* Set MOSI pin direction to output */
    PORTA.DIR &= ~PIN5_bm; /* Set MISO pin direction to input */
    PORTA.DIR |= PIN6_bm;  /* Set SCK pin direction to output */
    PORTA.DIR |= PIN7_bm;  /* Set SS pin direction to output */

    SPI0.CTRLA = SPI_CLK2X_bm           /* Enable double-speed */
               | SPI_DORD_bm            /* LSB is transmitted first */
               | SPI_ENABLE_bm          /* Enable module */
               | SPI_MASTER_bm          /* SPI module in Host mode */
               | SPI_PRESC_DIV16_gc;    /* System Clock divided by 16 */
}

uint8_t SPI0_exchangeData(uint8_t data)
{
    SPI0.DATA = data;

    while (!(SPI0.INTFLAGS & SPI_IF_bm))  /* Waits until data are exchanged*/
    {
        ;
    }

    return SPI0.DATA;
}

void clientSelect(void)
{
    PORTA.OUT &= ~PIN7_bm; // Set SS pin value to LOW
}

void clientDeselect(void)
{
    PORTA.OUT |= PIN7_bm;  // Set SS pin value to HIGH
}

int main(void)
{
    uint8_t data = 0;

    SPI0_init();

    while (1)
    {
        clientSelect();
        SPI0_exchangeData(data);
        clientDeselect();
    }
}

Receiving Data as a Client SPI Device Full Code Example

#include <avr/io.h>
#include <avr/interrupt.h>

void SPI0_init(void);

volatile uint8_t receiveData = 0;
volatile uint8_t writeData = 0;

void SPI0_init(void)
{
    PORTA.DIR &= ~PIN4_bm; /* Set MOSI pin direction to input */
    PORTA.DIR |= PIN5_bm;  /* Set MISO pin direction to output */
    PORTA.DIR &= ~PIN6_bm; /* Set SCK pin direction to input */
    PORTA.DIR &= ~PIN7_bm; /* Set SS pin direction to input */

    SPI0.CTRLA = SPI_DORD_bm        /* LSB is transmitted first */
               | SPI_ENABLE_bm      /* Enable module */
               & (~SPI_MASTER_bm);  /* SPI module in Client mode */

    SPI0.INTCTRL = SPI_IE_bm;       /* SPI Interrupt enable */
}

ISR(SPI0_INT_vect)
{
    receiveData = SPI0.DATA;

    SPI0.DATA = writeData;

    SPI0.INTFLAGS = SPI_IF_bm; /* Clear the Interrupt flag by writing 1 */
}

int main(void)
{    
    SPI0_init();

    sei(); /* Enable Global Interrupts */

    while (1)
    {
        ;
    }
}

Changing Data Type Full Code Example

#include <avr/io.h>

void SPI0_init(void);
void clientSelect(void);
void clientDeselect(void);
uint8_t SPI0_exchangeData(uint8_t data);

void SPI0_init(void)
{
    PORTA.DIR |= PIN4_bm;  /* Set MOSI pin direction to output */
    PORTA.DIR &= ~PIN5_bm; /* Set MISO pin direction to input */
    PORTA.DIR |= PIN6_bm;  /* Set SCK pin direction to output */
    PORTA.DIR |= PIN7_bm;  /* Set SS pin direction to output */

    SPI0.CTRLA = SPI_CLK2X_bm           /* Enable double-speed */
               | SPI_DORD_bm            /* LSB is transmitted first */
               | SPI_ENABLE_bm          /* Enable module */
               | SPI_MASTER_bm          /* SPI module in Host mode */
               | SPI_PRESC_DIV16_gc;    /* System Clock divided by 16 */

    SPI0.CTRLB |= SPI_MODE_3_gc; /* Data Mode 3 */
}

uint8_t SPI0_exchangeData(uint8_t data)
{
    SPI0.DATA = data;

    while (!(SPI0.INTFLAGS & SPI_IF_bm))   /* Waits until data are exchanged*/
    {
        ;
    }
   
    return SPI0.DATA;
}

void clientSelect(void)
{
    PORTA.OUT &= ~PIN7_bm; // Set SS pin value to LOW
}

void clientDeselect(void)
{
    PORTA.OUT |= PIN7_bm;  // Set SS pin value to HIGH
}

int main(void)
{
    uint8_t data = 0;

    SPI0_init();

    while (1)
    {
        clientSelect();        
        SPI0_exchangeData(data);
        clientDeselect();
    }
}