Multiple SPI devices same bus fails - ESP32 v3.0.2

I recently migrated to the ESP32 3.x update, and still tracking down issues. Posting this in case it helps others, and also maybe you have suggestions how I can implement this better?

I have multiple SPI devices on the VSPI (FSPI) bus on the ESP32 S3 FN8 (custom PCB).

On version 2.x, I simply created a unique SPIClass for each device, pointing to the same VSPI bus. In the code snippet below I'll use the barometer and imu for example:


//uninitialised pointers to SPI objects
SPIClass * baro_vspi = NULL;
SPIClass * imu_vspi = NULL;

void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting Setup");
  
  baro_vspi = new SPIClass(VSPI);
  baro_vspi->begin(SPI_CLK, SPI_MISO, SPI_MOSI, SPI_SS_BARO);
  pinMode(baro_vspi->pinSS(), OUTPUT);
  digitalWrite(baro_vspi->pinSS(), HIGH);

  imu_vspi = new SPIClass(VSPI);
  imu_vspi->begin(SPI_CLK, SPI_MISO, SPI_MOSI, SPI_SS_IMU);
  pinMode(imu_vspi->pinSS(), OUTPUT);
  digitalWrite(imu_vspi->pinSS(), HIGH);
}

This worked fine on version 2.x, but now on version 3.0 the code hangs when we eventually do an spi->transfer(). I traced this down to the specific SPIClass->begin() instruction. It seems like on v3.0 you can't call that twice (once for each device) if they're pointing to the same bus?

Full code is below. If you comment out the second begin line (imu_vspi->begin(...)), then it "works". But of course with that line commented out, you no longer have the SPI object set up properly and can't reference the different chip select pin (SPI_SS_IMU).

Is there a better way to handle multiple SPI devices on the same bus? I realize I can create my own helper functions and handle the chip select pins manually. But I don't see the point of the SPIClass configuring a unique SS pin if you can't create multiple classes to handle multiple devices.

Any tips? Thanks!

#include <SPI.h>

#define SPI_SS_IMU       10
#define SPI_MOSI         11
#define SPI_CLK          12
#define SPI_MISO         13  
#define SPI_SS_BARO      18

#if CONFIG_IDF_TARGET_ESP32S2 || CONFIG_IDF_TARGET_ESP32S3
#define VSPI FSPI
#endif

static const int spiClk = 1000000;  // 1 MHz

//uninitialised pointers to SPI objects
SPIClass * baro_vspi = NULL;
SPIClass * imu_vspi = NULL;

void setup() {
  delay(1000);
  Serial.begin(115200);
  delay(1000);
  Serial.println("Starting Setup");
  
  baro_vspi = new SPIClass(VSPI);
  baro_vspi->begin(SPI_CLK, SPI_MISO, SPI_MOSI, SPI_SS_BARO);
  pinMode(baro_vspi->pinSS(), OUTPUT);
  digitalWrite(baro_vspi->pinSS(), HIGH);

  imu_vspi = new SPIClass(VSPI);
  imu_vspi->begin(SPI_CLK, SPI_MISO, SPI_MOSI, SPI_SS_IMU);   // comment this line out to make it "work"
  pinMode(imu_vspi->pinSS(), OUTPUT);
  digitalWrite(imu_vspi->pinSS(), HIGH);
}

void loop() {
  spi_Command(baro_vspi, 0b01010101);  // junk data to illustrate usage
  delay(100);
  spi_Command(imu_vspi, 0b01010101);  // junk data to illustrate usage
  delay(100);
}

void spi_Command(SPIClass *spi, byte data) {
  
  spi->beginTransaction(SPISettings(spiClk, MSBFIRST, SPI_MODE0));    
  Serial.print("Began tx. ");
  digitalWrite(spi->pinSS(), LOW);                                    
  Serial.print("SS low. ");
  spi->transfer(data);     
  Serial.print("sent data. ");
  digitalWrite(spi->pinSS(), HIGH);                                    
  Serial.print("SS high. ");
  spi->endTransaction();                                                
  Serial.println("end tx. ");
}

