ESP32 Multiple Buses SPI & TFT_eSPI

How can I use ESP32 Multiple Busses SPI (hspi) with TFT_eSPI library

Did you have a problem when you tried it ?

Yes, I don’t understand how to tell the library to use the hspi bus, if you just assign the hspi bus pins in the library configuration file, I see SPI packets for the TFT display on the vspi pins

Please show your config and your sketch

I have found it simpler to use VSPI for the TFT and HSPI for other SPI devices
what other SPI devices do you wish to connect to the ESP32?

1 Like

SD Card, AD9833, MCP41010, MCP41050, MCP41100

which display are you using (in particular which display controller chip)?

have you tested any of the SPI devices as yet?

in the main it appears that sharing an SPI bus with an SD reader can cause problems - see Sharing the SPI bus among SD card and other SPI devices

EDIT:
AD9833 works OK with ESP32
not used MCP41010, etc but X9C103S Digital Potentiometer works OK with ESP32

ILI9341:
My sketch:

Спойлер
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library
#include <FS.h>
#include "SD.h"
#include "SPIFFS.h"
#include <LittleFS.h>

#define SD_MISO 19
#define SD_MOSI 23
#define SD_SCK  18

#define  SD_CS          5
#define  AD9833_CS     15
#define  MCP41x1_CS    16  // Define chipselect pin for MCP41010 (CS for Volume)
#define  MCP41x1_ALC   17  // Define chipselect pin for MCP41050 (CS for ALC)

#define VSPI_MISO   MISO
#define VSPI_MOSI   MOSI
#define VSPI_SCLK   SCK
#define VSPI_SS     SS

#define HSPI_MISO   12
#define HSPI_MOSI   13
#define HSPI_SCLK   14
#define HSPI_SS     0

//  G0 - TFT_CS         0 <-> TFT_CS
//  G2 - TFT_RST        2 <-> TFT_RST
//  G4 - TFT_DC         4 <-> TFT_DC
//

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

//uninitalised pointers to SPI objects
SPIClass * vspi1 = NULL;
SPIClass * vspi2 = NULL;
SPIClass * vspi3 = NULL;
SPIClass * vspi4 = NULL;
SPIClass * hspi = NULL;


#define TFT_GREY 0x5AEB

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library

float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;    // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 120, omx = 120, omy = 120, ohx = 120, ohy = 120; // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                    // for next 1 second timeout

static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6); // Get H, M, S from compile time

bool initial = 1;


void spiCommand(SPIClass *spi, byte data, int spiclk) {
  //use it as you would the regular arduino SPI API
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW); //pull SS slow to prep other end for transfer
  spi->transfer(data);
  digitalWrite(spi->pinSS(), HIGH); //pull ss high to signify end of data transfer
  spi->endTransaction();
}

void writeValueMCP41xxx(SPIClass *spi, byte data, int spiclk) {
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW);
  byte MSb = 0x11;         // 0b00010001 ; where x = 0, command bits for write
  byte LSb = data & 0xFF;  // No value greater than 255 ; send 0x0 otherwise
  uint16_t transmission = MSb << 8 | LSb;
  spi->transfer(transmission);
  digitalWrite(spi->pinSS(), HIGH);   // Disable chip select MCP41xxx
  spi->endTransaction();
}

void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return;
  }

  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print("  DIR : ");
      Serial.println(file.name());
      if (levels) {
        listDir(fs, file.path(), levels - 1);
      }
    } else {
      Serial.print("  FILE: ");
      Serial.print(file.name());
      Serial.print("  SIZE: ");
      Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}


