XIAO-ESP32S3 Sense: Error while using the SD card and TFT display simultaneously

Hi all,

I am trying to build a mini camera with Seeedstudio XIAO-ESP32-S3-Sense and Waveshare Pico 240x135 ST7789 display.

I have configured the User_Setup for the display using the Round Display example on the seeedstudio website.

User setup is included as a comment in the following code.

The code works in this order:

  1. setup(): initializes camera, sd card, display and shutter button pin
  2. loop(): it gets a frame from camera buffer
    2.a. passes it to the TFT display using pushScaledFrame()
    2.b. if the shutter button is pressed, it instead passes the frame to captureAndSave()
    2.c. returns the frame to camera buffer and loop repeats

The code works fine and displays the camera output initially, but the board crashes when shutter is pressed. As per the seeedstudio website both the SD card and TFT display share the same SPI pins (GPIO9 for MOSI, GPIO7 for SCLK) and that other SPI functions (TFT display in my case) cannot be used simultaneously with the SD card. The website also points to another github page by MjRovai which states that the SPI conflict can be resolved by defining SD_CS_PIN 21 in the code.

I did that, but the code still doesn’t work.

I have confirmed that the rest of my code works properly by commenting out the initDisplay() in setup() and pushScaledFrame() in loop() to completely turn off the display. Then the image is captured and saved to SD card without any issue when the shutter button is pressed.

I am new to programming with microcontrollers and could really use some help.

TIA.

1 Like

Hi there,

And Welcome here…

So sounds like your close, You’ll get it. It does work.
What output do you get /or See when you run the UserSelect_Setup.ino in the tools for the TFT_eSPI lib.
Run that code and verify via the Serial output “that it is set the way you think it is”. (probably not) This is where everyone gets hung up. Run that code and report back.
You can check also some of the Round Display Demo’s I have posted with code and different configs for TFT_eSPI.lib

HTH
GL :slight_smile: PJ :v:

I am thinking a chip select on the spi bus…

Hey! Thanks for the reply.

I didn’t find the exact UserSelect_Setup.ino that you mentioned but instead run another test file:
Arduino\libraries\TFT_eSPI\examples\Test and diagnostics\Read_User_Setup\Read_User_Setup.ino

This is the serial output:

TFT_eSPI ver = 2.5.43
Processor    = ESP32
Frequency    = 240MHz
Transactions = Yes
Interface    = SPI
Display driver = 7789
Display width  = 135
Display height = 240

R0 x offset = 52
R0 y offset = 40
MOSI    = GPIO 9
SCK     = GPIO 7
TFT_CS   = GPIO 2
TFT_DC   = GPIO 4
TFT_RST  = GPIO 1

Font GLCD   loaded
Font 2      loaded
Font 4      loaded
Font 6      loaded
Font 7      loaded
Font 8      loaded
Smooth font enabled

Display SPI frequency = 40.00

Please let me know if this if this is correct.

chip select is here

according to the XIAO standard the SD chip select is 2

Hi there,

Yep, Excellent , I must be losing it…With the Setup Select ,LOL
You ran the correct one.
I see it says the driver is the correct config.
The pins would be next, there are caveats when using the Round display with TFT_eSPI.lib, Seeed has also the Round_Display.lib that does some of it behind the scene.
easiest may be to switch the shutter button io pin. There are jumpers to remove or bridge to cut if you want a custom setup.

I see you commented out some portion to run , what about trying it with the round display lib ? and display it on the round screen first.
I feel like the SD pin should be left as it is though, and work out the display part seperatly, maybe read captured image from the SD you saved and display it
only as a display only test?

HTH
GL :slight_smile: PJ :v:

For the xiao-esp32-s3-sense module, the SD chip select is actually on GPIO3.

However, I have assigned SD_CS_PIN to GPIO21 as per this solution:

1 Like

Hi there,

can you post up a picture how it’s connected ?

GL :slight_smile: PJ :v:

FYI , there is 2 SD drives , one on the round display and another on the Sense camera daughter board … so two CS’s … talk about nutty.

1 Like

always remember we are talking about XIAO pins=2

Hey, I am not using the Round display. I have a Waveshare 240x135 rectangular display.

