Chapter 132: SPI Interface Implementation with ESP-IDF

Chapter Objectives

After completing this chapter, you will be able to:

  • Understand the fundamental principles of the Serial Peripheral Interface (SPI).
  • Explain the roles of Master and Slave devices in SPI communication.
  • Describe different SPI modes (CPOL, CPHA) and their significance.
  • Configure and initialize an SPI bus on ESP32 variants using ESP-IDF.
  • Add and configure an SPI peripheral device to the bus.
  • Perform data transmission and reception using SPI in Master mode.
  • Identify differences in SPI capabilities across various ESP32 family members.
  • Troubleshoot common issues encountered during SPI implementation.
  • Apply learned concepts to interface with SPI-based peripherals.

Introduction

The Serial Peripheral Interface (SPI) is a synchronous serial communication interface specification used for short-distance communication, primarily in embedded systems. It’s a widely adopted standard for connecting microcontrollers to various peripheral devices such as sensors, memory chips (Flash, EEPROM, SRAM), display controllers, analog-to-digital converters (ADCs), digital-to-analog converters (DACs), and even other microcontrollers.

SPI is favored for its simplicity, high throughput, and full-duplex communication capabilities. Understanding SPI is crucial for embedded systems engineers as it unlocks the ability to interface with a vast ecosystem of peripheral components. In this chapter, we will delve into the theory of SPI, explore how to implement it on ESP32 microcontrollers using the ESP-IDF framework, and discuss practical considerations for various ESP32 variants.

Theory

SPI Basics

SPI is a synchronousserialfull-duplexmaster-slave interface. Let’s break down these terms:

  • Synchronous: Communication is synchronized by a clock signal generated by the master device. Both master and slave operate based on this shared clock.
  • Serial: Data is sent one bit at a time over a single data line (in each direction).
  • Full-duplex: Data can be sent and received simultaneously.
  • Master-Slave Architecture:
    • Master: The device that initiates communication, generates the clock signal, and selects the slave device to communicate with. Typically, the microcontroller (e.g., ESP32) acts as the master.
    • Slave: The device that responds to the master. It uses the clock signal from the master and only communicates when selected by the master. Peripherals like sensors or memory chips are typically slaves.

An SPI bus typically uses four logic signals:

  1. SCLK (Serial Clock): Clock signal generated by the master. Data is transferred on specific edges (rising or falling) of this clock.
  2. MOSI (Master Out, Slave In): Data line for sending data from the master to the slave.
  3. MISO (Master In, Slave Out): Data line for sending data from the slave to the master.
  4. CS (Chip Select) or SS (Slave Select): Signal used by the master to select individual slave devices. This line is typically active low. When a master wants to communicate with a specific slave, it asserts (pulls low) the CS line connected to that slave. Only the selected slave will participate in the SPI transaction.

Data Transfer:

When the master wishes to send data to a slave, it first selects the slave by activating its CS line. Then, it generates clock pulses on the SCLK line. On each clock pulse (or a specific edge), the master sends a bit on the MOSI line, and simultaneously, the slave sends a bit on the MISO line. This happens regardless of whether meaningful data is being sent in both directions; effectively, data is shifted out and shifted in. Data is typically shifted out with the Most Significant Bit (MSB) first, but this can sometimes be configurable.

SPI Modes (CPOL and CPHA)

The timing of data transfer relative to the clock signal is defined by two parameters: Clock Polarity (CPOL) and Clock Phase (CPHA). These parameters must be the same for the master and the connected slave device for successful communication. There are four possible SPI modes:

  • CPOL (Clock Polarity): Defines the idle state of the clock signal.
    • CPOL = 0: Clock is low when idle.
    • CPOL = 1: Clock is high when idle.
  • CPHA (Clock Phase): Defines the clock edge on which data is sampled.
    • CPHA = 0: Data is sampled on the leading (first) clock edge, and shifted out on the trailing (second) clock edge of each clock cycle.
    • CPHA = 1: Data is shifted out on the leading (first) clock edge, and sampled on the trailing (second) clock edge of each clock cycle.

The four modes are summarized below:

Mode CPOL CPHA Clock Idle State Data Sampled On Data Shifted On
0 0 0 Low Rising Edge (First active edge) Falling Edge (Second active edge)
1 0 1 Low Falling Edge (Second active edge) Rising Edge (First active edge)
2 1 0 High Falling Edge (First active edge) Rising Edge (Second active edge)
3 1 1 High Rising Edge (Second active edge) Falling Edge (First active edge)

(Note: “Rising Edge” assumes CPOL=0 leads with a rising edge, and CPOL=1 leads with a falling edge. The terms “first active edge” and “second active edge” are more precise.)

It’s crucial to consult the datasheet of the SPI slave device to determine the correct CPOL and CPHA mode it supports.

Advantages of SPI:

  • Full-duplex communication: Data can be sent and received simultaneously.
  • High speed: Generally faster than I2C or UART due to the synchronous clock and simpler protocol. Speeds can reach tens of MHz.
  • Simple protocol: No complex addressing scheme like I2C (slave selection is done via dedicated CS lines).
  • Flexible data size: Not limited to 8-bit data frames; can transfer arbitrary numbers of bits.
  • Low power consumption (relative to parallel interfaces).

Disadvantages of SPI:

  • More pins required: Needs at least four pins (SCLK, MOSI, MISO, CS). Each additional slave requires an extra CS pin on the master.
  • No acknowledgment: The master doesn’t receive an explicit acknowledgment from the slave that data was received correctly (unlike I2C). Error checking must be handled at a higher software level if needed.
  • No flow control: No built-in hardware flow control.
  • Master-driven only: Slaves cannot initiate communication.
  • Shorter distances: Typically used for communication on the same PCB or over very short cables due to signal integrity issues at high speeds.