void setup(void) {
  Serial.begin(115200);
  delay(3000);

  vspi1 = new SPIClass(VSPI);
  vspi2 = new SPIClass(VSPI);
  vspi3 = new SPIClass(VSPI);
  vspi4 = new SPIClass(VSPI);

  vspi1->begin();
  vspi2->begin(SCK, MISO, MOSI, AD9833_CS);
  vspi3->begin(SCK, MISO, MOSI, MCP41x1_CS);
  vspi4->begin(SCK, MISO, MOSI, MCP41x1_ALC);

  pinMode(vspi1->pinSS(), OUTPUT);   // VSPI SS - SD_CS       (SD CARD)
  pinMode(vspi2->pinSS(), OUTPUT);   // VSPI SS - AD9833_CS   (AD9833)
  pinMode(vspi3->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_CS  (Potenciometr)
  pinMode(vspi4->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_ALS (Als)

  if (!SD.begin(SD_CS, *vspi1, 4000000)) {
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("SD Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  listDir(SD, "/", 0);

  Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
  Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));


  tft.init();
  tft.setRotation(0);

  //tft.fillScreen(TFT_BLACK);
  //tft.fillScreen(TFT_RED);
  //tft.fillScreen(TFT_GREEN);
  //tft.fillScreen(TFT_BLUE);
  //tft.fillScreen(TFT_BLACK);
  tft.fillScreen(TFT_GREY);

  tft.setTextColor(TFT_WHITE, TFT_GREY);  // Adding a background colour erases previous text automatically

  // Draw clock face
  tft.fillCircle(120, 120, 118, TFT_GREEN);
  tft.fillCircle(120, 120, 110, TFT_BLACK);

  // Draw 12 lines
  for (int i = 0; i < 360; i += 30) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 114 + 120;
    yy0 = sy * 114 + 120;
    x1 = sx * 100 + 120;
    yy1 = sy * 100 + 120;

    tft.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
  }

  // Draw 60 dots
  for (int i = 0; i < 360; i += 6) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 102 + 120;
    yy0 = sy * 102 + 120;
    // Draw minute markers
    tft.drawPixel(x0, yy0, TFT_WHITE);

    // Draw main quadrant dots
    if (i == 0 || i == 180) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
    if (i == 90 || i == 270) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
  }

  tft.fillCircle(120, 121, 3, TFT_WHITE);

  // Draw text at position 120,260 using fonts 4
  // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m
  // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : .
  tft.drawCentreString("Time flies", 120, 260, 4);

  targetTime = millis() + 1000;
}

void loop() {

  // spiCommand(vspi1, 0b01010101, 1000000); // junk data to illustrate usage
  // delay(10);
  spiCommand(vspi2, 0b01110111, 1000000); // junk data to illustrate usage
  delay(100);
  static uint8_t pot = 0;
  writeValueMCP41xxx(vspi3, pot, 1000000);
  pot++;
  delay(100);
  static uint8_t alc = 255;
  writeValueMCP41xxx(vspi4, alc, 1000000);
  alc--;
  delay(100);

  if (targetTime < millis()) {
    targetTime += 1000;
    ss++;              // Advance second
    if (ss == 60) {
      ss = 0;
      mm++;            // Advance minute
      if (mm > 59) {
        mm = 0;
        hh++;          // Advance hour
        if (hh > 23) {
          hh = 0;
        }
      }
    }

    // Pre-compute hand degrees, x & y coords for a fast screen update
    sdeg = ss * 6;                // 0-59 -> 0-354
    mdeg = mm * 6 + sdeg * 0.01666667; // 0-59 -> 0-360 - includes seconds
    hdeg = hh * 30 + mdeg * 0.0833333; // 0-11 -> 0-360 - includes minutes and seconds
    hx = cos((hdeg - 90) * 0.0174532925);
    hy = sin((hdeg - 90) * 0.0174532925);
    mx = cos((mdeg - 90) * 0.0174532925);
    my = sin((mdeg - 90) * 0.0174532925);
    sx = cos((sdeg - 90) * 0.0174532925);
    sy = sin((sdeg - 90) * 0.0174532925);

    if (ss == 0 || initial) {
      initial = 0;
      // Erase hour and minute hand positions every minute
      tft.drawLine(ohx, ohy, 120, 121, TFT_BLACK);
      ohx = hx * 62 + 121;
      ohy = hy * 62 + 121;
      tft.drawLine(omx, omy, 120, 121, TFT_BLACK);
      omx = mx * 84 + 120;
      omy = my * 84 + 121;
    }

    // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
    tft.drawLine(osx, osy, 120, 121, TFT_BLACK);
    osx = sx * 90 + 121;
    osy = sy * 90 + 121;
    tft.drawLine(osx, osy, 120, 121, TFT_RED);
    tft.drawLine(ohx, ohy, 120, 121, TFT_WHITE);
    tft.drawLine(omx, omy, 120, 121, TFT_WHITE);
    tft.drawLine(osx, osy, 120, 121, TFT_RED);

    tft.fillCircle(120, 121, 3, TFT_RED);
  }
}

