[ESP32/Arduino] TFT_ESPI initialisation prevents further use of SD card

Description

I had an Arduino project in operation for a couple of years that uses an ESP32 devkit v1 board and DFRobot ST7735S display with microSD slot.

Now I have come back to this again to add some new features to sections of the code unrelated to the display or SD card using a newer version of the Arduino IDE (and board packages/libraries). I cannot utilise the display and SD card at the same time if TFT_eSPI has been initialised, where previously this would work.

Something in the period since I originally wrote this project has changed, as rolling back to my previous (working) code has the same issue.

I’ve no idea which direction to go to try and resolve this and my lack of C++ experience limits my deeper diagnostic ability.

What MCU/Processor/Board and compiler are you using?

  • ESP32 Devkit v1 connected to DFRobot ST7735S display
  • Arduino IDE 2.3.7 on Windows 10. Board profile set to DOIT ESP32 DEVKIT V1 from esp32 board package
  • esp32 board package 3.3.5
    Libraries:
  • lvgl 9.4.0
  • TFT_eSPI 2.5.43
  • SPI 3.3.5 (from ESP32 board package)
  • SDFat 2.3.0 (from Arduino libraries)
  • SD 3.3.5 (from ESP32 board package)
  • FS 3.3.5 (from ESP32 board package)

What do you want to achieve?

Use of SD card to display images with LVGL, while also having the SD card available to read text files for other functions.

What have you tried so far?

  • Running LVGL_Arduino example to test the display (works)
  • Running SDFat SdInfo and ReadCSVFile examples to test the SD card (works)
  • Reverting lv_conf.h to lv_conf_template.h content and enabling tft_espi support in that config
  • Rewriting SD access using Arduino SD library
  • Rewriting SD access using FATfs library
  • Changing SPI clock speed in sd.begin() and in TFT_eSPI User_Setup.h
  • Filesystem check of SD card (confirmed FAT32 no errors)
  • Swapping SD card

Using the LVGL_Arduino example as a starting point and adding SD card interfacing:

  • Moving call to sd.begin() to occur before lv_init() - sd.begin() successfully initialises
  • Moving call to sd.begin() to occur after lv_init() but before lv_tft_espi_create() - sd.begin() successfully initialises
  • Moving call to sd.begin() to occur after lv_init() and lv_tft_espi_create() - sd.begin() fails

Code to reproduce

User_setup.h from TFT_eSPI:

#define USER_SETUP_ID 2
#define ST7735_DRIVER
#define TFT_WIDTH  128
#define TFT_HEIGHT 160
#define ST7735_GREENTAB2
#define TFT_CS 5  // Chip select control pin D8
#define TFT_DC 21  // Data Command control pin
#define TFT_RST 22  // Reset pin (could connect to NodeMCU RST, see next line)
#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts
#define SMOOTH_FONT
#define SPI_FREQUENCY  16000000
#define SPI_TOUCH_FREQUENCY  2500000

Modified LVGL Arduino example

/*Using LVGL with Arduino requires some extra steps:
 *Be sure to read the docs here: https://docs.lvgl.io/master/integration/framework/arduino.html  */
#include <lvgl.h>

#include <TFT_eSPI.h>
#include "SdFat.h"

/*To use the built-in examples and demos of LVGL uncomment the includes below respectively.
 *You also need to copy `lvgl/examples` to `lvgl/src/examples`. Similarly for the demos `lvgl/demos` to `lvgl/src/demos`.
 *Note that the `lv_examples` library is for LVGL v7 and you shouldn't install it for this version (since LVGL v8)
 *as the examples and demos are now part of the main LVGL library. */

//#include <examples/lv_examples.h>
//#include <demos/lv_demos.h>

/*Set to your screen resolution and rotation*/
#define TFT_HOR_RES   128
#define TFT_VER_RES   160
#define TFT_ROTATION  LV_DISPLAY_ROTATION_90

// SD card pins
#define PIN_SD_CS 17

/*LVGL draw into this buffer, 1/10 screen size usually works well. The size is in bytes*/
#define DRAW_BUF_SIZE (TFT_HOR_RES * TFT_VER_RES / 10 * (LV_COLOR_DEPTH / 8))
uint32_t draw_buf[DRAW_BUF_SIZE / 4];

struct network {
    char ssid[32] = "";
    char password[32] = "";
};

SdFs sd;
SdFile file;
static lv_fs_drv_t drv;

