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:
- Sending Data
as a Host SPI Device:
The device will be configured as a host, will control the client, and will send data using a method called polling.
- Receiving
Data as a Client SPI Device:
The device will be configured as a client and will wait for the incoming data. The data reception will be triggered by interrupts.
- Changing Data
Transfer Type:
The device will be configured as a host and will send data with respect to the clock polarity and the clock phase.
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.
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.
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.
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.
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.
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.
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.
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
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:
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:
- Activating the interrupts for the
microcontroller. The macro can be used by including the
<avr/interrupt.h>
file:sei();
- Activating the interrupts for the
peripheral can be done by activating the IE flag from the INTCTRL register:
SPI0.INTCTRL = SPI_IE_bm;
- 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.
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; }
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.
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.
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;
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
- AVR128DA48 product page: www.microchip.com/wwwproducts/en/AVR128DA48
- AVR128DA48 Curiosity Nano Evaluation Kit product page: https://www.microchip.com/Developmenttools/ProductDetails/DM164151
- AVR128DA28/32/48/64 Data Sheet
- Getting Started with the AVR® DA Family
- ATmega4809 product page: www.microchip.com/wwwproducts/en/ATMEGA4809
- megaAVR® 0-series Family Data Sheet
- ATmega809/1609/3209/4809 – 48-Pin Data Sheet megaAVR® 0-series
- ATmega4809 Xplained Pro product page: https://www.microchip.com/developmenttools/ProductDetails/atmega4809-xpro
12.7 Appendix
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(); } }