This is how everything is wired up currently.

Hi, if you are talking about the digital pin notation, I have left the D2 pin unoccupied. Please check the wiring diagram which I just posted.

Hi there,

Ok, So that should work but which BSP are you using on the ESP32S3
(first 2 lines compiler output)
Which Package?

HTH
GL :slight_smile: PJ :v:

Also could you post the code and use the code tags “</>” paste it in there.

it this unit.

You may need to init something like this one?
YMMV

#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>

// GPIOs based on your wiring
#define TFT_RST 1
#define TFT_DC 4
#define TFT_CS 2
#define TFT_MOSI 9  // Data out
#define TFT_SCLK 7  // Clock out

Adafruit_ST7789 tft = Adafruit_ST7789(&SPI, TFT_CS, TFT_DC, TFT_RST);

void setup() {
  SPI.begin(TFT_SCLK, TFT_RST, TFT_MOSI, TFT_CS);
  //Serial.begin(115200);
  tft.init(240, 280);

  delay(1000);
  tft.setSPISpeed(10000000);  // 10 MHz max
  tft.setRotation(1);
  tft.fillScreen(ST77XX_BLACK);
  tft.setTextColor(ST77XX_WHITE);
  tft.setTextSize(2);
  tft.setCursor(10, 10);
  tft.println("Hello NV3030B");
}

void loop() {}

:v:

Hi,

Yes that is the display unit which I have.

First few lines of the compiler output are as follows,

FQBN: esp32:esp32:XIAO_ESP32S3:PSRAM=opi
Using board ‘XIAO_ESP32S3’ from platform in folder: C:\Users\witz\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.0.7
Using core ‘esp32’ from platform in folder: C:\Users\witz\AppData\Local\Arduino15\packages\esp32\hardware\esp32\3.0.7

. 
.
.
.

Serial port COM5
Connecting…
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
.
.
.
.

My full code:

//  Seeedstudio XIAO-ESP32-S3-Sense + Waveshare Pico 240x135 ST7789 display

//------------------------------------------------
// ******** User Setup file for TFT_eSPI *********

// #define ST7789_DRIVER

// #define TFT_WIDTH  135
// #define TFT_HEIGHT 240

// #define TFT_MISO -1
// #define TFT_MOSI  9     
// #define TFT_SCLK  7    
// #define TFT_CS    2     
// #define TFT_DC    4     
// #define TFT_RST   1        
// #define TFT_BACKLIGHT_ON HIGH

// #define SPI_FREQUENCY  40000000

// #define USE_HSPI_PORT

// #define LOAD_GLCD
// #define LOAD_FONT2
// #define LOAD_FONT4
// #define LOAD_FONT6
// #define LOAD_FONT7
// #define LOAD_FONT8
// #define LOAD_GFXFF
// #define SMOOTH_FONT
//-------------------------------------------



#include <esp_camera.h>
#include <SD.h>
#include <TFT_eSPI.h>
#include <img_converters.h>


//-----------------------------------------------------------------------
//defining camera pins for CAMERA_MODEL_XIAO_ESP32S3
#define PWDN_GPIO_NUM     -1
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM     10
#define SIOD_GPIO_NUM     40
#define SIOC_GPIO_NUM     39

#define Y9_GPIO_NUM       48
#define Y8_GPIO_NUM       11
#define Y7_GPIO_NUM       12
#define Y6_GPIO_NUM       14
#define Y5_GPIO_NUM       16
#define Y4_GPIO_NUM       18
#define Y3_GPIO_NUM       17
#define Y2_GPIO_NUM       15
#define VSYNC_GPIO_NUM    38
#define HREF_GPIO_NUM     47
#define PCLK_GPIO_NUM     13

#define LED_GPIO_NUM      21


//-----------------------------------------------------------------------
//defining SD chip select pin for SD card
#define SD_CS_PIN 21