PS: for completeness, when you comment out the second ->begin, you get proper repeating serial prints:
Began tx. SS low. sent data. SS high. end tx.
Began tx. SS low. sent data. SS high. end tx.
..etc

when you leave that second ->begin line in place, you get:
Began tx. SS low. <hang>

Post an annotated schematic showing all the devices connected to the processor. Please not any lead over 10"/25cm.

Hi gilshultz, thanks for the quick response!

I can provide a schematic, but it's exactly as you'd expect (all SPI devices share MOSI, MISO, CLK, and each device gets its own chip select). No PCB trace is longer than about 20mm.

This has been working fine for months under ESP32 Board Manager v2.x. It's only with the recent migration to 3.0 that it fails with multiple ->begin() calls to the same bus.

My main question is still around the SPIClass implementation.

I just finished reworking the code to manage all devices with the same single SPIClass (just some extra handling of the chip selects because you can't use the SPIClass->pinSS reference).

It works... until I start using the u8g2 library for the LCD (also on the same VSPI bus). The code hangs right after calling u8g2.begin().

I assume the u8g2 library must be making its own call to SPI->begin (makes sense), which unfortunately triggers the same failure mode seen above.

Is anyone using multiple SPI devices on a single ESP32 bus with version 3.0.x? Even better if you're using the u8g2 display library also. How did you implement it?

Maybe I can initialize the u8g2 object first, and rely on that call to begin the SPI bus, then I won't separately begin the SPI bus for my other peripherals.

Any reason why this changed between V2 and V3?

Thanks for any help!

Sorry I am not familiar with the library but it sounds like you are on the correct track. Good Luck. Can you go back to the old board manager?

Thanks, I'll take the luck :). I can go back to the old version, but I'm on the new version because it fixed a timer interrupt issue for me. I'm now trying to decide which problem I'd rather deal with :rofl:.

Since I don't have [easy] control of what happens inside the u8g2 lcd display library, I tried putting that initialization first, then not calling spi->begin() for any of my other devices. The code seems to run without hanging, increasing my confidence that the error is in multiple calls to spi->begin(), or something of that nature.

I'm just shocked that no one else seems to have had this issue yet? I assume many people are running multiple SPI devices with libraries etc... surprised I haven't been able to find this issue reported yet for ESP32 v3.x.

posted issue to github too for reference

I hope I'm just doing something wrong and there's a really easy and obvious way to run multiple SPI devices on the new version.

I have a hard time believing that it ever worked. What you're doing makes no sense to me. Creating two SPIClass objects each referencing the same hardware SPI device seems like a disaster waiting to happen.

Why don't you just use the standard Arduino SPI library paradigm? When you #include SPI.h, you get the SPI object predefined on HSPI. Why don't you just use that along with the beginTransaction() function and SPISettings class to customize the the SPI configuration for each slave (if required)? Why do you want to use VSPI instead anyway?

Hi gfvalvo! I would love it if the mistake is on my side -- that's the easiest kind to fix! :slight_smile:

A few answers (please correct my understanding if anything seems off)

>>Why do you want to use VSPI instead anyway?
In my full project I'm using both available SPI busses on the ESP32-S3. One is configured for quad-mode SDCard support; the other for the remaining sensors and LCDdisplay etc.
I could switch which busses are used for what, but either way I need them both working, and according to the ESP32S3 manual, 'SPI3' (Arduino defines this as VSPI) is general purpose and 'SPI2' (Arduino defined as HSPI) is preferred for higher speed use, so I use HSPI for SDIO and VSPI for the other assorted sensors/LCD etc.

>>Why don't you just use the standard Arduino SPI library paradigm?
Yes my intent was to take advantage of standard Arduino way of doing things. I believe my code example above is a near-copy of the SPI sketch example available in the IDE. Is there another way of setting up SPI without needing to create a SPIClass object at all? All the SPI examples I've seen create a new class and then use the beginTransaction() and SPISettings() functions as you mention. I'd be delighted to see a different approach that may work! Can you share an example please?

>>Creating two SPIClass objects each referencing the same hardware SPI device seems like a disaster waiting to happen.
Maybe so. But as mentioned above I've re-worked the code to use a single class for all of my "manually initialized" SPI devices.... but still I have an issue where libraries come in. The u8g2 display library creates a new class object (including handling all the SPI communication for the display), and I don't have easy control over that. And unfortunately this still seems to conflict with my other SPIClass->begin() call. (Again though, only in board manager v3.x)

>> I have a hard time believing that it ever worked.
Maybe it's a miracle! But it was working great under board manager V2.x. In fact I had 3 of my own classes plus the library-generated u8g2 object all pointing to the same SPI bus.

I think the punchline still is: Having >= two classes pointing to the same bus seems to conflict in the new 3.x update.
Yet I believe I still need at least two (one for the display managed by the library, and one for all the "other" devices). I'm not thinking of a way around that... :frowning:

(I'm ignoring the SD card mentioned above since it's on its own bus)

Thanks for the backstory. First, looking in esp32-hal-spi.h, it appears that two SPI buses are defined for ESP32S3. FSPI is 0 and HSPI is 1:

#if CONFIG_IDF_TARGET_ESP32C2 || CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32C6 || CONFIG_IDF_TARGET_ESP32H2 || CONFIG_IDF_TARGET_ESP32S3
#define FSPI 0
#define HSPI 1
#elif CONFIG_IDF_TARGET_ESP32S2
#define FSPI 1  //SPI 1 bus. ESP32S2: for external memory only (can use the same data lines but different SS)
#define HSPI 2  //SPI 2 bus. ESP32S2: external memory or device  - it can be matrixed to any pins
#define SPI2 2  // Another name for ESP32S2 SPI 2
#define SPI3 3  //SPI 3 bus. ESP32S2: device only - it can be matrixed to any pins
#elif CONFIG_IDF_TARGET_ESP32
#define FSPI 1  //SPI 1 bus attached to the flash (can use the same data lines but different SS)
#define HSPI 2  //SPI 2 bus normally mapped to pins 12 - 15, but can be matrixed to any pins
#define VSPI 3  //SPI 3 bus normally attached to pins 5, 18, 19 and 23, but can be matrixed to any pins
#endif

On ESP32S3, the standard Arduino SPI object is created on FSPI (in SPI.cpp):

#if CONFIG_IDF_TARGET_ESP32
SPIClass SPI(VSPI);
#else
SPIClass SPI(FSPI);
#endif

I looked a little bit into the u8g2 display library. It doesn't create a new SPIClass object. It just uses the standard Arduino SPI object (FSPI on ESP32S3). All SPI accesses in this library take place in U8x8lib.cpp. So, it would be relatively straight forward to modify the library to accept a custom SPIClass object via a C++ reference.

However, the path of least resistance would be to accept the SPI object that the library uses (FSPI on ESP32S3). Don't create another SPIClass object on that interface. Don't call SPI.begin() either as u8g2 also does that. So, I think if you call begin() on your u8g2 object first, you should be all set. It will call begin() on the SPI object for you. So, your other devices on SPI (FSPI on ESP32S3) will be ready to go. As you noted, you'll need to keep track of those devices' Slave Select pin outside of the SPIClass object and activate them before accessing those devices.

Note, all of the above only applies to ESP32 Arduino core V3.x. I didn't check Core 2.x.

Hi @gfvalvo I really appreciate the thorough consideration you're providing here; thanks!

I wasn't aware of the static instance of SPIClass that Arduino provides... When I was looking at the multiple-bus SPI example available in the IDE, I assumed I had to create my own SPIClass instances as shown.

So, trying your suggestion, it does seem to work! There is only a single SPIClass instance (the default one), and I can even call SPI.begin() multiple times and there is no error (or more precisely: u8g2 library calls SPI.begin(), and I can call it again when initializing my other SPI devices). Not that I'd necessarily need to, as you suggest, but it makes the code more modular to be able to call begin() in my SPI routines and not have to be dependent upon a library for a different device doing it for me earlier.
This is actually great news, because I'm probably going to use another separate library for the IMU, so I would likely have multiple begin() calls outside of my control.

So the issue seems to be specifically with more than one SPIClass instance [in version 3.x of ESP32]. It sure is a coincidence it worked in V2.x and I was getting along just fine.

Thank again for the helpful suggestions and all your time reviewing the situation!