static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

does the code work? if not what happens?
did you test each device in a separate program before attempting to implement the complete program?

the test code works, but on the oscillogram I see calls via the vspi channel to the TFT display, periodically the data on the display may disappear and not appear again

I found that reducing the SPI frequency in the TFT_eSPI setup file (where you specify the SPI pins) helps, e.g. 10MHz

#define SPI_FREQUENCY  10000000
  1. In principle, I don’t understand how the TFT_eSPI library can find out that it needs to work through the hspi class I defined
  2. Why do I see foreign debris on vspi pins?
  3. how to overcome it

Variant 2:

Спойлер
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library - https://github.com/Bodmer/TFT_eSPI
#include <FS.h>
#include "SD.h"
#include "SPIFFS.h"
#include <LittleFS.h>

#define SD_MISO 19
#define SD_MOSI 23
#define SD_SCK  18

#define  SD_CS          5
#define  AD9833_CS     15
#define  MCP41x1_CS    16  // Define chipselect pin for MCP41010 (CS for Volume)
#define  MCP41x1_ALC   17  // Define chipselect pin for MCP41050 (CS for ALC)

#define VSPI_MISO   MISO
#define VSPI_MOSI   MOSI
#define VSPI_SCLK   SCK
#define VSPI_SS     SS

#define HSPI_MISO   12
#define HSPI_MOSI   13
#define HSPI_SCLK   14
#define HSPI_SS     0

//  G0 - TFT_CS         0 <-> TFT_CS
//  G2 - TFT_RST        2 <-> TFT_RST
//  G4 - TFT_DC         4 <-> TFT_DC
//

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

//uninitalised pointers to SPI objects
SPIClass * vspi1 = NULL;
SPIClass * vspi2 = NULL;
SPIClass * vspi3 = NULL;
SPIClass * vspi4 = NULL;
SPIClass * hspi = NULL;


#define TFT_GREY 0x5AEB

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library

float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;    // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 120, omx = 120, omy = 120, ohx = 120, ohy = 120; // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                    // for next 1 second timeout

static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6); // Get H, M, S from compile time

bool initial = 1;


void spiCommand(SPIClass *spi, byte data, int spiclk) {
  //use it as you would the regular arduino SPI API
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW); //pull SS slow to prep other end for transfer
  spi->transfer(data);
  digitalWrite(spi->pinSS(), HIGH); //pull ss high to signify end of data transfer
  spi->endTransaction();
}

void writeValueMCP41xxx(SPIClass *spi, byte data, int spiclk) {
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW);
  byte MSb = 0x11;         // 0b00010001 ; where x = 0, command bits for write
  byte LSb = data & 0xFF;  // No value greater than 255 ; send 0x0 otherwise
  uint16_t transmission = MSb << 8 | LSb;
  spi->transfer(transmission);
  digitalWrite(spi->pinSS(), HIGH);   // Disable chip select MCP41xxx
  spi->endTransaction();
}

void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return;
  }

  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print("  DIR : ");
      Serial.println(file.name());
      if (levels) {
        listDir(fs, file.path(), levels - 1);
      }
    } else {
      Serial.print("  FILE: ");
      Serial.print(file.name());
      Serial.print("  SIZE: ");
      Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}