ESP-IDF SPI Driver Architecture

The ESP-IDF provides a comprehensive SPI Master driver located in the spi_master component. It allows you to configure and use the ESP32’s SPI controllers. The ESP32 series MCUs have multiple SPI controllers, some ofwhich are dedicated to internal flash and PSRAM, while others are available for general-purpose use.

Key Concepts in ESP-IDF SPI Driver:

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    subgraph ESP32 Microcontroller
        direction TB
        MCU_Core[CPU / Application Code] -->|Controls| SPI_Host_Controller{"{SPI Host Controller<br>(e.g., SPI2_HOST, SPI3_HOST)}"}
    end

    SPI_Host_Controller -->|Manages| SPI_Bus["SPI Bus (Physical Lines:<br>MOSI, MISO, SCLK)"]
    
    subgraph "SPI Peripherals (Slaves)"
        direction TB
        SPI_Device1["SPI Slave Device 1<br>(e.g., Sensor)"]
        SPI_Device2["SPI Slave Device 2<br>(e.g., Memory Chip)"]
        SPI_Device3["SPI Slave Device N<br>(e.g., ADC/DAC)"]
    end

    SPI_Bus -->|Shared Lines| SPI_Device1
    SPI_Bus -->|Shared Lines| SPI_Device2
    SPI_Bus -->|Shared Lines| SPI_Device3

    SPI_Host_Controller -.->|Unique CS Line| CS1[CS for Device 1]
    SPI_Host_Controller -.->|Unique CS Line| CS2[CS for Device 2]
    SPI_Host_Controller -.->|Unique CS Line| CSN[CS for Device N]
    
    CS1 --> SPI_Device1
    CS2 --> SPI_Device2
    CSN --> SPI_Device3

    classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px,color:#333,font-family:'Open Sans';
    classDef mcu fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
    classDef host fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
    classDef bus fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
    classDef device fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
    classDef cs fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;

    class MCU_Core,SPI_Host_Controller mcu;
    class SPI_Bus bus;
    class SPI_Device1,SPI_Device2,SPI_Device3 device;
    class CS1,CS2,CSN cs;
  1. SPI Host: Refers to the SPI peripheral controller on the ESP32 (e.g., SPI2_HOSTSPI3_HOST).
  2. SPI Bus: The physical SPI lines (MOSI, MISO, SCLK) associated with an SPI host. A single bus can be shared by multiple slave devices, each selected by a unique CS line.
  3. SPI Device: Represents a slave peripheral connected to the SPI bus. Each device on the bus needs a unique CS line and may have specific configurations (like SPI mode or clock speed).

Core Structures:

spi_bus_config_t: Used to configure the SPI bus itself. It defines the GPIO pins for MOSI, MISO, and SCLK, as well as other bus-level parameters.

C
typedef struct {
    int mosi_io_num;              ///< GPIO pin for Master Out Slave In (=SDO)
    int miso_io_num;              ///< GPIO pin for Master In Slave Out (=SDI)
    int sclk_io_num;              ///< GPIO pin for Serial Clock line
    int quadwp_io_num;            ///< GPIO pin for WP (Write Protect) signal, only available in quad SPI mode. Set to -1 if not used.
    int quadhd_io_num;            ///< GPIO pin for HD (Hold) signal, only available in quad SPI mode. Set to -1 if not used.
    int data2_io_num;             ///< GPIO pin for data2 signal in quad/octal SPI mode. Set to -1 if not used.
    int data3_io_num;             ///< GPIO pin for data3 signal in quad/octal SPI mode. Set to -1 if not used.
    int data4_io_num;             ///< GPIO pin for data4 signal in octal SPI mode. Set to -1 if not used.
    int data5_io_num;             ///< GPIO pin for data5 signal in octal SPI mode. Set to -1 if not used.
    int data6_io_num;             ///< GPIO pin for data6 signal in octal SPI mode. Set to -1 if not used.
    int data7_io_num;             ///< GPIO pin for data7 signal in octal SPI mode. Set to -1 if not used.
    int max_transfer_sz;          ///< Maximum transfer size, in bytes. Defaults to 4094 if 0.
    uint32_t flags;               ///< Abilities of bus to be checked by the driver. Or-ed value of ``SPICOMMON_BUSFLAG_*`` flags.
    int intr_flags;               ///< Interrupt flags. Set to 0 if not used.
    // ... other fields for advanced configurations
} spi_bus_config_t;
Field Name Description Typical Value / Note
mosi_io_num GPIO pin number for Master Out Slave In (MOSI) signal. e.g., 23 (ESP32 DevKitC). Must be a valid GPIO.
miso_io_num GPIO pin number for Master In Slave Out (MISO) signal. e.g., 19 (ESP32 DevKitC). Set to -1 if MISO is not used (simplex Tx).
sclk_io_num GPIO pin number for Serial Clock (SCLK) signal. e.g., 18 (ESP32 DevKitC). Must be a valid GPIO.
quadwp_io_num GPIO pin for Write Protect (WP) signal in Quad SPI mode. Set to -1 for standard SPI.
quadhd_io_num GPIO pin for Hold (HD) signal in Quad SPI mode. Set to -1 for standard SPI.
max_transfer_sz Maximum transfer size in bytes for a single transaction. Defaults to 4094 if 0. Can be adjusted based on DMA capability and application needs. E.g. 32, 64, 4000.
flags Abilities of the bus (e.g., SPICOMMON_BUSFLAG_MASTER). Usually 0 for default master mode, or specific flags if needed.
intr_flags Interrupt flags for the SPI bus. Set to 0 if not using specific interrupt features at bus level.