//-----------------------------------------------------------------------
//  code to initialize camera
void initCamera(){
  camera_config_t config;
  config.ledc_channel = LEDC_CHANNEL_0;
  config.ledc_timer = LEDC_TIMER_0;
  config.pin_d0 = Y2_GPIO_NUM;
  config.pin_d1 = Y3_GPIO_NUM;
  config.pin_d2 = Y4_GPIO_NUM;
  config.pin_d3 = Y5_GPIO_NUM;
  config.pin_d4 = Y6_GPIO_NUM;
  config.pin_d5 = Y7_GPIO_NUM;
  config.pin_d6 = Y8_GPIO_NUM;
  config.pin_d7 = Y9_GPIO_NUM;
  config.pin_xclk = XCLK_GPIO_NUM;
  config.pin_pclk = PCLK_GPIO_NUM;
  config.pin_vsync = VSYNC_GPIO_NUM;
  config.pin_href = HREF_GPIO_NUM;
  config.pin_sccb_sda = SIOD_GPIO_NUM;
  config.pin_sccb_scl = SIOC_GPIO_NUM;
  config.pin_pwdn = PWDN_GPIO_NUM;
  config.pin_reset = RESET_GPIO_NUM;
  config.xclk_freq_hz = 20000000;
  config.frame_size = FRAMESIZE_QVGA;
  // config.pixel_format = PIXFORMAT_JPEG;
  config.pixel_format = PIXFORMAT_RGB565;
  config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
  config.fb_location = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count = 1;

  // camera initialization and error handling
  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }

  //getting sensor details
  sensor_t *s = esp_camera_sensor_get();
  Serial.println("Camera Started. PID is");
  Serial.println(s->id.PID, HEX);
}


//-----------------------------------------------------------------------
//  code to initialize SD card
int photoIndex = 0;

void initSD() {
  if (SD.begin(SD_CS_PIN)) {
    uint64_t cardSize = SD.cardSize() / (1024 * 1024);
    Serial.printf("SD mounted. Size: %lluMB\n", cardSize);
  } else {
    Serial.println("SD not found");
  }

  if (!SD.exists("/photos")) {
    SD.mkdir("/photos");
    Serial.println("/photos created.");
  }

  // Find next available index
  while (true) {
    char path[32];
    sprintf(path, "/photos/img_%04d.jpg", photoIndex);
    if (!SD.exists(path)) break;
    photoIndex++;
  }
  Serial.printf("Next photo index: %d\n", photoIndex);

}


//-----------------------------------------------------------------------
//  code to initialize TFT display
TFT_eSPI tft = TFT_eSPI();

void initDisplay() {
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE);
  tft.setTextSize(2);
  tft.setCursor(65, 50);
  tft.print("Display OK");
  delay(2000);
}


//-----------------------------------------------------------------------
//  code to scale the QVGA frame and send to TFT display
#define DISPLAY_W  240
#define DISPLAY_H  135
#define SRC_W      320 //QVGA frame width
#define SRC_H      240 //QVGA frame height
#define SCALED_W   180
#define SCALED_H   135
#define OFFSET_X   30
#define OFFSET_Y   0
static uint16_t lineBuf[SCALED_W];

void pushScaledFrame(uint16_t *buf) {
  tft.startWrite();
  for (int y = 0; y < SCALED_H; y++) {
    int srcY = (y * SRC_H) / SCALED_H;
    for (int x = 0; x < SCALED_W; x++) {
      int srcX = (x * SRC_W) / SCALED_W;
      lineBuf[x] = buf[srcY * SRC_W + srcX];
    }
    tft.pushImage(OFFSET_X, y, SCALED_W, 1, lineBuf);
  }
  tft.endWrite();
}


//-----------------------------------------------------------------------
//  code to convert RGB565 frame to JPG and save to SD card
#define SHUTTER_PIN 44

void captureAndSave(camera_fb_t *fb) {
  uint8_t *jpegBuf = NULL;
  size_t   jpegLen = 0;

  // Convert RGB565 → JPEG
  bool ok = frame2jpg(fb, 90, &jpegBuf, &jpegLen);
  esp_camera_fb_return(fb);

  if (!ok || !jpegBuf) {
    Serial.println("JPEG conversion failed.");
    return;
  }

  char path[32];
  sprintf(path, "/photos/img_%04d.jpg", photoIndex);
  File file = SD.open(path, FILE_WRITE);
  if (!file) {
    free(jpegBuf);
    return;
  }
  file.write(jpegBuf, jpegLen);
  file.close();
  free(jpegBuf);

  Serial.printf("Saved: %s (%d bytes)\n", path, jpegLen);
  photoIndex++;
}