void setup(void) {
  Serial.begin(115200);
  delay(3000);

  vspi1 = new SPIClass(VSPI);
  vspi2 = new SPIClass(VSPI);
  vspi3 = new SPIClass(VSPI);
  vspi4 = new SPIClass(VSPI);
  hspi = new SPIClass(HSPI);
  
  vspi1->begin();
  vspi2->begin(SCK, MISO, MOSI, AD9833_CS);
  vspi3->begin(SCK, MISO, MOSI, MCP41x1_CS);
  vspi4->begin(SCK, MISO, MOSI, MCP41x1_ALC);
  hspi->begin(HSPI_SCLK,HSPI_MISO,HSPI_MOSI,HSPI_SS);

  pinMode(vspi1->pinSS(), OUTPUT);   // VSPI SS - SD_CS       (SD CARD)
  digitalWrite(vspi1->pinSS(), HIGH);
  pinMode(vspi2->pinSS(), OUTPUT);   // VSPI SS - AD9833_CS   (AD9833)
  digitalWrite(vspi2->pinSS(), HIGH);
  pinMode(vspi3->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_CS  (Potenciometr)
  digitalWrite(vspi3->pinSS(), HIGH);
  pinMode(vspi4->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_ALS (Als)
  digitalWrite(vspi4->pinSS(), HIGH);
  
  pinMode(hspi->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_ALS (Als)
  digitalWrite(hspi->pinSS(), HIGH);

  if (!SD.begin(SD_CS, *vspi1, 4000000)) {
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("SD Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  listDir(SD, "/", 0);

  Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
  Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));


  tft.init();
  tft.setRotation(0);

  //tft.fillScreen(TFT_BLACK);
  //tft.fillScreen(TFT_RED);
  //tft.fillScreen(TFT_GREEN);
  //tft.fillScreen(TFT_BLUE);
  //tft.fillScreen(TFT_BLACK);
  tft.fillScreen(TFT_GREY);

  tft.setTextColor(TFT_WHITE, TFT_GREY);  // Adding a background colour erases previous text automatically

  // Draw clock face
  tft.fillCircle(120, 120, 118, TFT_GREEN);
  tft.fillCircle(120, 120, 110, TFT_BLACK);

  // Draw 12 lines
  for (int i = 0; i < 360; i += 30) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 114 + 120;
    yy0 = sy * 114 + 120;
    x1 = sx * 100 + 120;
    yy1 = sy * 100 + 120;

    tft.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
  }

  // Draw 60 dots
  for (int i = 0; i < 360; i += 6) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 102 + 120;
    yy0 = sy * 102 + 120;
    // Draw minute markers
    tft.drawPixel(x0, yy0, TFT_WHITE);

    // Draw main quadrant dots
    if (i == 0 || i == 180) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
    if (i == 90 || i == 270) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
  }

  tft.fillCircle(120, 121, 3, TFT_WHITE);

  // Draw text at position 120,260 using fonts 4
  // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m
  // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : .
  tft.drawCentreString("Time flies", 120, 260, 4);

  targetTime = millis() + 1000;
}

void loop() {

  // spiCommand(vspi1, 0b01010101, 1000000); // junk data to illustrate usage
  // delay(10);
  spiCommand(vspi2, 0b01110111, 1000000); // junk data to illustrate usage
  delay(100);
  static uint8_t pot = 0;
  writeValueMCP41xxx(vspi3, pot, 1000000);
  pot++;
  delay(100);
  static uint8_t alc = 255;
  writeValueMCP41xxx(vspi4, alc, 1000000);
  alc--;
  delay(100);

  if (targetTime < millis()) {
    targetTime += 1000;
    ss++;              // Advance second
    if (ss == 60) {
      ss = 0;
      mm++;            // Advance minute
      if (mm > 59) {
        mm = 0;
        hh++;          // Advance hour
        if (hh > 23) {
          hh = 0;
        }
      }
    }

    // Pre-compute hand degrees, x & y coords for a fast screen update
    sdeg = ss * 6;                // 0-59 -> 0-354
    mdeg = mm * 6 + sdeg * 0.01666667; // 0-59 -> 0-360 - includes seconds
    hdeg = hh * 30 + mdeg * 0.0833333; // 0-11 -> 0-360 - includes minutes and seconds
    hx = cos((hdeg - 90) * 0.0174532925);
    hy = sin((hdeg - 90) * 0.0174532925);
    mx = cos((mdeg - 90) * 0.0174532925);
    my = sin((mdeg - 90) * 0.0174532925);
    sx = cos((sdeg - 90) * 0.0174532925);
    sy = sin((sdeg - 90) * 0.0174532925);

    if (ss == 0 || initial) {
      initial = 0;
      // Erase hour and minute hand positions every minute
      tft.drawLine(ohx, ohy, 120, 121, TFT_BLACK);
      ohx = hx * 62 + 121;
      ohy = hy * 62 + 121;
      tft.drawLine(omx, omy, 120, 121, TFT_BLACK);
      omx = mx * 84 + 120;
      omy = my * 84 + 121;
    }

    // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
    tft.drawLine(osx, osy, 120, 121, TFT_BLACK);
    osx = sx * 90 + 121;
    osy = sy * 90 + 121;
    tft.drawLine(osx, osy, 120, 121, TFT_RED);
    tft.drawLine(ohx, ohy, 120, 121, TFT_WHITE);
    tft.drawLine(omx, omy, 120, 121, TFT_WHITE);
    tft.drawLine(osx, osy, 120, 121, TFT_RED);

    tft.fillCircle(120, 121, 3, TFT_RED);
  }
}

static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

I uncommented the line in the User_Setup.h file and the garbage disappeared.
#define USE_HSPI_PORT

Спойлер
#include <SPI.h>
#include <TFT_eSPI.h> // Hardware-specific library - https://github.com/Bodmer/TFT_eSPI
#include <FS.h>
#include "SD.h"
#include "SPIFFS.h"
#include <LittleFS.h>

#define SD_MISO 19
#define SD_MOSI 23
#define SD_SCK  18

#define  SD_CS          5
#define  AD9833_CS     15
#define  MCP41x1_CS    16  // Define chipselect pin for MCP41010 (CS for Volume)
#define  MCP41x1_ALC   17  // Define chipselect pin for MCP41050 (CS for ALC)

#define VSPI_MISO   MISO
#define VSPI_MOSI   MOSI
#define VSPI_SCLK   SCK
#define VSPI_SS     SS

#define HSPI_MISO   12
#define HSPI_MOSI   13
#define HSPI_SCLK   14
#define HSPI_SS     0

//  G0 - TFT_CS         0 <-> TFT_CS
//  G2 - TFT_RST        2 <-> TFT_RST
//  G4 - TFT_DC         4 <-> TFT_DC
//

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

//uninitalised pointers to SPI objects
SPIClass * vspi1 = NULL;
SPIClass * vspi2 = NULL;
SPIClass * vspi3 = NULL;
SPIClass * vspi4 = NULL;
SPIClass * hspi = NULL;


#define TFT_GREY 0x5AEB

TFT_eSPI tft = TFT_eSPI();       // Invoke custom library

float sx = 0, sy = 1, mx = 1, my = 0, hx = -1, hy = 0;    // Saved H, M, S x & y multipliers
float sdeg = 0, mdeg = 0, hdeg = 0;
uint16_t osx = 120, osy = 120, omx = 120, omy = 120, ohx = 120, ohy = 120; // Saved H, M, S x & y coords
uint16_t x0 = 0, x1 = 0, yy0 = 0, yy1 = 0;
uint32_t targetTime = 0;                    // for next 1 second timeout

static uint8_t conv2d(const char* p); // Forward declaration needed for IDE 1.6.x
uint8_t hh = conv2d(__TIME__), mm = conv2d(__TIME__ + 3), ss = conv2d(__TIME__ + 6); // Get H, M, S from compile time

bool initial = 1;


void spiCommand(SPIClass *spi, byte data, int spiclk) {
  //use it as you would the regular arduino SPI API
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW); //pull SS slow to prep other end for transfer
  spi->transfer(data);
  digitalWrite(spi->pinSS(), HIGH); //pull ss high to signify end of data transfer
  spi->endTransaction();
}