For standard SPI, you primarily care about mosi_io_nummiso_io_num, and sclk_io_num. The quadwp_io_num and quadhd_io_num are for Quad SPI modes, often used with flash memory, and are set to -1 for standard SPI. max_transfer_sz is important if you need to transfer large chunks of data.

spi_device_interface_config_t: Used to configure a specific SPI slave device that will be attached to the bus. It defines the CS pin, SPI mode, clock speed for this device, etc.

C
typedef struct {
    uint8_t command_bits;           ///< Amount of bits in command phase (0-16), used when ``SPI_DEVICE_HALFDUPLEX`` flag is set.
    uint8_t address_bits;           ///< Amount of bits in address phase (0-64), used when ``SPI_DEVICE_HALFDUPLEX`` flag is set.
    uint8_t dummy_bits;             ///< Amount of dummy bits to insert between address and data phase.
    uint8_t mode;                   ///< SPI mode (0-3).
    uint8_t duty_cycle_pos;         ///< Duty cycle of positive clock, usually 128 for 50% duty cycle.
    uint8_t cs_ena_pretrans;        ///< Amount of SPI bit-cycles the CS should be activated before the transmission.
    uint8_t cs_ena_posttrans;       ///< Amount of SPI bit-cycles the CS should be kept active after the transmission.
    int clock_speed_hz;             ///< Clock speed, in Hz. Set to 0 if not used.
    int input_delay_ns;             ///< Maximum data valid time of slave.
    int spics_io_num;               ///< CS GPIO pin for this device, or -1 if not used.
    uint32_t flags;                 ///< Bitwise OR of ``SPI_DEVICE_*`` flags.
    int queue_size;                 ///< Transaction queue size. This sets how many transactions can be 'in flight' (queued using ``spi_device_queue_trans``) at the same time.
    spi_transaction_cb_t pre_cb;    ///< Callback to be called before a transmission is started.
    spi_transaction_cb_t post_cb;   ///< Callback to be called after a transmission has completed.
} spi_device_interface_config_t;

Field Name Description Typical Value / Note
mode SPI mode (0-3) defining CPOL and CPHA. e.g., 0, 1, 2, or 3. Must match slave device.
clock_speed_hz Clock speed in Hz for this specific slave device. e.g., 1*1000*1000 (1MHz), 10*1000*1000 (10MHz). Limited by slave and board.
spics_io_num GPIO pin number for Chip Select (CS) for this device. e.g., 5. Set to -1 if CS is handled manually or not used (rare).
queue_size Transaction queue size. Number of transactions that can be queued. e.g., 1 for simple polling, 7 for multiple queued transactions.
command_bits Number of bits in the command phase (0-16). Used with SPI_DEVICE_HALFDUPLEX. E.g. 8 for an 8-bit command.
address_bits Number of bits in the address phase (0-64). Used with SPI_DEVICE_HALFDUPLEX. E.g. 24 for a 24-bit address.
dummy_bits Number of dummy bits to insert between address and data phase. Device specific, e.g., 0, 8.
flags Bitwise OR of SPI_DEVICE_* flags. e.g., SPI_DEVICE_HALFDUPLEX, SPI_DEVICE_POSITIVE_CS. Usually 0 for default behavior.
pre_cb / post_cb Callbacks before/after a transaction. NULL if not used. Useful for custom logic like manual CS control.

Key fields include mode (0-3 for CPOL/CPHA), clock_speed_hzspics_io_num (the CS pin for this specific device), and queue_size if using queued transactions.

spi_transaction_t: This structure describes a single SPI transaction (a block of data to be sent and/or received).

C
typedef struct spi_transaction_t {
    uint32_t flags;                 ///< Bitwise OR of ``SPI_TRANS_*`` flags.
    uint16_t cmd;                   /**< Command data, driven on MOSI line before address field.
                                     * Only used when ``SPI_TRANS_USE_CMD`` is set in ``flags``.
                                     */
    uint64_t addr;                  /**< Address data, driven on MOSI line after command field.
                                     * Only used when ``SPI_TRANS_USE_ADDR`` is set in ``flags``.
                                     */
    size_t length;                  ///< Total data length, in bits.
    size_t rxlength;                ///< Total data length received, in bits. May be 0 when transmit-only.
    void *user;                     ///< User-defined variable. Can be used to store context.
    union {
        const void *tx_buffer;      ///< Pointer to transmit buffer, or NULL for no MOSI phase.
        uint8_t tx_data[4];         ///< If ``SPI_TRANS_USE_TXDATA`` is set, data set here is sent directly from this field.
    };
    union {
        void *rx_buffer;            ///< Pointer to receive buffer, or NULL for no MISO phase.
        uint8_t rx_data[4];         ///< If ``SPI_TRANS_USE_RXDATA`` is set, data is received directly to this field.
    };
    // ... other fields for more complex transactions (DC line, etc.)
} spi_transaction_t;


Field Name Description Key Aspect / Usage
flags Bitwise OR of SPI_TRANS_* flags. e.g., SPI_TRANS_USE_TXDATA, SPI_TRANS_USE_RXDATA, SPI_TRANS_MODE_DIO, etc.
cmd Command data (0-16 bits) sent on MOSI before address. Used when SPI_TRANS_USE_CMD flag is set. Requires command_bits in device config.
addr Address data (0-64 bits) sent on MOSI after command. Used when SPI_TRANS_USE_ADDR flag is set. Requires address_bits in device config.
length Total data length of the data phase, in bits. Crucial: Specify in bits, not bytes (e.g., for 4 bytes, length is 32).
rxlength Total data length received, in bits. Set to 0 for transmit-only. For full-duplex, usually same as length. Can differ in half-duplex.
user User-defined variable for context. Can be a pointer to any custom data structure needed in callbacks.
tx_buffer Pointer to the transmit buffer. Data to be sent. NULL if no MOSI phase or using tx_data.
tx_data[4] Small transmit data (up to 4 bytes). Used if SPI_TRANS_USE_TXDATA flag is set. Data sent directly from this array.
rx_buffer Pointer to the receive buffer. Buffer to store received data. NULL if no MISO phase or using rx_data.
rx_data[4] Small receive data (up to 4 bytes). Used if SPI_TRANS_USE_RXDATA flag is set. Data received directly into this array.