//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
void setup() {
  Serial.begin(115200);

  initCamera();

  initSD();

  initDisplay();

  pinMode(SHUTTER_PIN, INPUT_PULLUP);

}



//-----------------------------------------------------------------------
//-----------------------------------------------------------------------
void loop() {


  camera_fb_t *fb = esp_camera_fb_get();  //get frame from camera buffer

  if (!fb) {
    Serial.println("Frame capture failed");
    delay(100);
    return;

  }else {
    pushScaledFrame((uint16_t *)fb->buf); //display the frame on screen
  
    if(digitalRead(SHUTTER_PIN) == LOW){ //capture when shutter button is pressed
      Serial.println("Shutter Pressed");
      captureAndSave(fb);

      while(digitalRead(SHUTTER_PIN) == LOW); //while loop ends when button is released
    }

    esp_camera_fb_return(fb); //return frame to camera buffer and continue the loop
  }

}

Also, just so that we are on the same page, the initDisplay() on my code works fine. It shows Display OK on the TFT display at the start, holds for two seconds and switches to the camera stream (which is called in the loop).

Hi there,

Ok, SO good to know.
Looks like you are returning the same frame buffer twice when the shutter is pressed.

In captureAndSave():
and again in the Loop ? 'esp_camera_fb_return(fb)`;

Fix that it should work, but also
Do not use GPIO21 for LED control while the SD card is mounted, because that pin is the SD card CS on XIAO ESP32S3 Sense. AFAIK :crossed_fingers:

HTH

GL :slight_smile: PJ :v:

You appear to be clobbering the frame buffer ?
I’d also make the SPI setup explicit before initializing either device:

SPI.begin(7, 8, 9, -1);

SPI bus contention could be problem #2 that will set it.
Then:

  • initialize TFT
  • initialize SD with SD.begin(21)
    let loop() be the only place that returns the frame.

GO :grin:

Hi, thanks for pointing out my mistake with the frame buffer. I have changed that to only return once in the loop.

I also removed #define LED_GPIO_NUM 21

Further I included the <SPI.h> library and modified the setup() to:

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

  SPI.begin(7, 8, 9, -1);

  initCamera();

  initDisplay();
  
  initSD();

  pinMode(SHUTTER_PIN, INPUT_PULLUP);

}

Now the serial output is
Camera Started. PID is
26

And then Display OK is shown on the TFT display.

Next thing that should ideally be printed in the serial monitor is either the output of:

Serial.printf(“SD mounted. Size: %lluMB\n”, cardSize);

or

Serial.println(“SD not found”);

but it just doesn’t print anything in my case, meaning that it crashes right after the first line of initSD() which is: if (SD.begin(SD_CS_PIN))

Hi there,

Ok well, some progress.
Try slowing down the SPI speed for the SD card.
#define SPI_FREQUENCY 10000000
Sometimes SD.begin() is where this falls over if another SPI device was already initialized at a much higher clock.

Sometimes , Even though the TFT has no MISO, if its CS is left low or floating when SD.begin() starts, it can still interfere with bus setup.

Add this or something like it before initSD():

#define TFT_CS   2
#define TFT_DC   4
#define TFT_RST  1

void setup() {
  Serial.begin(115200);
  SPI.begin(7, 8, 9, -1);

  pinMode(TFT_CS, OUTPUT);
  digitalWrite(TFT_CS, HIGH);   // deselect TFT

  pinMode(SD_CS_PIN, OUTPUT);
  digitalWrite(SD_CS_PIN, HIGH); // deselect SD too

  initCamera();
  initDisplay();

  digitalWrite(TFT_CS, HIGH);    // make sure TFT stays deselected
  delay(10);

  initSD();

  pinMode(SHUTTER_PIN, INPUT_PULLUP);
}

Before SD.begin(), explicitly set both CS pins as outputs and drive the TFT CS high. Then call SD.begin(21, SPI, 4000000) and lower the TFT SPI frequency to 10 MHz for testing.

here is draft to try see what changes, you have to fully release spi display bus raising the CS and DC

