Skip to content

SPI

SPI INTRODUCTION

Serial Peripheral Interface

SPI (Serial Peripheral Interface) is, as the name suggests, a serial communication interface for peripheral devices. It was originally defined by Motorola for its MC68HCXX series processors. SPI is a high-speed, full-duplex, synchronous serial communication bus and has been widely used in communication between many MCUs, storage chips, AD converters, and LCDs. Similar to I2C communication, SPI allows one master device and one or more slave devices on the communication bus. To communicate with a slave device, the master device requires at least four data lines:

  • MOSI (Master Out / Slave In): Master data output, slave data input, used for the master to send data to the slave.

  • MISO (Master In / Slave Out): Master data input, slave data output, used for the slave to send data to the master.

  • SCLK (Serial Clock): Clock signal generated by the master device to determine the communication rate.

  • CS (Chip Select): Slave device chip select signal generated by the master device; the slave device is selected when the signal is low.

Multiple slave SPI communication networks are connected as shown in the diagram below:

SPI

From the diagram, it can be observed that the MOSI, MISO, and SCLK pins are connected to every device on the SPI bus. If the CS pin is set to low, the slave device listens to and communicates with the master. The SPI master can communicate with only one slave at a time. To communicate with another slave device, the master must terminate the current communication before starting a new one.

SPI communication operates in four different modes. Some slave devices may be pre-configured at the factory to operate in a specific mode, which cannot be changed. Both the master and slave must operate in the same mode for successful communication. The SPI mode is determined by configuring CPOL (Clock Polarity) and CPHA (Clock Phase).

  • CPOL (Clock Polarity): Refers to the idle state of the clock signal when no data is being transmitted. If the idle state is high, CPOL = 1; if the idle state is low, CPOL = 0.
  • CPHA (Clock Phase): Refers to the clock edge at which data is sampled. CPHA = 0 means data is sampled on the first edge (odd edge), determined by CPOL (either rising or falling edge). CPHA = 1 means data is sampled on the second edge (even edge).

SPI-MODE

1) Mode 0 (CPOL=0, CPHA=0):
In idle state, SCLK is low. Data is sampled on the first edge, which is the rising edge (SCLK transitions from low to high). Data is transmitted on the falling edge.

2) Mode 1 (CPOL=0, CPHA=1):
In idle state, SCLK is low. Data is sampled on the second edge, which is the falling edge (SCLK transitions from high to low). Data is transmitted on the rising edge.

3) Mode 2 (CPOL=1, CPHA=0):
In idle state, SCLK is high. Data is sampled on the first edge, which is the falling edge (SCLK transitions from high to low). Data is transmitted on the rising edge.

4) Mode 3 (CPOL=1, CPHA=1):
In idle state, SCLK is high. Data is sampled on the second edge, which is the rising edge (SCLK transitions from low to high). Data is transmitted on the falling edge.

ESP32-S3 SPI Controller

The ESP32-S3 chip integrates four SPI controllers: SPI0, SPI1, SPI2, and SPI3. Among these, SPI0 and SPI1 are primarily used internally to access external FLASH and PSRAM, leaving SPI2 and SPI3 available for general use. SPI2 is also known as HSPI, and SPI3 is referred to as VSPI. Both are classified as GP-SPI (General Purpose SPI).

GP-SPI Features:

  • Supports both master and slave modes.
  • Supports half-duplex and full-duplex communication.
  • Supports multiple data modes:
  • SPI2: 1-bit SPI mode, 2-bit Dual SPI mode, 4-bit Quad SPI mode, QPI mode, 8-bit Octal mode, and OPI mode.
  • SPI3: 1-bit SPI mode, 2-bit Dual SPI mode, 4-bit Quad SPI mode, and QPI mode.
  • Configurable clock frequency:
  • In master mode: Up to 80 MHz.
  • In slave mode: Up to 60 MHz.
  • Configurable read/write order for data bits.
  • Configurable clock polarity (CPOL) and phase (CPHA).
  • Supports four SPI clock modes (Mode 0 to Mode 3).
  • Provides multiple CS (Chip Select) lines in master mode:
  • SPI2: CS0 ~ CS5.
  • SPI3: CS0 ~ CS2.
  • Supports interfacing with SPI-connected devices such as sensors, display controllers, flash, or RAM chips.

