ESP32 SPI API under the Ardino IDE

First try posting a tutorial be brutally honest.

I am a fan of the ESP32.

In search for faster IMU transfer speeds, I drifted over to using SPI. I had been using the ESP32 AD APi for quite sometime for greater performance. I figured, correctly, that by using the ESP32 SPi APi I might get faster SPI speeds.

The user should understand how to split and reassemble 16 or 32 bit data into byte chunks.

Using the ESP32 SPI API.

Why? Simply put, more control and better speeds.

The ESP32 has 4 SPI hardware modules and each module can service up to 3 SPI devices per channel. Two of the SPI modules are available for the ESP32 user. The ESP32 SPI modules that can be used are known as HSPI and VSPI.

The SPI hardware modules or SPI channels are configured and initialized first. The the channel is then configured for device use.

The ESP32 SPI channels VSPI and HSPI should be assigned GPIO pins on the ESP32 portA (GPIO_NUM-0 to GPIO_NUM_32). It’s not a good idea to put a SPI device on portB GPIO pins due to portsB’s design.

The default SPI channel is channel VSPI. VSPI uses the following ESP32 (WROOM) pinouts:
SPI_MOSI = GPIO_NUM_23
SPI_MISO = GPIO_NUM_19
SPI_SCK = GPIO_NUM_18
SPI_CS = GPIO_NUM_5

The natural HSPI pins for the ESP32 are as follows:
SPI_MOSI = GPIO_NUM_13
SPI_MISO = GPIO_NUM_12 <<< a.k.a. the TDI pin
SPI_SCK = GPIO_NUM_14 <<< a.k.a. the TMS pin
SPI_CS = GPIO_NUM_15

Be aware, the trick to using HSPI is that the SPI device cannot send data before its requested. During program load the ESP 32 program loaded will use GPIO_NUM_12 and GPIO_NUM_14. GPIO_NUM_12 and GPIO_NUM_14 must not be made to randomly change state during program load. I know, not a SPI device but its an easy example to use, a GPS unit with its tx pin connected to GPIO_NUM_12 can cause the program load to fail if the GPS just starts sending data as soon as its energized.

Of course, with the ESP32, any pin function can be reassigned to most other pins.

The ESP32 SPI module is an independently operating module that does not require CPU intervention to operate. The ESP32 SPI can be ran in full duplex mode and can send and receive data in the background. The ESP32 SPI module uses freeRTOS Queue’s.

The following h and cpp items will be used in this tutorial.

ESP32_SPI_API.cpp