network read_config_network(bool backup = false) {
    bool open_ok = false;
    if (backup == true) {
        Serial.println(F("READ_NETWORK_CONFIG: Reading backup network config"));
        open_ok = file.open("/backup.cfg");
    } else {
        Serial.println(F("READ_NETWORK_CONFIG: Reading main network config"));
        open_ok = file.open("/main.cfg");
    }
    network netconf;
    if (open_ok) {
        String ssid = "";
        String password = "";
        bool got_ssid = false;
        char line[32];
        while (file.available()) {            
            int n = file.fgets(line, sizeof(line));
            if (n <= 0) {
              Serial.println(F("READ_NETWORK_CONFIG: fgets failed"));
            }
            if (line[n - 1] != '\n' && n == (sizeof(line) - 1)) {
              Serial.println(F("READ_NETWORK_CONFIG: line too long"));
            }
            // remove CR and LF
            if (line[n - 1] == '\n') {
              line[n -1] = 0;
            }
            if (line[n - 2] == '\r') {
              line[n -2] = 0;
            }
            if (got_ssid == false) {
                ssid = line;
                got_ssid = true;
            } else {
                password = line;
                break;
            }
            
        }
        file.close();

        strcpy(netconf.ssid, ssid.c_str());
        strcpy(netconf.password, password.c_str());
        Serial.println(F("READ_NETWORK_CONFIG: Config read ok!"));
        return netconf;
    } else {
        Serial.println(F("READ_NETWORK_CONFIG: Unable to open file"));
        return netconf;
    }

}

// Define LVGL filesystem driver
static void *sd_open(lv_fs_drv_t *drv, const char *path, lv_fs_mode_t mode) {
  (void)drv;
  uint8_t oflag = 0;
  if (mode == LV_FS_MODE_WR) oflag = O_WRONLY | O_CREAT | O_TRUNC;
  else if (mode == LV_FS_MODE_RD) oflag = O_RDONLY;
  else if (mode == (LV_FS_MODE_WR | LV_FS_MODE_RD)) oflag = O_RDWR | O_CREAT;

  SdFile *fp = new SdFile();
  if (!fp->open(path, oflag)) {
    delete fp;
    return NULL;
  }
  return fp;
}

static lv_fs_res_t sd_close(lv_fs_drv_t *drv, void *file_p) {
  (void)drv;
  SdFile *fp = (SdFile *)file_p;
  fp->close();
  delete fp;
  return LV_FS_RES_OK;
}

static lv_fs_res_t sd_read(lv_fs_drv_t *drv, void *file_p, void *buff, uint32_t btr, uint32_t *br) {
  (void)drv;
  SdFile *fp = (SdFile *)file_p;
  *br = fp->read(buff, btr);
  return LV_FS_RES_OK;
}

static lv_fs_res_t sd_seek(lv_fs_drv_t *drv, void *file_p, uint32_t pos, lv_fs_whence_t whence) {
  (void)drv;
  SdFile *fp = (SdFile *)file_p;
  if (whence == LV_FS_SEEK_SET) fp->seekSet(pos);
  else if (whence == LV_FS_SEEK_CUR) fp->seekCur(pos);
  else if (whence == LV_FS_SEEK_END) fp->seekEnd(pos);
  return LV_FS_RES_OK;
}

static lv_fs_res_t sd_tell(lv_fs_drv_t *drv, void *file_p, uint32_t *pos_p) {
  (void)drv;
  SdFile *fp = (SdFile *)file_p;
  *pos_p = fp->curPosition();
  return LV_FS_RES_OK;
}

void init_sd_driver(void) {
    Serial.println(F("Initialising LVGL->SD filesystem driver"));
    lv_fs_drv_init(&drv);
    drv.letter = 'S';
    drv.open_cb = sd_open;
    drv.close_cb = sd_close;
    drv.read_cb = sd_read;
    drv.seek_cb = sd_seek;
    drv.tell_cb = sd_tell;
    lv_fs_drv_register(&drv);
}

bool init_sd(void) {
    Serial.println(F("Initialising SD card"));
    bool ok = false;
        
    if (!(sd.begin(SdSpiConfig(PIN_SD_CS, SHARED_SPI, SD_SCK_MHZ(16))))) {
        Serial.println("SD card failed!");
    } else {               
        Serial.println("SD card OK");
        ok = true;
        init_sd_driver();
    }
    return ok;
}

#if LV_USE_LOG != 0
void my_print( lv_log_level_t level, const char * buf )
{
    LV_UNUSED(level);
    Serial.println(buf);
    Serial.flush();
}
#endif