The SPI2 and SPI3 interface signal lines can be mapped to chip pins through the GPIO matrix and IO_MUX, offering great flexibility in pin assignment.

Test Case

This section demonstrates using SPI to control an LCD display. Please refer to the LCD chapter for detailed guidance. The test case is as follows:

"After pressing reset, the SPI LCD module will continuously display some information and switch background colors. The blinking LED indicates that the program is running."

Circuit Diagram

LCD-CIRCUIT

Dependencies

ESP-IDF provides a set of APIs to use SPI. To enable this functionality, the necessary header files must be included:

#include "driver/spi_master.h"

Key Function Analysis

Initialization and Configuration

This function initializes the SPI bus and configures its GPIO pins, clock parameters, and master mode settings. The function prototype is as follows:

esp_err_t spi_bus_initialize(spi_host_device_t host_id,
                    const spi_bus_config_t *bus_config,
                                spi_dma_chan_t dma_chan);

The parameters of this function are described in the table below:

Parameter Description
host_id Specifies the host device ID of the SPI bus
bus_config Pointer to a spi_bus_config_t structure used to configure the SPI bus pins (SCLK, MISO, MOSI) and other parameters
dma_chan Specifies which DMA channel to use. Valid values are: SPI_DMA_CH_AUTO, SPI_DMA_DISABLED, or a number between 1 and 2

Return Value:
ESP_OK indicates successful configuration. Other values indicate configuration failure.

This function uses a spi_bus_config_t structure as an input parameter. Below is the definition of the spi_bus_config_t structure relevant to our use case:

typedef struct {
    int miso_io_num;     /* MISO pin number */
    int mosi_io_num;     /* MOSI pin number */
    int sclk_io_num;     /* Clock pin number */
    int quadwp_io_num;   /* WP pin number for Quad mode, set to -1 if unused */
    int quadhd_io_num;   /* HD pin number for Quad mode, set to -1 if unused */
    int max_transfer_sz; /* Maximum transfer size */
                        /* Other specific configuration parameters */
} spi_bus_config_t;
After completing the configuration of the above structure parameters, the structure can be passed to the spi_bus_initialize function to instantiate the SPI bus.

Device Configuration

This function is used to allocate a device on the SPI bus. The function prototype is as follows:

esp_err_t spi_bus_add_device(spi_host_device_t host_id,
       const spi_device_interface_config_t *dev_config,
                           spi_device_handle_t *handle);
| Parameter | Description | |-----------|-------------| | host_id | Specifies the host device ID of the SPI bus. | | dev_config | A pointer to an spi_device_interface_config_t structure, used to configure the communication parameters of the SPI device, such as clock rate and SPI mode. | | handle | Returns the created device handle. |

Return Value:
- ESP_OK: Configuration was successful.
- Other values indicate a configuration failure.

This function uses spi_host_device_t and spi_device_interface_config_t structured variables to pass configuration parameters to the SPI peripheral device. The definition of the structure is shown as follows:

/**
 * @brief Enumeration of three SPI peripheral devices accessible via software
 */
typedef enum {
    /* SPI1 can only be used as GPSPI on ESP32 */
    SPI1_HOST = 0, /* SPI1 */
    SPI2_HOST = 1, /* SPI2 */
#if SOC_SPI_PERIPH_NUM > 2
    SPI3_HOST = 2, /* SPI3 */
#endif
    SPI_HOST_MAX, /* Invalid host value */
} spi_host_device_t;

typedef struct {
    uint32_t command_bits; /* Number of bits in the command phase */
    uint32_t address_bits; /* Number of bits in the address phase */
    uint32_t dummy_bits;   /* Number of bits in the dummy phase */
    int clock_speed_hz;    /* Clock speed in Hz */
    uint32_t mode;         /* SPI mode (0-3) */
    int spics_io_num;      /* CS pin number */
    ...                    /* Other device-specific configuration parameters */
} spi_device_interface_config_t;