For basic transactions, you’ll use length (in bits!), tx_buffer (pointer to data to send), and rx_buffer (pointer to buffer to store received data). If tx_buffer is NULL, MOSI will send all zeros (or high-Z depending on flags). If rx_buffer is NULL, data from MISO is discarded. For full-duplex, both are provided. The length field is for the data phase, while cmd and addr fields can be used for command and address phases common in memory-mapped SPI devices.

Initialization and Usage Flow:

%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TD
    A[Start: Configure SPI] --> B{"Define SPI Host ID<br>(e.g., SPI2_HOST)"};
    B --> C[1- Initialize SPI Bus];
    C --> C1(Configure <i>spi_bus_config_t</i><br>- MOSI, MISO, SCLK pins<br>- max_transfer_sz);
    C1 --> C2{"Call <i>spi_bus_initialize()</i>"};
    C2 -- Success --> D[2- Add SPI Device to Bus];
    C2 -- Error --> Z1[Error Handling: Bus Init Failed];
    
    D --> D1("Configure <i>spi_device_interface_config_t</i><br>- CS pin, SPI mode (0-3)<br>- clock_speed_hz, queue_size");
    D1 --> D2{"Call <i>spi_bus_add_device()</i><br>Get <i>spi_device_handle_t</i>"};
    D2 -- Success --> E[3- Perform Transactions];
    D2 -- Error --> Z2[Error Handling: Add Device Failed];

    E --> E1("Populate <i>spi_transaction_t</i><br>- tx_buffer/rx_buffer<br>- length (in bits!)");
    E --> E_Choice{Choose Transaction Type};
    E_Choice -- Polling --> E_Poll{"Call <i>spi_device_polling_transmit()</i><br>(Blocking)"};
    E_Choice -- Queued --> E_Queue_Start{"Call <i>spi_device_queue_trans()</i><br>(Non-blocking)"};
    E_Choice -- Simple Blocking --> E_Transmit{"Call <i>spi_device_transmit()</i><br>(Internally queues & gets result)"};

    E_Poll -- Done --> E_Result{Transaction Complete?};
    E_Transmit -- Done --> E_Result;
    E_Queue_Start -- Queued --> E_Queue_Wait{"Call <i>spi_device_get_trans_result()</i><br>(Blocking, waits for completion)"};
    E_Queue_Wait -- Done --> E_Result;
    
    E_Result -- Yes --> F("Process Received Data<br>(from rx_buffer / rx_data)");
    E_Result -- Error --> Z3[Error Handling: Transaction Failed];
    
    F --> G{More Transactions with this Device?};
    G -- Yes --> E1;
    G -- No --> H["4- Cleanup (Optional/End of Use)"];

    H --> H1{Need to remove this device?};
    H1 -- Yes --> H2{"Call <i>spi_bus_remove_device()</i>"};
    H1 -- No --> H3{Need to free the bus?};
    H2 --> H3;
    H3 -- Yes --> H4{"Call <i>spi_bus_free()</i>"};
    H3 -- No --> I[End];
    H4 --> I;

    Z1 --> X[Stop/Report Error];
    Z2 --> X;
    Z3 --> X;

    classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px,color:#333,font-family:'Open Sans';
    classDef startEnd fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6; 
    classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF; 
    classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E; 
    classDef io fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46; 
    classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B; 

    class A,I startEnd;
    class B,C,C1,D,D1,E,E1,F,H,H1,H2,H3,H4 process;
    class C2,D2,E_Choice,E_Result,G decision;
    class F io; 
    class Z1,Z2,Z3,X error;
  1. Initialize the SPI Bus:
    • Configure the spi_bus_config_t structure with MOSI, MISO, SCLK pin numbers.
    • Call spi_bus_initialize(spi_host_device_t host_id, const spi_bus_config_t *bus_config, spi_dma_chan_t dma_chan) to initialize the specified SPI host (e.g., SPI2_HOST).
    • dma_chan can be SPI_DMA_CH_AUTO to let the driver automatically select a DMA channel, or SPI_DMA_DISABLED if DMA is not needed (though DMA is often beneficial for performance).
  2. Add an SPI Device to the Bus:
    • Configure the spi_device_interface_config_t structure with device-specific parameters (CS pin, SPI mode, clock speed).
    • Call spi_bus_add_device(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle) to register the slave device with the bus. This will return a spi_device_handle_t which is used for subsequent transactions with this device.
    • You can add multiple devices to the same bus, each with its own configuration and spi_device_handle_t.
  3. Perform Transactions:
    • Populate an spi_transaction_t structure with the data to be sent/received and its length.
    • To perform a transaction and wait for it to complete (polling):esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
    • For more advanced, non-blocking, queued transactions:esp_err_t spi_device_queue_trans(spi_device_handle_t handle, spi_transaction_t *trans_desc, TickType_t ticks_to_wait);Followed by:esp_err_t spi_device_get_trans_result(spi_device_handle_t handle, spi_transaction_t **trans_desc, TickType_t ticks_to_wait);
    • A simpler, but potentially blocking, full-duplex transmit/receive function is:esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);This function internally handles queuing and retrieving the result for a single transaction.
  4. Remove Device and Deinitialize Bus (if no longer needed):
    • esp_err_t spi_bus_remove_device(spi_device_handle_t handle);
    • esp_err_t spi_bus_free(spi_host_device_t host_id);