#include "ESP32_SPI_API.h"
/////////////////////////////
///////////////////////////
uint8_t txData[2] = { };
uint8_t rxData[25] = { };
uint8_t low;
int8_t high;
//////
//////////////////////////////////
uint8_t GetLowBits()
{
  return low;
}
int8_t GetHighBits()
{
  return high;
}
////////////////////////////////////////
int fInitializeSPI_Channel( int spiCLK, int spiMOSI, int spiMISO, spi_host_device_t SPI_Host, bool EnableDMA)
{
  esp_err_t intError;
  spi_bus_config_t bus_config = { };
  bus_config.sclk_io_num = spiCLK; // CLK
  bus_config.mosi_io_num = spiMOSI; // MOSI
  bus_config.miso_io_num = spiMISO; // MISO
  bus_config.quadwp_io_num = -1; // Not used
  bus_config.quadhd_io_num = -1; // Not used
  intError = spi_bus_initialize( HSPI_HOST, &bus_config, EnableDMA) ;
  return intError;
}
//////
int fInitializeSPI_Devices( spi_device_handle_t &h, int csPin)
{
  esp_err_t intError;
  spi_device_interface_config_t dev_config = { };  // initializes all field to 0
  dev_config.address_bits     = 0;
  dev_config.command_bits     = 0;
  dev_config.dummy_bits       = 0;
  dev_config.mode             = 3;
  dev_config.duty_cycle_pos   = 0;
  dev_config.cs_ena_posttrans = 0;
  dev_config.cs_ena_pretrans  = 0;
  dev_config.clock_speed_hz   = 5000000;
  dev_config.spics_io_num     = csPin;
  dev_config.flags            = 0;
  dev_config.queue_size       = 1;
  dev_config.pre_cb           = NULL;
  dev_config.post_cb          = NULL;
  spi_bus_add_device(HSPI_HOST, &dev_config, &h);
  // return intError;
  // return h;
} // void fInitializeSPI_Devices()
///////////////////////////////////////////////////////////////
int fReadSPIdata16bits( spi_device_handle_t &h, int _address )
{
  uint8_t address = _address;
    esp_err_t intError = 0;
    low=0; high=0;
    spi_transaction_t trans_desc;
    trans_desc = { };
    trans_desc.addr =  0;
    trans_desc.cmd = 0;
    trans_desc.flags = 0;
    trans_desc.length = (8 * 3); // total data bits
    trans_desc.tx_buffer = txData;
    trans_desc.rxlength = 8 * 2 ; // Number of bits NOT number of bytes
    trans_desc.rx_buffer = rxData;
    txData[0] = address | 0x80;
    intError = spi_device_transmit( h, &trans_desc);
    low = rxData[0]; high = rxData[1];
  //  if ( intError != 0 )
  //  {
  //    log_i ( " Transmitting error = %s" esp_err_to_name(intError) );
  //  }
  return intError;
} // void fSendSPI( uint8_t count, uint8_t address, uint8_t DataToSend)
////
int fWriteSPIdata8bits( spi_device_handle_t &h, int _address, int _sendData )
{
  uint8_t address =  _address;
  uint8_t sendData = _sendData;
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 2); // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = 0 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = NULL;
  txData[0] = address  & 0x7F;
  txData[1] = sendData;
  intError = spi_device_transmit( h, &trans_desc);
  return intError;
//  //  if ( intError != 0 )
//  //  {
//    log_i( " LSM9DS1_REGISTER_CTRL_REG6_XL. Transmitting error = %s", esp_err_to_name(intError) );
//  //  }
} // void fWriteSPIdata8bits(  spi_device_handle_t &h, uint8_t address, uint8_t sendData )
//

ESP32_SPI_API.h file for the cpp

#include <driver/spi_master.h>
#include "sdkconfig.h"
#include "esp_system.h" //This inclusion configures the peripherals in the ESP system.
////////////////////////////////////
//
//#define MAGTYPE  true
//#define XGTYPE   false
//////////////////////////
///////////////////////////
//
////////////////////////////
 uint8_t GetLowBits();
 int8_t GetHighBits();
 int fReadSPIdata16bits( spi_device_handle_t &h, int address );
 int fWriteSPIdata8bits( spi_device_handle_t &h, int address, int sendData );
 int fInitializeSPI_Devices( spi_device_handle_t &h, int csPin);
// spi_device_handle_t fInitializeSPI_Devices( int csPin);
int fInitializeSPI_Channel( int spiCLK, int spiMOSI, int spiMISO, spi_host_device_t SPI_Host, bool EnableDMA);

To begin using the ESP32 SPI API the driver/spi_master.h header file needs to be included in a project; see ESP32_SPI_API.h.

The first 2 declarations of variables, see ESP32_SPI_API.cpp, are the receive and transmit buckets. The variables uint8_t txData[2] = { }; and uint8_t rxData[25] = { }; are to be used as transmission and receive buckets.

The device that will receive the SPI data receives up to, 16 bits and determined, from reading a device spec sheet, the length of the longest message to be received, with a few added bytes for safety.

The SPI devices sends data to the MCU in 8 bit chunks. The variables uint8_t low; and int8_t high; will be used to store the extracted SPi data from the device and serve to facilitate reassembling the numbers into 16 bit numbers. If signed values are transferred then changing uint8_t to int8_t may in order.

Starting the show begins with initializing the SPI channel.

int fInitializeSPI_Channel( int spiCLK, int spiMOSI, int spiMISO, spi_host_device_t SPI_Host, bool EnableDMA)
{
  esp_err_t intError;
  spi_bus_config_t bus_config = { };
  bus_config.sclk_io_num = spiCLK; // CLK
  bus_config.mosi_io_num = spiMOSI; // MOSI
  bus_config.miso_io_num = spiMISO; // MISO
  bus_config.quadwp_io_num = -1; // Not used
  bus_config.quadhd_io_num = -1; // Not used
  intError = spi_bus_initialize( HSPI_HOST, &bus_config, EnableDMA) ;
  return intError;
}