void writeValueMCP41xxx(SPIClass *spi, byte data, int spiclk) {
  spi->beginTransaction(SPISettings(spiclk, MSBFIRST, SPI_MODE0));
  digitalWrite(spi->pinSS(), LOW);
  byte MSb = 0x11;         // 0b00010001 ; where x = 0, command bits for write
  byte LSb = data & 0xFF;  // No value greater than 255 ; send 0x0 otherwise
  uint16_t transmission = MSb << 8 | LSb;
  spi->transfer16(transmission);
  digitalWrite(spi->pinSS(), HIGH);   // Disable chip select MCP41xxx
  spi->endTransaction();
}

void listDir(fs::FS &fs, const char * dirname, uint8_t levels) {
  Serial.printf("Listing directory: %s\n", dirname);

  File root = fs.open(dirname);
  if (!root) {
    Serial.println("Failed to open directory");
    return;
  }
  if (!root.isDirectory()) {
    Serial.println("Not a directory");
    return;
  }

  File file = root.openNextFile();
  while (file) {
    if (file.isDirectory()) {
      Serial.print("  DIR : ");
      Serial.println(file.name());
      if (levels) {
        listDir(fs, file.path(), levels - 1);
      }
    } else {
      Serial.print("  FILE: ");
      Serial.print(file.name());
      Serial.print("  SIZE: ");
      Serial.println(file.size());
    }
    file = root.openNextFile();
  }
}