Practical Examples

Example 1: SPI Master Sending Data (Loopback Test)

This example demonstrates how to configure the ESP32 as an SPI master and send a few bytes of data. For testing purposes, you can create a physical loopback by connecting the MOSI pin to the MISO pin externally. This way, the data sent by the master will be immediately received back.

Hardware Setup (Loopback):

  • Connect GPIO pin designated as MOSI to GPIO pin designated as MISO.
  • Use a logic analyzer on SCLK, MOSI, MISO, and CS pins to observe the signals if available.

Project Setup (VS Code with ESP-IDF Extension):

  1. Create a new ESP-IDF project.
  2. Open main/CMakeLists.txt and ensure spi_master is listed in REQUIRES or PRIV_REQUIRES for the main component, e.g.:idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES spi_master driver esp_log)
  3. Copy the following code into main/main.c.

Code (main/main.c):

C
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_log.h"

static const char *TAG = "SPI_EXAMPLE";

// Define SPI host (HSPI on ESP32, check variant notes for others)
// For ESP32, ESP32-S2, ESP32-S3: SPI2_HOST is often referred to as HSPI_HOST.
// For ESP32-C3, ESP32-C6, ESP32-H2: SPI2_HOST is typically the general-purpose SPI controller.
#define SPI_HOST_ID SPI2_HOST

// Define GPIO pins for SPI communication
// These can be reconfigured to other available GPIOs.
// Make sure these pins are not used by other peripherals (e.g., JTAG, internal flash).
#define PIN_NUM_MOSI 23 // Example for ESP32 DevKitC
#define PIN_NUM_MISO 19 // Example for ESP32 DevKitC
#define PIN_NUM_SCLK 18 // Example for ESP32 DevKitC
#define PIN_NUM_CS   5  // Example for ESP32 DevKitC

// SPI device handle
spi_device_handle_t spi_device;

void app_main(void)
{
    esp_err_t ret;

    ESP_LOGI(TAG, "Initializing SPI bus...");

    // Configuration for the SPI bus
    spi_bus_config_t buscfg = {
        .mosi_io_num = PIN_NUM_MOSI,
        .miso_io_num = PIN_NUM_MISO,
        .sclk_io_num = PIN_NUM_SCLK,
        .quadwp_io_num = -1, // Not used
        .quadhd_io_num = -1, // Not used
        .max_transfer_sz = 32 // Max transfer size in bytes
    };

    // Initialize the SPI bus
    // Using SPI_DMA_CH_AUTO to automatically assign a DMA channel.
    // For ESP32, valid DMA channels for SPI2 (HSPI) are 1 or 2.
    // For ESP32-S2, SPI2_HOST can use DMA channel 1 or 2.
    // For ESP32-S3, SPI2_HOST can use any DMA channel.
    // For C and H series, check datasheets for specific DMA channel availability for SPI2.
    // If DMA is not desired, use SPI_DMA_DISABLED (0).
    ret = spi_bus_initialize(SPI_HOST_ID, &buscfg, SPI_DMA_CH_AUTO);
    ESP_ERROR_CHECK(ret); // Check for errors

    ESP_LOGI(TAG, "SPI bus initialized.");
    ESP_LOGI(TAG, "Adding SPI device...");

    // Configuration for the SPI device
    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 10 * 1000 * 1000, // Clock out at 10 MHz
        .mode = 0,                          // SPI mode 0 (CPOL=0, CPHA=0)
        .spics_io_num = PIN_NUM_CS,         // CS pin
        .queue_size = 7,                    // We want to queue 7 transactions at a time
        //.pre_cb = NULL,                   // Callback before transaction (can be NULL)
        //.post_cb = NULL,                  // Callback after transaction (can be NULL)
    };

    // Attach the device to the SPI bus
    ret = spi_bus_add_device(SPI_HOST_ID, &devcfg, &spi_device);
    ESP_ERROR_CHECK(ret);
    ESP_LOGI(TAG, "SPI device added.");

    // Prepare transaction data
    char send_buffer[32] = "Hello SPI from ESP32!";
    char recv_buffer[32] = {0}; // Initialize with zeros

    spi_transaction_t t;
    memset(&t, 0, sizeof(t)); // Zero out the transaction structure
    t.length = strlen(send_buffer) * 8; // Length is in bits
    t.tx_buffer = send_buffer;
    t.rx_buffer = recv_buffer;
    // For loopback, MOSI is connected to MISO.
    // So, what we send on tx_buffer should appear on rx_buffer.

    ESP_LOGI(TAG, "Performing SPI transaction...");
    ESP_LOGI(TAG, "Sending: %s", send_buffer);

    // Perform the SPI transaction (blocking)
    // spi_device_transmit is a wrapper around spi_device_queue_trans and spi_device_get_trans_result.
    ret = spi_device_transmit(spi_device, &t);
    ESP_ERROR_CHECK(ret); // Check for errors

    ESP_LOGI(TAG, "SPI transaction completed.");

    // For loopback, rx_buffer should now contain what was sent.
    // Note: The first few bytes received might be garbage if the slave wasn't ready or
    // if MISO was floating before the transaction started.
    // In a real loopback, it should match.
    ESP_LOGI(TAG, "Received: %s (Length: %d bits, expected %d bits)", recv_buffer, t.rxlength, t.length);

    // Validate received data (optional, for loopback)
    if (memcmp(send_buffer, recv_buffer, strlen(send_buffer)) == 0) {
        ESP_LOGI(TAG, "Loopback test successful! Sent and received data match.");
    } else {
        ESP_LOGW(TAG, "Loopback test failed or partial match.");
        // Print hex for debugging
        ESP_LOG_BUFFER_HEXDUMP(TAG, send_buffer, strlen(send_buffer), ESP_LOG_INFO);
        ESP_LOG_BUFFER_HEXDUMP(TAG, recv_buffer, t.rxlength / 8, ESP_LOG_INFO);
    }

    // To send more data, repeat the transaction process:
    // 1. Prepare spi_transaction_t
    // 2. Call spi_device_transmit() or spi_device_polling_transmit()

    // Example of using spi_device_polling_transmit (simpler for one-off transactions)
    // This function doesn't use the queue set in devcfg.queue_size.
    // It's a blocking call.
    char another_message[] = "Polling Test";
    memset(&t, 0, sizeof(t));
    t.length = sizeof(another_message) * 8; // Include null terminator for string
    t.tx_buffer = another_message;
    t.rx_buffer = recv_buffer; // Re-use recv_buffer or use another

    ESP_LOGI(TAG, "Performing another SPI transaction using polling_transmit...");
    ESP_LOGI(TAG, "Sending: %s", another_message);
    ret = spi_device_polling_transmit(spi_device, &t);
    ESP_ERROR_CHECK(ret);
    ESP_LOGI(TAG, "Polling transaction completed.");
    ESP_LOGI(TAG, "Received (polling): %s", recv_buffer);


    // When done, you might want to remove the device and free the bus
    // This is usually done if the SPI bus is no longer needed during runtime.
    // For many applications, the bus is initialized once and used throughout.
    // ESP_LOGI(TAG, "Removing SPI device...");
    // ret = spi_bus_remove_device(spi_device);
    // ESP_ERROR_CHECK(ret);
    // ESP_LOGI(TAG, "Freeing SPI bus...");
    // ret = spi_bus_free(SPI_HOST_ID);
    // ESP_ERROR_CHECK(ret);

    ESP_LOGI(TAG, "SPI example finished.");
}