There is a gottcha with using the ESP32 SPI API under the Arduino IDE. This does not work

 spi_bus_config_t buscfg={
        .miso_io_num = PIN_NUM_MISO,
        .mosi_io_num = PIN_NUM_MOSI,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 32,
    };

Many ESP32 SPi API examples show that the way to setup the configuration structure, see above. This methood, simply, does not work in the Arduino IDE.

More importantly the ESP32 has a 'bug' with declared structures. When a structure is declared, on an ESP32, the memory is set aside for structure storage but the structure elements are not cleared or set to default values, automatically.

By declaring a structure and explicitly cause the structures default values to be set is the only way to make sure that the structure is not filled with some random values; spi_bus_config_t bus_config = { };

After the SPI bus configuration channel is initialized, the SPI pin numbers are assigned and an expected max byte transfer size is declared, the SPI channel configuration is used to configure a SPI buss channel; spi_bus_initialize( HSPI_HOST, &bus_config, EnableDMA); that will be used.

Once the channel is initialized then the SPI module interface can be configured.

int fInitializeSPI_Devices( spi_device_handle_t &h, int csPin)
{
  esp_err_t intError;
  spi_device_interface_config_t dev_config = { };  // initializes all field to 0
  dev_config.address_bits     = 0;
  dev_config.command_bits     = 0;
  dev_config.dummy_bits       = 0;
  dev_config.mode             = 3;// for DMA, only 1 or 3 is available
  dev_config.duty_cycle_pos   = 0;
  dev_config.cs_ena_posttrans = 0;
  dev_config.cs_ena_pretrans  = 0;
  dev_config.clock_speed_hz   = 5000000;
  dev_config.spics_io_num     = csPin;
  dev_config.flags            = 0;
  dev_config.queue_size       = 1;
  dev_config.pre_cb           = NULL;
  dev_config.post_cb          = NULL;
  spi_bus_add_device(HSPI_HOST, &dev_config, &h);
  // return intError;
  // return h;
} // void fInitializeSPI_Devices()

The items of address_bits, command_bits, and dummy_bits can be set here for all SPI transmissions or can be set, at a later time, on a per data transfer basis. As can be seen the SPI DMA mode is set for 3 which is to use DMA if and when available. A clock speed is defined as is the chip select pin. If the attached device fails with a DMS setting of 3 then the SPI module will fall back, so it's worth a try.

Importantly with the setting of the SPI configuration, spi_bus_add_device(HSPI_HOST, &dev_config, &h);, a handle (&h) is returned that will be used to reference the SPI module in use.

The queue (freeRTOS) size setting is for both send and receive queues. If 2 is set there will be 2 send queues and 2 receive queues made available to the SPI module.

Settings not mentioned can be reviewed by going to the ESP32 API link in the first posting.

When developing, initially, for the SPI API the error message can be very handy in getting to the root of issues.

Now for sending data.

int fWriteSPIdata8bits( spi_device_handle_t &h, int _address, int _sendData )
{
  uint8_t address =  _address;
  uint8_t sendData = _sendData;
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 2); // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = 0 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = NULL;
  txData[0] = address  & 0x7F;
  txData[1] = sendData;
  intError = spi_device_transmit( h, &trans_desc);
  return intError;
  //  if ( intError != 0 )
  //  {
    log_i( " LSM9DS1_REGISTER_CTRL_REG6_XL. Transmitting error = %s", esp_err_to_name(intError) );
  //  }
} // void fWriteSPIdata8bits(  spi_device_handle_t &h, uint8_t address, uint8_t sendData )

Initially, comment out the return intError; and uncomment the following:

    if ( intError != 0 )
    {
       log_i( " LSM9DS1_REGISTER_CTRL_REG6_XL. Transmitting error = %s", esp_err_to_name(intError) );
    }

for troubleshooting. The macro esp_err() will be of great benifit to obtaining human readable messages from the ESP32.