Data Transmission

Based on their functionality, the following functions can be categorized together. Below is a table introducing each function's purpose and parameters.

Function Description
spi_device_transmit() This function sends an SPI transaction, waits for it to complete, and returns the result.
handle: The handle of the device.
trans_desc: A pointer to an spi_transaction_t structure, describing the details of the transaction to be sent.
spi_device_polling_transmit() This function sends a polling transaction, waits for it to complete, and returns the result.
handle: The handle of the device.
trans_desc: A pointer to an spi_transaction_t structure, describing the details of the transaction to be sent.

Code

spi.h

/**
 * @file spi.h
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief 
 * @version 1.0
 * @date 2024-11-18
 * @ref Alientek SPI driver
 * @copyright Copyright (c) 2024
 * 
 */

#ifndef __SPI_H__
#define __SPI_H__

/* Dependencies */
#include <string.h>
#include "esp_log.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"

/* GPIO Definitions */
#define SPI_MOSI_GPIO_PIN   GPIO_NUM_11         /* SPI2_MOSI */
#define SPI_CLK_GPIO_PIN    GPIO_NUM_12         /* SPI2_CLK */
#define SPI_MISO_GPIO_PIN   GPIO_NUM_13         /* SPI2_MISO */

/* Function Prototypes */

/**
 * @brief       Initialize SPI
 * @param       None
 * @retval      None
 */
void spi2_init(void);

/**
 * @brief       Send command via SPI
 * @param       handle : SPI handle
 * @param       cmd    : Command to send
 * @retval      None
 */
void spi2_write_cmd(spi_device_handle_t handle, uint8_t cmd);

/**
 * @brief       Send data via SPI
 * @param       handle : SPI handle
 * @param       data   : Data to send
 * @param       len    : Length of data to send
 * @retval      None
 */
void spi2_write_data(spi_device_handle_t handle, const uint8_t *data, int len); 

/**
 * @brief       Process data via SPI
 * @param       handle       : SPI handle
 * @param       data         : Data to send
 * @retval      t.rx_data[0] : Received data
 */
uint8_t spi2_transfer_byte(spi_device_handle_t handle, uint8_t byte);        

#endif

spi.c

/**
 * @file spi.c
 * @author 
 * @brief 
 * @version 1.0
 * @date 2024-11-18
 * @ref Alientek SPI driver
 * 
 */

#include "spi.h"

/**
 * @brief       Initialize SPI
 * @param       None
 * @retval      None
 */
void spi2_init(void)
{
    esp_err_t ret = 0;
    spi_bus_config_t spi_bus_conf = {0};

    /* SPI bus configuration */
    spi_bus_conf.miso_io_num = SPI_MISO_GPIO_PIN;                               /* SPI_MISO pin */
    spi_bus_conf.mosi_io_num = SPI_MOSI_GPIO_PIN;                               /* SPI_MOSI pin */
    spi_bus_conf.sclk_io_num = SPI_CLK_GPIO_PIN;                                /* SPI_SCLK pin */
    spi_bus_conf.quadwp_io_num = -1;                                            /* SPI write protection signal pin, not enabled */
    spi_bus_conf.quadhd_io_num = -1;                                            /* SPI hold signal pin, not enabled */
    spi_bus_conf.max_transfer_sz = 160 * 80 * 2;                                /* Configure maximum transfer size in bytes */

    /* Initialize SPI bus */
    ret = spi_bus_initialize(SPI2_HOST, &spi_bus_conf, SPI_DMA_CH_AUTO);        /* SPI bus initialization */
    ESP_ERROR_CHECK(ret);                                                       /* Check parameter values */
}

/**
 * @brief       Send command via SPI
 * @param       handle : SPI handle
 * @param       cmd    : Command to send
 * @retval      None
 */