Build Instructions (VS Code):

  1. Connect your ESP32 board to your computer.
  2. In VS Code, ensure the correct ESP-IDF target (e.g., esp32, esp32s3) and COM port are selected.
  3. Click the “Build” button (typically a gear icon or Ctrl+E B).
  4. If the build is successful, click the “Flash” button (Ctrl+E F).
  5. Click the “Monitor” button (Ctrl+E M) to view the serial output.

Run/Flash/Observe:

  • After flashing, open the ESP-IDF Monitor.
  • You should see log messages indicating SPI bus initialization, device addition, and the data being sent.
  • If you have connected MOSI to MISO (loopback), the “Received” data should match the “Sent” data.
  • If you have a logic analyzer, you can observe the SCLK, MOSI, MISO, and CS signals to verify the SPI communication waveforms, clock speed, and data.

Tip: For the loopback test to work reliably, ensure a good connection between MOSI and MISO. Without a loopback or a connected slave device, the recv_buffer will contain indeterminate data (whatever is on the MISO line, which might be floating or noise).

Variant Notes

The ESP32 family offers several SPI controllers. The exact number and their typical usage can vary:

  • ESP32:
    • Features 4 SPI controllers: SPI0, SPI1, SPI2 (HSPI), SPI3 (VSPI).
    • SPI0/SPI1: Typically used internally for accessing the integrated flash memory and PSRAM. Direct use by applications is complex and generally not recommended unless you are an advanced user.
    • SPI2 (HSPI) & SPI3 (VSPI): General-purpose SPI controllers available for application use. In ESP-IDF, these are typically referred to by SPI2_HOST and SPI3_HOST respectively.
    • Default IOMUX pins for SPI2 (HSPI): CS0 (GPIO15), SCLK (GPIO14), MISO (GPIO12), MOSI (GPIO13).
    • Default IOMUX pins for SPI3 (VSPI): CS0 (GPIO5), SCLK (GPIO18), MISO (GPIO19), MOSI (GPIO23).
    • All SPI signals can be routed to other GPIOs via the GPIO matrix.
  • ESP32-S2:
    • Features 4 SPI controllers: SPI0, SPI1, SPI2, SPI3.
    • SPI0/SPI1: Used for flash and PSRAM.
    • SPI2 & SPI3: General-purpose SPI controllers (SPI2_HOSTSPI3_HOST).
    • Pins are configurable via GPIO matrix. Check datasheet for default IOMUX pins if any, but GPIO matrix is flexible.
  • ESP32-S3:
    • Features 4 SPI controllers: SPI0, SPI1, SPI2, SPI3.
    • SPI0/SPI1: Used for flash and PSRAM (supports Octal SPI).
    • SPI2 & SPI3: General-purpose SPI controllers (SPI2_HOSTSPI3_HOST).
    • Pins are configurable via GPIO matrix.
  • ESP32-C3:
    • Features 2 main SPI controllers (SPI0, SPI1) primarily for flash, and one general-purpose SPI controller (SPI2).
    • SPI0/SPI1: Used for flash access.
    • SPI2 (FSPI): General-purpose SPI controller, referred to as SPI2_HOST.
    • Pins are configurable via GPIO matrix. Default pins for SPI2 are often: CS0 (GPIO10), SCLK (GPIO6), MISO (GPIO2), MOSI (GPIO7). Always verify with the specific module/board documentation and datasheet.
  • ESP32-C6:
    • Features 3 SPI controllers: SPI0, SPI1, SPI2 (GPSPI).
    • SPI0: Cache, can access flash.
    • SPI1: MSPI, for flash/PSRAM access.
    • SPI2 (GPSPI): General-purpose SPI controller, referred to as SPI2_HOST.
    • Pins are configurable via GPIO matrix.
  • ESP32-H2:
    • Features 3 SPI controllers: SPI0, SPI1, SPI2 (GPSPI).
    • SPI0: Cache, can access flash.
    • SPI1: MSPI, for flash/PSRAM access.
    • SPI2 (GPSPI): General-purpose SPI controller, referred to as SPI2_HOST.
    • Pins are configurable via GPIO matrix.