To send ESP32 SPi data several items are needed; int fWriteSPIdata8bits( spi_device_handle_t &h, int _address, int _sendData ). Those items are the SPi device handle, the register address or instruction of/for the device and the data to be sent. In this case only 8 bit data will be sent so their will be no requirement to break the data down into bytes.

A SPI packet is sent by using a transaction structure spi_transaction_t trans_desc; and trans_desc = { }; creates and initializes the transaction structure.

The transaction requires the total number of bytes that will be transmitted; trans_desc.length = (8 * 2); // total data bits. 8 bits of register or command data and 8 bits of data.

The data to be sent is assigned to the transaction structure; trans_desc.tx_buffer = txData;.

The receive buffers are set trans_desc.rxlength = 0 ; // Number of bits NOT number of bytes trans_desc.rx_buffer = NULL;; as their will be no data received those lines are zeroed and nulled. This lets the module know not to wait for a reply.

The data is assigned to the sending data buffer txData[0] = address & 0x7F; txData[1] = sendData; and, finally the data is sent intError = spi_device_transmit( h, &trans_desc); using the data and SPi handle.


The macro esp_err_to_name(intError) converts many ESP32 error numbers into something readable.

To receive data

int fReadSPIdata16bits( spi_device_handle_t &h, int _address )
{
  uint8_t address = _address;
    esp_err_t intError = 0;
    low=0; high=0;
    spi_transaction_t trans_desc;
    trans_desc = { };
    trans_desc.addr =  0;
    trans_desc.cmd = 0;
    trans_desc.flags = 0;
    trans_desc.length = (8 * 3); // total data bits
    trans_desc.tx_buffer = txData;
    trans_desc.rxlength = 8 * 2 ; // Number of bits NOT number of bytes
    trans_desc.rx_buffer = rxData;
    txData[0] = address | 0x80;
    intError = spi_device_transmit( h, &trans_desc);
    low = rxData[0]; high = rxData[1];
  //  if ( intError != 0 )
  //  {
  //    log_i( " WHO I am LSM9DS1. Transmitting error = %s" , esp_err_to_name(intError) );
  //  }
  return intError;

Initially comment/uncomment the code to allow error messages to be printed.

Using the SPI handle and address of the device to be read a request of data from the device can be made. The data will be transmitted from the device in 8 bit chunks.

The variables low=0; high=0; will be used to hold the low 8 bits and the high 8 bits. The bit total is set trans_desc.length = (8 * 3); // total data bits. A receive buffer is assigned trans_desc.tx_buffer = txData; and the number of expected bits are declared trans_desc.rxlength = 8 * 2 ; // Number of bits NOT number of bytes. The requested data ius assigned txData[0] = address | 0x80; and a request for data is sent intError = spi_device_transmit( h, &trans_desc);.

Once the a request for data is sent 2 things can be done. Either allow background communications to take place or wait for the data to be returned. In this case, the code will wait for a data return and place the data into the proper buckets low = rxData[0]; high = rxData[1];

To receive SPI Queued data, a SPI read transaction is carried out, the queued data will arrive just like the process of request and wait but their will be no waiting for the device to send.

An added extra

int fWriteSPIdata32bits( spi_device_handle_t &h, int _sendData0, int _sendData1, int _sendData2, int _sendData3 )
{
  // uint8_t address =  _address;
  // uint8_t sendData = _sendData;
  esp_err_t intError;
  spi_transaction_t trans_desc;
  trans_desc = { };
  trans_desc.addr =  0;
  trans_desc.cmd = 0;
  trans_desc.flags = 0;
  trans_desc.length = (8 * 4); // total data bits
  trans_desc.tx_buffer = txData;
  trans_desc.rxlength = 0 ; // Number of bits NOT number of bytes
  trans_desc.rx_buffer = NULL;
  txData[0] = (uint8_t)_sendData0; // command bits
  txData[1] = (uint8_t)_sendData1; // lower bits
  txData[2] = (uint8_t)_sendData2; // higher bits
  txData[3] = (uint8_t)_sendData3; // address
  intError = spi_device_transmit( h, &trans_desc);
  return intError;
} // void fWriteSPIdata8bits(  spi_device_handle_t &h, uint8_t address, uint8_t sendData )

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.