/* LVGL calls it when a rendered image needs to copied to the display*/
void my_disp_flush( lv_display_t *disp, const lv_area_t *area, uint8_t * px_map)
{
    /*Call it to tell LVGL you are ready*/
    lv_display_flush_ready(disp);
}

/*use Arduinos millis() as tick source*/
static uint32_t my_tick(void)
{
    return millis();
}

void setup()
{
    String LVGL_Arduino = "Hello Arduino! ";
    LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();

    Serial.begin( 115200 );
    Serial.println( LVGL_Arduino );

    lv_init();

    init_sd(); // <-- this init works fine

    /*Set a tick source so that LVGL will know how much time elapsed. */
    lv_tick_set_cb(my_tick);

    /* register print function for debugging */
#if LV_USE_LOG != 0
    lv_log_register_print_cb( my_print );
#endif

    network test_a = read_config_network(false); // <-- this read works because it is before lv_tft_espi_create()

    lv_display_t * disp;
    Serial.println("Example is using TFT_ESPI");
    /*TFT_eSPI can be enabled lv_conf.h to initialize the display in a simple way*/
    disp = lv_tft_espi_create(TFT_HOR_RES, TFT_VER_RES, draw_buf, sizeof(draw_buf));
    lv_display_set_rotation(disp, TFT_ROTATION);

    network test_b = read_config_network(false); // <-- this read fails because it is after lv_tft_espi_create()

    lv_obj_t *label = lv_label_create( lv_screen_active() );
    lv_label_set_text( label, "Hello Arduino, I'm LVGL!" );
    lv_obj_align( label, LV_ALIGN_CENTER, 0, 0 );

    lv_obj_t * logo = lv_image_create(lv_screen_active());
    lv_image_set_src(logo, "S:images/logo.bin"); // <-- this also fails, even with the filesystem driver
    lv_obj_align(logo, LV_ALIGN_TOP_MID, 0, 10);

    Serial.println( "Setup done" );
}

void loop()
{
    lv_timer_handler(); /* let the GUI do its work */
    delay(5); /* let this time pass */
}

Screenshot and/or video

Serial output when running the above example:

02:32:49.297 -> ets Jun  8 2016 00:22:57
02:32:49.297 -> 
02:32:49.297 -> rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
02:32:49.297 -> configsip: 0, SPIWP:0xee
02:32:49.297 -> clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
02:32:49.297 -> mode:DIO, clock div:1
02:32:49.297 -> load:0x3fff0030,len:4744
02:32:49.341 -> load:0x40078000,len:15672
02:32:49.341 -> load:0x40080400,len:3164
02:32:49.341 -> entry 0x4008059c
02:32:49.589 -> Hello Arduino! V9.4.0
02:32:49.589 -> Initialising SD card
02:32:49.589 -> SD card OK
02:32:49.589 -> Initialising LVGL->SD filesystem driver
02:32:49.589 -> READ_NETWORK_CONFIG: Reading main network config
02:32:49.589 -> READ_NETWORK_CONFIG: Config read ok!
02:32:49.589 -> Example is using TFT_ESPI
02:32:50.496 -> READ_NETWORK_CONFIG: Reading main network config
02:32:50.800 -> READ_NETWORK_CONFIG: Unable to open file
02:32:51.138 -> Setup done

I don’t have a fix for you, but I’m pretty sure that the card is using the same bus as either the touch screen or the tft display. I have roughly the same issue, I get a message about it, but it’s not causing any problems at the moment, so I’m not working on it (yet).
You have to set your code to tell it to use different or specific buses I believe.

That should give you a direction to go in, sorry I can’t be more specific.

1 Like

Sorry. I should clarify. That display includes a microSD card slot that shares the same SPI bus as the display. The display and SD card have their own chip select pins.

As they are integrated together, I do not have the option to have them on physically seperate SPI buses.

My fallback on this will likely be to purchase a standalone SD card breakout board and connect it to different SPI pins on the ESP32 board, although it is not an ideal solution.

After recreating User_setup.h from the original TFT-eSPI version, I have managed to resolve this. After comparing it to my original post, I dug a bit deeper.

It seems this will fail as described in my OP if TFT_MISO, TFT_MOSI and TFT_SCLK are not explicitly defined in User_setup.h. It is unclear why this is an issue, as the pins I am using match the VSPI pins for this board.

Note that with the failure mode in my OP, SD card access will be unavailable via the SdFs interface and unavailable to LVGL image creation (via lv_image_set_src() ).

Thanks for your attention.