ESP32 Variant Total SPI Controllers General Purpose Controllers (ESP-IDF Host Name) Notes / Typical Internal Use
ESP32 4 (SPI0, SPI1, SPI2, SPI3)
  • SPI2_HOST (HSPI)
  • SPI3_HOST (VSPI)
SPI0/SPI1 typically for Flash/PSRAM. Pins configurable via GPIO Matrix.
ESP32-S2 4 (SPI0, SPI1, SPI2, SPI3)
  • SPI2_HOST
  • SPI3_HOST
SPI0/SPI1 for Flash/PSRAM. Pins configurable via GPIO Matrix.
ESP32-S3 4 (SPI0, SPI1, SPI2, SPI3)
  • SPI2_HOST
  • SPI3_HOST
SPI0/SPI1 for Flash/PSRAM (Octal SPI support). Pins configurable via GPIO Matrix.
ESP32-C3 3 (SPI0, SPI1, SPI2)
  • SPI2_HOST (FSPI)
SPI0/SPI1 for Flash. SPI2 is general purpose. Pins configurable via GPIO Matrix. Fewer GP SPI controllers.
ESP32-C6 3 (SPI0, SPI1, SPI2)
  • SPI2_HOST (GPSPI)
SPI0 (Cache/Flash access), SPI1 (MSPI for Flash/PSRAM). SPI2 is general purpose. Pins configurable.
ESP32-H2 3 (SPI0, SPI1, SPI2)
  • SPI2_HOST (GPSPI)
SPI0 (Cache/Flash access), SPI1 (MSPI for Flash/PSRAM). SPI2 is general purpose. Pins configurable.

Note: Always consult the specific datasheet for your ESP32 variant and module for the most accurate information. SPI_DMA_CH_AUTO is generally recommended for DMA channel selection.

General Recommendations:

  • Always use SPI2_HOST or SPI3_HOST (if available on the variant) for interfacing with custom peripherals.
  • When choosing GPIO pins, ensure they are not used by other critical functions (e.g., JTAG, strapping pins in a conflicting state, UART0 for logging if you need it).
  • Refer to the specific datasheet for your ESP32 variant and module for the most accurate pin information and SPI controller capabilities.
  • The SPI_DMA_CH_AUTO option for spi_bus_initialize is generally recommended as it simplifies DMA channel selection. DMA significantly offloads the CPU during SPI transactions. If DMA is not desired or causes issues, SPI_DMA_DISABLED (or 0) can be used.

Common Mistakes & Troubleshooting Tips

Mistake / Issue Symptom(s) Troubleshooting / Solution
Incorrect Pin Assignments
  • No SCLK signal observed.
  • No data on MOSI/MISO.
  • Device unresponsive.
  • ESP32 crashes or behaves erratically (if critical pins like JTAG are misused).
  • Verify Datasheet: Double-check ESP32 variant and development board datasheet for correct, available GPIOs.
  • Check spi_bus_config_t: Ensure mosi_io_num, miso_io_num, sclk_io_num are correct.
  • Check spi_device_interface_config_t: Ensure spics_io_num is correct for the target slave.
  • Logic Analyzer: Use a logic analyzer to probe the assigned pins for activity.
  • Avoid Reserved Pins: Ensure pins are not used by Flash (SPI0/1), JTAG, or critical strapping pins.
Mismatched SPI Mode (CPOL/CPHA)
  • Data consistently garbled or shifted.
  • Receiving all 0xFF or 0x00.
  • Intermittent communication.
  • Consult Slave Datasheet: Find the required SPI mode (0, 1, 2, or 3) for the slave device.
  • Set Correct Mode: Update the mode field in spi_device_interface_config_t to match the slave.
  • Test All Modes: If unsure, systematically try all four modes.
Clock Speed Too High
  • No response from slave.
  • Garbled data, especially with longer transmissions.
  • Works at lower speeds but fails at higher speeds.
  • Check Slave Max Speed: Refer to the slave device’s datasheet for its maximum supported SPI clock frequency.
  • Start Low: Begin with a conservative clock speed (e.g., 100000 Hz or 1000000 Hz) in clock_speed_hz.
  • Gradually Increase: If stable, incrementally increase speed.
  • Wiring Quality: Ensure short, direct wires. Breadboards and long wires limit speed due to signal integrity issues (capacitance, inductance, noise). Use twisted pairs or shielded cables for off-board connections if possible.
Incorrect Chip Select (CS) Handling
  • Slave device does not respond.
  • Multiple slaves on the bus interfere with each other.
  • Data meant for one slave is received by another or none.
  • Verify spics_io_num: Ensure it’s correctly set to the GPIO connected to the slave’s CS pin in spi_device_interface_config_t.
  • Driver Manages CS: Let the ESP-IDF SPI driver manage CS assertion/de-assertion. Avoid manual GPIO control of CS unless for very specific reasons (and then use pre_cb/post_cb carefully).
  • Unique CS for Each Slave: If multiple slaves share the bus, each must have a unique CS pin and its own spi_device_handle_t.
  • Active Low/High: Most devices use active-low CS. If a device uses active-high, you might need the SPI_DEVICE_POSITIVE_CS flag or to invert CS logic manually (less common with driver).