void spi2_write_cmd(spi_device_handle_t handle, uint8_t cmd)
{
    esp_err_t ret;
    spi_transaction_t t = {0};

    t.length = 8;                                       /* Number of bits to transmit (1 byte = 8 bits) */
    t.tx_buffer = &cmd;                                 /* Fill the command */
    ret = spi_device_polling_transmit(handle, &t);      /* Start transmission */
    ESP_ERROR_CHECK(ret);                               /* Usually no issues */
}

/**
 * @brief       Send data via SPI
 * @param       handle : SPI handle
 * @param       data   : Data to send
 * @param       len    : Length of data to send
 * @retval      None
 */
void spi2_write_data(spi_device_handle_t handle, const uint8_t *data, int len)
{
    esp_err_t ret;
    spi_transaction_t t = {0};

    if (len == 0)
    {
        return;                                     /* No data to transmit if length is 0 */
    }

    t.length = len * 8;                             /* Number of bits to transmit (1 byte = 8 bits) */
    t.tx_buffer = data;                             /* Fill the data */
    ret = spi_device_polling_transmit(handle, &t);  /* Start transmission */
    ESP_ERROR_CHECK(ret);                           /* Usually no issues */
}

/**
 * @brief       Process data via SPI
 * @param       handle       : SPI handle
 * @param       data         : Data to send
 * @retval      t.rx_data[0] : Received data
 */
uint8_t spi2_transfer_byte(spi_device_handle_t handle, uint8_t data)
{
    spi_transaction_t t;

    memset(&t, 0, sizeof(t));

    t.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA;
    t.length = 8;
    t.tx_data[0] = data;
    spi_device_transmit(handle, &t);

    return t.rx_data[0];
}
In the spi2_init() function, the primary task is the configuration of SPI parameters, such as SPI pin configuration, data transfer size, and SPI bus settings. This function completes the initialization of the SPI.

Tip

For LCD-related code, please refer to the LCD section.

main.c

/**
 * @file main.c
 * @author SHUAIWEN CUI (SHUAIWEN001@e.ntu.edu.sg)
 * @brief 
 * @version 1.0
 * @date 2024-11-17
 * 
 * @copyright Copyright (c) 2024
 * 
 */

/* Dependencies */
// Basic
#include "esp_system.h"
#include "esp_chip_info.h"
#include "esp_psram.h"
#include "esp_flash.h"
#include "nvs_flash.h"
#include "esp_log.h"

// RTOS
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

// BSP
#include "led.h"
#include "rgb.h"
#include "key.h"
#include "exit.h"
#include "lcd.h"
#include "spi.h"


/**
 * @brief Entry point of the program
 * @param None
 * @retval None
 */
void app_main(void)
{
    uint8_t x = 0;
    esp_err_t ret;


    ret = nvs_flash_init();

    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }

    led_init();
    spi2_init();
    lcd_init();
    vTaskDelay(500);

    while (1)
    {
        switch (x)
        {
            case 0:
            {
                lcd_clear(WHITE);
                break;
            }
            case 1:
            {
                lcd_clear(BLACK);
                break;
            }
            case 2:
            {
                lcd_clear(BLUE);
                break;
            }
            case 3:
            {
                lcd_clear(RED);
                break;
            }
            case 4:
            {
                lcd_clear(MAGENTA);
                break;
            }
            case 5:
            {
                lcd_clear(GREEN);
                break;
            }
            case 6:
            {
                lcd_clear(CYAN);
                break;
            }
            case 7:
            {
                lcd_clear(YELLOW);
                break;
            }
            case 8:
            {
                lcd_clear(BRRED);
                break;
            }
            case 9:
            {
                lcd_clear(GRAY);
                break;
            }
            case 10:
            {
                lcd_clear(LGRAY);
                break;
            }
            case 11:
            {
                lcd_clear(BROWN);
                break;
            }
        }

        lcd_show_string(0, 0, 240, 32, 32, "ESP32", RED);
        lcd_show_string(0, 33, 240, 24, 24, "SPILCD TEST", RED);
        lcd_show_string(0, 60, 240, 16, 16, "CSW@NTU", RED);
        x++;

        if (x == 12)
        {
            x = 0;
        }

        rgb_toggle();
        vTaskDelay(500);
    }
}