#include <SPI.h>
#include <SD.h>
#include <TFT_eSPI.h>

#define TFT_CS      2
#define TFT_DC      4
#define TFT_RST     1
#define SD_CS_PIN   21

TFT_eSPI tft = TFT_eSPI();

void initDisplay() {
  Serial.println("initDisplay(): start");

  pinMode(TFT_CS, OUTPUT);
  digitalWrite(TFT_CS, HIGH);   // deselect TFT

  pinMode(TFT_DC, OUTPUT);
  pinMode(TFT_RST, OUTPUT);

  delay(10);
  tft.init();
  tft.setRotation(1);
  tft.fillScreen(TFT_BLACK);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextSize(2);
  tft.setCursor(40, 50);
  tft.println("Display OK");

  digitalWrite(TFT_CS, HIGH);   // keep TFT deselected
  Serial.println("initDisplay(): done");
}

bool initSD() {
  Serial.println("initSD(): start");

  pinMode(SD_CS_PIN, OUTPUT);
  digitalWrite(SD_CS_PIN, HIGH);   // deselect SD first
  digitalWrite(TFT_CS, HIGH);      // make sure TFT stays off the bus
  delay(10);

  // Conservative SD startup speed
  bool ok = SD.begin(SD_CS_PIN, SPI, 4000000);

  if (!ok) {
    Serial.println("initSD(): SD.begin failed");
    return false;
  }

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

  if (!SD.exists("/photos")) {
    if (SD.mkdir("/photos")) {
      Serial.println("/photos created");
    } else {
      Serial.println("Failed to create /photos");
    }
  }

  Serial.println("initSD(): done");
  return true;
}

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

  Serial.println("\n=== XIAO ESP32S3 Sense TFT + SD SPI test ===");

  // Shared SPI bus for TFT + Sense SD
  SPI.begin(7, 8, 9, -1);
  Serial.println("SPI.begin done");

  // Put both CS lines in safe state before any library init
  pinMode(TFT_CS, OUTPUT);
  digitalWrite(TFT_CS, HIGH);

  pinMode(SD_CS_PIN, OUTPUT);
  digitalWrite(SD_CS_PIN, HIGH);

  delay(20);

  initDisplay();

  // Re-deselect TFT before touching SD
  digitalWrite(TFT_CS, HIGH);
  delay(20);

  bool sdOK = initSD();

  tft.fillRect(0, 90, 240, 40, TFT_BLACK);
  tft.setCursor(20, 100);
  if (sdOK) {
    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.println("SD OK");
  } else {
    tft.setTextColor(TFT_RED, TFT_BLACK);
    tft.println("SD FAIL");
  }
}

void loop() {
} 

If this still hangs exactly at SD.begin(), then I would stop trying to “fix code” the likely root cause is Seeed’s own hardware routing rule: the Sense microSD slot is tied to the SPI pins through J3, and Seeed says you cannot use the microSD function and the XIAO SPI function at the same time unless you reconfigure J3.

:v:
HTH
GL :slight_smile: PJ :+1:

I have has similar issues trying to get an SD card to share an SPI bus with another SPI device, a LoRa module in my case.

On the XIAO ESP32S3 Sense I have found that some SD cards will prevent the LoRa module initializing on the SPI bus and some SD cards do not interfere with the LoRa module.

Why you can get this interference with some SD cards I do not know and its not an issue specific to the XIAO ESP32S3 Sense either, you get the same issues with an ESP32S3 Camera Dev Board.

For long term reliability it seems to make sense to always run the SD card on its own SPI bus or in MMC mode.

Hey, I ran the test code that you provided and it again hangs at SD.begin().

Here is the serial output:


=== XIAO ESP32S3 Sense TFT + SD SPI test ===
SPI.begin done
initDisplay(): start
initDisplay(): done
initSD(): start

but in this code it says SPI can be used even without reconfiguring J3 just by setting SD_CS_PIN 21.

Hi, I have tried it with multiple freshly formatted SD cards and the issue was still the same.

Does running the SD card on its own SPI bus mean using only the SD card and not the display?

Also, can you please elaborate on how to run the SD card in MMC mode for this board?