void setup(void) {
  Serial.begin(115200);
  delay(3000);

  vspi1 = new SPIClass(VSPI);
  vspi2 = new SPIClass(VSPI);
  vspi3 = new SPIClass(VSPI);
  vspi4 = new SPIClass(VSPI);
  hspi = new SPIClass(HSPI);
  
  vspi1->begin();
  vspi2->begin(SCK, MISO, MOSI, AD9833_CS);
  vspi3->begin(SCK, MISO, MOSI, MCP41x1_CS);
  vspi4->begin(SCK, MISO, MOSI, MCP41x1_ALC);
  hspi->begin(HSPI_SCLK,HSPI_MISO,HSPI_MOSI,HSPI_SS);

  pinMode(vspi1->pinSS(), OUTPUT);   // VSPI SS - SD_CS       (SD CARD)
  digitalWrite(vspi1->pinSS(), HIGH);
  pinMode(vspi2->pinSS(), OUTPUT);   // VSPI SS - AD9833_CS   (AD9833)
  digitalWrite(vspi2->pinSS(), HIGH);
  pinMode(vspi3->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_CS  (Potenciometr)
  digitalWrite(vspi3->pinSS(), HIGH);
  pinMode(vspi4->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_ALS (Als)
  digitalWrite(vspi4->pinSS(), HIGH);
  
  pinMode(hspi->pinSS(), OUTPUT);   // VSPI SS - MCP41x1_ALS (Als)
  digitalWrite(hspi->pinSS(), HIGH);

  if (!SD.begin(SD_CS, *vspi1, 4000000)) {
    Serial.println("Card Mount Failed");
    return;
  }
  uint8_t cardType = SD.cardType();

  if (cardType == CARD_NONE) {
    Serial.println("No SD card attached");
    return;
  }

  Serial.print("SD Card Type: ");
  if (cardType == CARD_MMC) {
    Serial.println("MMC");
  } else if (cardType == CARD_SD) {
    Serial.println("SDSC");
  } else if (cardType == CARD_SDHC) {
    Serial.println("SDHC");
  } else {
    Serial.println("UNKNOWN");
  }

  uint64_t cardSize = SD.cardSize() / (1024 * 1024);
  Serial.printf("SD Card Size: %lluMB\n", cardSize);

  listDir(SD, "/", 0);

  Serial.printf("Total space: %lluMB\n", SD.totalBytes() / (1024 * 1024));
  Serial.printf("Used space: %lluMB\n", SD.usedBytes() / (1024 * 1024));


  tft.init();
  tft.setRotation(0);

  //tft.fillScreen(TFT_BLACK);
  //tft.fillScreen(TFT_RED);
  //tft.fillScreen(TFT_GREEN);
  //tft.fillScreen(TFT_BLUE);
  //tft.fillScreen(TFT_BLACK);
  tft.fillScreen(TFT_GREY);

  tft.setTextColor(TFT_WHITE, TFT_GREY);  // Adding a background colour erases previous text automatically

  // Draw clock face
  tft.fillCircle(120, 120, 118, TFT_GREEN);
  tft.fillCircle(120, 120, 110, TFT_BLACK);

  // Draw 12 lines
  for (int i = 0; i < 360; i += 30) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 114 + 120;
    yy0 = sy * 114 + 120;
    x1 = sx * 100 + 120;
    yy1 = sy * 100 + 120;

    tft.drawLine(x0, yy0, x1, yy1, TFT_GREEN);
  }

  // Draw 60 dots
  for (int i = 0; i < 360; i += 6) {
    sx = cos((i - 90) * 0.0174532925);
    sy = sin((i - 90) * 0.0174532925);
    x0 = sx * 102 + 120;
    yy0 = sy * 102 + 120;
    // Draw minute markers
    tft.drawPixel(x0, yy0, TFT_WHITE);

    // Draw main quadrant dots
    if (i == 0 || i == 180) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
    if (i == 90 || i == 270) tft.fillCircle(x0, yy0, 2, TFT_WHITE);
  }

  tft.fillCircle(120, 121, 3, TFT_WHITE);

  // Draw text at position 120,260 using fonts 4
  // Only font numbers 2,4,6,7 are valid. Font 6 only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : . - a p m
  // Font 7 is a 7 segment font and only contains characters [space] 0 1 2 3 4 5 6 7 8 9 : .
  tft.drawCentreString("Time flies", 120, 260, 4);

  targetTime = millis() + 1000;
}

void loop() {

  // spiCommand(vspi1, 0b01010101, 1000000); // junk data to illustrate usage
  // delay(10);
  spiCommand(vspi2, 0b01110111, 1000000); // junk data to illustrate usage
  delay(100);
  static uint8_t pot = 0;
  writeValueMCP41xxx(vspi3, pot, 1000000);
  pot++;
  delay(100);
  static uint8_t alc = 255;
  writeValueMCP41xxx(vspi4, alc, 1000000);
  alc--;
  delay(100);

  if (targetTime < millis()) {
    targetTime += 1000;
    ss++;              // Advance second
    if (ss == 60) {
      ss = 0;
      mm++;            // Advance minute
      if (mm > 59) {
        mm = 0;
        hh++;          // Advance hour
        if (hh > 23) {
          hh = 0;
        }
      }
    }

    // Pre-compute hand degrees, x & y coords for a fast screen update
    sdeg = ss * 6;                // 0-59 -> 0-354
    mdeg = mm * 6 + sdeg * 0.01666667; // 0-59 -> 0-360 - includes seconds
    hdeg = hh * 30 + mdeg * 0.0833333; // 0-11 -> 0-360 - includes minutes and seconds
    hx = cos((hdeg - 90) * 0.0174532925);
    hy = sin((hdeg - 90) * 0.0174532925);
    mx = cos((mdeg - 90) * 0.0174532925);
    my = sin((mdeg - 90) * 0.0174532925);
    sx = cos((sdeg - 90) * 0.0174532925);
    sy = sin((sdeg - 90) * 0.0174532925);

    if (ss == 0 || initial) {
      initial = 0;
      // Erase hour and minute hand positions every minute
      tft.drawLine(ohx, ohy, 120, 121, TFT_BLACK);
      ohx = hx * 62 + 121;
      ohy = hy * 62 + 121;
      tft.drawLine(omx, omy, 120, 121, TFT_BLACK);
      omx = mx * 84 + 120;
      omy = my * 84 + 121;
    }

    // Redraw new hand positions, hour and minute hands not erased here to avoid flicker
    tft.drawLine(osx, osy, 120, 121, TFT_BLACK);
    osx = sx * 90 + 121;
    osy = sy * 90 + 121;
    tft.drawLine(osx, osy, 120, 121, TFT_RED);
    tft.drawLine(ohx, ohy, 120, 121, TFT_WHITE);
    tft.drawLine(omx, omy, 120, 121, TFT_WHITE);
    tft.drawLine(osx, osy, 120, 121, TFT_RED);

    tft.fillCircle(120, 121, 3, TFT_RED);
  }
}

static uint8_t conv2d(const char* p) {
  uint8_t v = 0;
  if ('0' <= *p && *p <= '9')
    v = *p - '0';
  return 10 * v + *++p - '0';
}

The question remains: how to share the hspi bus between several devices