Transaction Length/Buffer Issues
  • Incomplete data sent/received.
  • ESP32 crashes (buffer overflows/underflows, invalid pointers).
  • Unexpected data values.
  • Length in Bits: Remember spi_transaction_t::length and spi_transaction_t::rxlength are in bits, not bytes. (e.g., 8 bytes = 64 bits).
  • Valid Buffers: Ensure tx_buffer and rx_buffer point to valid, sufficiently large memory regions.
  • Initialize Buffers: memset receive buffers if expecting known fill patterns on no data.
  • NULL Pointers: If only sending, rx_buffer can be NULL. If only receiving (after a command), tx_buffer can be NULL (driver sends zeros).
  • DMA Alignment: If using DMA, buffers might need to be DMA-capable (e.g., allocated from heap_caps_malloc(…, MALLOC_CAP_DMA)). Usually not an issue for stack-allocated buffers if small.
Floating MISO Line / No Slave Connected
  • Reading all 0xFF or random noise on MISO.
  • Check Connections: Ensure the slave device is properly connected, especially the MISO line.
  • Pull-up/Pull-down: The MISO line on the ESP32 side might need an external pull-up or pull-down resistor if the slave device doesn’t drive it strongly or when no slave is selected/active. ESP-IDF might configure internal pulls; check pin configuration.
  • Loopback Test: Perform a MOSI-MISO loopback test on the ESP32 itself to verify master functionality.

Warning: When using breadboards for SPI, especially at higher speeds (>1 MHz), be mindful of loose connections, parasitic capacitance, and crosstalk. Keep wires short and neat. For production, a PCB is essential for reliable SPI communication.

Exercises

  1. Modify Clock Speed and Observe:
    • Take the provided loopback example. Modify the clock_speed_hz in spi_device_interface_config_t to various values (e.g., 100 kHz, 1 MHz, 5 MHz, 20 MHz).
    • If you have a logic analyzer, observe how the SCLK frequency changes and if the data transfer remains successful. Note the maximum speed at which your loopback setup works reliably.
  2. Interface with a Real SPI Device (Conceptual):
    • Choose a common SPI peripheral (e.g., MCP23S17 SPI I/O expander, ADXL345 accelerometer, or a simple SPI EEPROM like 25LCxx series).
    • Study its datasheet: find its SPI mode, command structure, and how to read/write data (e.g., read device ID, read/write a register).
    • Write new ESP-IDF code to initialize the SPI bus and this device.
    • Implement functions to send necessary commands and read/write data according to the device’s protocol. (You don’t need the actual hardware to write the code structure, but it’s best if you can test it).
  3. Master-Slave Communication (Two ESP32s):
    • This is more advanced. If you have two ESP32 boards:
      • Configure one ESP32 as SPI Master (using the spi_master driver).
      • Configure the second ESP32 as an SPI Slave. This requires using the spi_slave driver (driver/spi_slave.h). The slave needs to be initialized to listen for transactions.
      • Implement a simple protocol where the master sends a command byte, and the slave responds with a predefined data byte or a status.
    • Self-study hint: Look into spi_slave_interface_config_t and spi_slave_transaction_t.
  4. Reading from SPI Flash (External):
    • If you have an external SPI flash chip (e.g., W25Q32), try to read its JEDEC ID.
    • Most SPI flash chips have a command (e.g., 0x9F) to read the Manufacturer ID, Device ID, etc.
    • You’ll need to send this command byte and then read back a few bytes of response.
    • Structure your spi_transaction_tcmd field might not be directly usable here if the command itself is part of the tx_buffer. You’d set tx_buffer to [0x9F]length to 8 bits for sending the command. Then, in a separate transaction or by setting rxlength appropriately in a full-duplex transaction, read the response into rx_buffer. Some devices require CS to stay active between command and data read. The SPI_DEVICE_HALFDUPLEX flag along with command_bitsaddress_bits in spi_device_interface_config_t and cmdaddr in spi_transaction_t can be useful for structured transfers like these.
  5. Explore Transaction Flags:
    • Investigate the flags field in spi_transaction_t (e.g., SPI_TRANS_USE_TXDATASPI_TRANS_USE_RXDATA).
    • Modify the example to send/receive small amounts of data (up to 4 bytes) directly using tx_data and rx_data arrays within the transaction structure instead of external buffers. This can be convenient for short, fixed-size transfers.

Summary

  • SPI is a synchronous, serial, full-duplex, master-slave communication protocol widely used for connecting microcontrollers to peripherals.
  • It uses SCLK (clock), MOSI (master out), MISO (master in), and CS (chip select) lines.
  • SPI communication timing is defined by CPOL (clock polarity) and CPHA (clock phase), resulting in four modes (0-3).
  • The ESP-IDF provides a robust spi_master driver for configuring ESP32 as an SPI master.
  • Key steps: Initialize SPI bus (spi_bus_initialize), add SPI device (spi_bus_add_device), perform transactions (spi_device_transmit or spi_device_polling_transmit using spi_transaction_t).
  • Different ESP32 variants offer varying numbers of SPI controllers; SPI2_HOST and SPI3_HOST are generally available for user applications.
  • Proper pin configuration, SPI mode matching, appropriate clock speed, and correct CS handling are crucial for successful SPI communication.
  • DMA can be used with SPI to offload CPU and improve performance, often enabled by default with SPI_DMA_CH_AUTO.

Further Reading

  • ESP-IDF SPI Master Driver Documentation:
  • ESP32 Technical Reference Manual (TRM):
    • Refer to the TRM for your specific ESP32 variant for in-depth details on the SPI controller hardware. (e.g., ESP32 TRM)
  • Application Notes and Examples:
    • Explore the examples directory in your ESP-IDF installation ($IDF_PATH/examples/peripherals/spi_master).

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top