Conflict between SD and lvgl

I’m using a Waveshere ESP32P4 nano.
Before I initialise lvgl I can read and write files on the builtin TF card reader using sdmmc.
I driving an RPi 5" touch screen. Once lvgl has been initialised I can no longer use the SD reader.
Below is a snippet from the ESP-IDF monitor.

I (2361) STXYfs: Found 2 .mvxy files.
I (2364) STXYfs: Reading /sdcard/bracketNema8.mvxy
I (2368) SD_Routines: Entered Read_Moves
I (2376) SD_Routines: Reading Moves
] (2377) SD_Routines: Read move coords: [0.0    15.5
] (2380) SD_Routines: Read move coords: [-8.0   7.5
] (2385) SD_Routines: Read move coords: [8.0    7.5
] (2389) SD_Routines: Read move coords: [8.0    23.5
] (2394) SD_Routines: Read move coords: [-8.0   23.5
I (2398) STXYfs: read 5 moves
I (2401) LVGL: Starting LVGL task
I (2404) STXYfs: lvgl_port_init done
I (2407) esp_i2c_init: I2C initialized successfully on port 1 (SCL GPIO: 8, SDA GPIO: 7)
I (2415) esp_enable_dsi_phy_power: MIPI DSI PHY Powered on
W (2420) esp_i2c_init: I2C already initialized.

-a lot of irrelevant lines removed-

I (2807) GT911: TouchPad_ID:0x39,0x31,0x31
I (2810) GT911: TouchPad_Config_Version:65
W (2821) esp_i2c_init: I2C already initialized.
I (2822) init_lcd: Raspberry Pi 5inch Touch Display V2 initialized successfully.
I (3326) STXYfs: Run application
I (3326) STXYfs: Set mode of stepper GPIO pins
I (3326) STXYfs: LVGL objects setup
I (3376) STXYfs: winfiles setup
I (3377) STXYfs: Filelist setup
Clicked filelist. It has 3 rows
Clicked row: 1, column: 0
Cell text: /sdcard/GearboxNema8.mvxy
Selected File: /sdcard/GearboxNema8.mvxy (Row: 1)
I (17537) SD_Routines: Entered Read_Moves
E (17641) sdmmc_cmd: sdmmc_read_sectors_dma: sdmmc_send_cmd returned 0x107, failed to get status (0x107)
E (17641) sdmmc_cmd: sdmmc_read_sectors: error 0x107 reading blocks 40961+[0..0]
E (17647) diskio_sdmmc: sdmmc_read_blocks failed (0x107)
E (17652) SD_Routines: Error opening file '/sdcard/GearboxNema8.mvxy': I/O error (Error code 5)

I (17660) STXYfs: read 0 moves
Clicked filelist. It has 3 rows
Clicked row: 1, column: 0

I don’t think it is a power supply problem; the buck converter can supply 2Amps.
If it comes to the worst I can read all the file data into PSRAM before starting lvgl. They are all quite small files.
But my next step in this project was to have a button to start up USB OTG and to connect to my PC to download files to the SD card. That is because the SD card is pretty much inaccessible once the housing encloses the nano.
Have I missed some setting or is there an insurmountable conflict between SD and LCD?

@DickyOZ Every operation on the SD after the LVGL initializes fails? Can you provide a minimal code snippet?

Hi Bruno,
A few code snippets.
1.Main routine setup

void app_main(void)
{
    // Enable 3.3v GPIO configuration
    ESP_LOGI(TAG, "Setting LDO_V04 to 3.3v");
    ESP_ERROR_CHECK(esp_ldo_acquire_channel(&ldo_cfg, &ldo_handle));
    ESP_LOGI(TAG, "Set LDO_V04 to 3.3v OK.");

    // Configure timer
    const esp_timer_create_args_t timer_args = {
        .callback = &timer_callback,
        .name = "periodic_timer"};

    ESP_ERROR_CHECK(esp_timer_create(&timer_args, &timer_handle));
    ESP_LOGI(TAG, "Timer created OK.");

    init_file_list();
    num_files = 0;
     if (Mount_SD() == ESP_OK) List_SD_Files();

    // Initialize LVGL and LCD
    lvgl_port_cfg_t lvgl_port_cfg = ESP_LVGL_PORT_INIT_CONFIG();
    lvgl_port_cfg.task_priority = 5;       // Lower it to a mid-tier priority (Default is often high, like 22)
    lvgl_port_cfg.task_stack = 12288;       // Allocate safe stack memory
    lvgl_port_cfg.task_affinity = 1;       
    lvgl_port_init(&lvgl_port_cfg);
    ESP_LOGI(TAG, "lvgl_port_init done");

    esp_err_t err = init_lcd();
    if (err != ESP_OK)
    {
        ESP_LOGE(TAG, "LCD initialization failed; not running APP");
        while (true)
            vTaskDelay(pdMS_TO_TICKS(1000));
    }

    vTaskDelay(pdMS_TO_TICKS(500)); // Wait for half a second to ensure LCD is ready

The header file for my prototype SD routines:

#pragma once
#define MAX_FILES 20
#define MAX_FILENAME_LEN 100
#define MAX_MOVES 50
#define MAX_MOVE_LEN 20

typedef struct
{
    char filename[MAX_FILENAME_LEN];
    char moves[MAX_MOVES][MAX_MOVE_LEN];
} mvxyData;

void list_directory_recursive(char *dir_path, int indent_level);
void List_SD_Files();
void init_file_list();
void update_filename(int index, const char *new_name);
int Read_Moves (int);
void SD_Task (void *pvParameters);
esp_err_t Mount_SD();
void Unmount_SD();

extern mvxyData* SDfileData;
extern int num_files;
extern int num_moves;
extern const char* selectedFile;

And the routines themselves (I decided to use PSRAM for storage because some files can be quite big in future.

#include "sdkconfig.h"
#include "esp_log.h"
#include "esp_vfs_fat.h"
#include "driver/sdmmc_host.h"
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_ldo_regulator.h" // Required for Waveshare P4 LDO power control
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include "SD_routines.h"

static const char *TAG = "SD_Routines";

#define MOUNT_POINT "/sdcard"
static char *mount_point = "/sdcard";
sdmmc_card_t *card;

int num_files = 0;
int num_moves = 0;
const char* selectedFile = NULL;
mvxyData* SDfileData;

	esp_ldo_channel_handle_t ldo_sdio = NULL;
	esp_ldo_channel_config_t ldo_sdio_config = {
			.chan_id = 4, // GPIO / channel used for board power
			.voltage_mv = 3300,
	};
	//Configure VFS (Virtual File System)
	esp_vfs_fat_sdmmc_mount_config_t mount_config = {
			.format_if_mount_failed = false,
			.max_files = 5,
			.allocation_unit_size = 16 * 1024};
	sdmmc_host_t host = SDMMC_HOST_DEFAULT();
	sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();

bool ends_with(const char *str, const char *ending)
{
	printf("String %s looking for endng %s)\n", str, ending);

	if (str == NULL || ending == NULL)
		return false;

	size_t str_len = strlen(str);
	size_t ending_len = strlen(ending);

	// If the ending is longer than the string, it can't end with it
	if (ending_len > str_len)
		return false;

	// Compare the end of the string with the ending string
	return (strncmp(str + str_len - ending_len, ending, ending_len) == 0);
}

void replace_char(char *str, char old_char, char new_char) {
    while (*str) {
        if (*str == old_char) {
            *str = new_char;
        }
        str++; // Move to the next character pointer
    }
}

void init_file_list()
{
	SDfileData = (mvxyData*)heap_caps_malloc(sizeof(mvxyData) * MAX_FILES, MALLOC_CAP_SPIRAM);

	//When necessary the PSRAM can be freed by:
	//free(SDfileData);
}

// Update function now demands you tell it which list to modify
void update_filename(int index, const char *new_name)
{
	if (index < 0 || index >= MAX_FILES)
	{
		ESP_LOGE(TAG, "SDfileData is null or index out of bounds: %d", index);
		return;
	}

	// Safely copy the text into the specified list slot
	
strncpy(SDfileData[index].filename, new_name, MAX_FILENAME_LEN - 1);
SDfileData[index].filename[MAX_FILENAME_LEN - 1] = '\0';}

void list_directory_recursive(char *dir_path, int indent_level)
{
	static const char *TAG = "sd_tree";

	DIR *dir = opendir(dir_path);
	if (dir == NULL)
	{
		ESP_LOGE(TAG, "Failed to open directory: %s", dir_path);
		return;
	}

	struct dirent *entry;
	char path[256];
	struct stat entry_stat;

	while ((entry = readdir(dir)) != NULL)
	{
		vTaskDelay(pdMS_TO_TICKS(100));
		// Skip current directory "." and parent directory ".." pointers
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
		{
			continue;
		}

		snprintf(path, sizeof(path), "%s/%s", dir_path, entry->d_name);

		// Get file/folder status metadata
		if (stat(path, &entry_stat) == 0)
		{
			// Create a visual indentation anchor for readability
			for (int i = 0; i < indent_level; i++)
			{
				printf("  ");
			}

			// Check if the current path is a directory
			if (S_ISDIR(entry_stat.st_mode))
			{
				printf("📁 [%s]\n", entry->d_name);
				// Recursively descend into the subfolder, increasing the indent
				list_directory_recursive(path, indent_level + 1);
			}
			else
			{
				// It is a regular file; print name and size in bytes and add to list
				// Only add .mvxy files to the list
				printf("📄 %s (%ld bytes)\n", entry->d_name, entry_stat.st_size);
				if (!ends_with(entry->d_name, ".mvxy"))
				{
					ESP_LOGI(TAG, "Skipping non .mvxy file: %s", entry->d_name);
				}
				else
				{
					ESP_LOGI(TAG, "Adding filename: %s to Array index %d", path, num_files);
					update_filename(num_files, path);
					int c = Read_Moves(num_files);
					ESP_LOGI(TAG, "Added: %d moves", c);
					num_files++;
				}
			}
		}
	}

	closedir(dir);
}

int Read_Moves (int fn)
{
	const char* mvfl = SDfileData[fn].filename;
	num_moves = 0;
	char line[MAX_MOVE_LEN];
	const char *filePath = mvfl;

	FILE *f = fopen(filePath, "r");
	if (f == NULL) {
		ESP_LOGE(TAG, "Error opening file '%s': %s (Error code %d)\n", 
            filePath, strerror(errno), errno);
	}
	else
	{
		num_moves = 0;
		while (fgets(line, sizeof(line), f) != NULL) {
			// Remove the trailing newline character, if present
			replace_char(line, '\t', ' ');
			size_t len = strlen(line);
			if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0';
			ESP_LOGI(TAG, "Read move coords: [%s]", line);
			strncpy(SDfileData[fn].moves[num_moves], line, MAX_MOVE_LEN);
			num_moves++;
			strncpy(SDfileData[fn].moves[num_moves], "\0", MAX_MOVE_LEN);
		}

		fclose(f);
	}

	return num_moves;
}

void List_SD_Files()
{
	ESP_LOGI(TAG, "Listing files");

	ESP_LOGI(TAG, "Card info");
	sdmmc_card_print_info(stdout, card);

	// List folders and files
	int level = 0;
	list_directory_recursive(mount_point, level);
	ESP_LOGI(TAG, "List of filename found on SD card:");
	for (int i = 0; i < num_files; i++)
	{
		ESP_LOGI(TAG, "Filename[%d] = %s", i, SDfileData[i].filename);
	}
	strcpy(SDfileData[num_files].filename, "\0");
}

esp_err_t Mount_SD()
{
	ESP_LOGI(TAG, "Initializing TF card...");
		esp_ldo_acquire_channel(&ldo_sdio_config, &ldo_sdio);
		host.max_freq_khz = 10000;
		host.flags &= ~SDMMC_HOST_FLAG_DDR;
		slot_config.width = 4; // 4-bit mode for fast access
		esp_err_t ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card);

		if (ret != ESP_OK) {
			if (ret == ESP_FAIL) {
				ESP_LOGE(TAG, "Failed to mount filesystem. If the card is unformatted, set format_if_mount_failed = true.");
			} else {
				ESP_LOGE(TAG, "Failed to initialize the card (%s).", esp_err_to_name(ret));
			}
		}

		ESP_LOGI(TAG, "Card mounted successfully");
		return ret;
	}
void Unmount_SD()
{
	// Unmount the card when done
	esp_err_t err = esp_vfs_fat_sdcard_unmount(mount_point, card);
	if (err != ESP_OK)
	{
		ESP_LOGE(TAG, "Failed to unmount SD card (%s)", esp_err_to_name(err));
	}
	else
	{
		ESP_LOGI(TAG, "SD card unmounted successfully");
	}
}

The tail end of MAIN. If I try to read any time after the port unlock the read fails.

       lv_obj_add_flag(winfiles, LV_OBJ_FLAG_HIDDEN);

        for (int i = 0; i < num_files; i++)
        {
            lv_table_set_cell_value(Filelist, i, 0, SDfileData[i].filename);
        }

        lvgl_port_unlock();
        
        // int c = Read_Moves(0);  //Only here to test if fs can be used while lvgl is running
        // ESP_LOGI(TAG, "Read %d Moves", c);
    }
    else
    {
        ESP_LOGE(TAG, "LCD initialization failed | not running APP");
        while (true) vTaskDelay(pdMS_TO_TICKS(1000));
    }

    while (true)
        vTaskDelay(pdMS_TO_TICKS(100));
}

If I try to read a file after the port unlock I get this error:

E (3499) sdmmc_cmd: sdmmc_read_sectors_dma: sdmmc_send_cmd returned 0x107, failed to get status (0x107)
E (3499) sdmmc_cmd: sdmmc_read_sectors: error 0x107 reading blocks 40960+[0..0]
E (3504) diskio_sdmmc: sdmmc_read_blocks failed (0x107)
E (3509) SD_Routines: Error opening file '/sdcard/bracketNema8.mvxy': I/O error (Error code 5)

I (3517) STXYfs: Read 0 Moves

This is the monitor output:

I (28) boot: ESP-IDF v6.0.1 2nd stage bootloader
I (28) boot: compile time Jun 29 2026 20:10:15
I (28) boot: Multicore bootloader
I (30) boot: chip revision: v1.3
I (31) boot: efuse block revision: v0.3
I (35) qio_mode: Enabling default flash chip QIO
I (39) boot.esp32p4: SPI Speed      : 40MHz
I (43) boot.esp32p4: SPI Mode       : QIO
I (47) boot.esp32p4: SPI Flash Size : 16MB
I (51) boot: Enabling RNG early entropy source...
I (55) boot: Partition Table:
I (58) boot: ## Label            Usage          Type ST Offset   Length
I (64) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (71) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (77) boot:  2 factory          factory app      00 00 00010000 00a00000
I (84) boot:  3 storage          Unknown data     01 82 00a10000 00500000
I (91) boot: End of partition table
I (94) esp_image: segment 0: paddr=00010020 vaddr=48090020 size=3c620h (247328) map
I (140) esp_image: segment 1: paddr=0004c648 vaddr=30100000 size=00068h (   104) load
I (142) esp_image: segment 2: paddr=0004c6b8 vaddr=4ff00000 size=03960h ( 14688) load
I (148) esp_image: segment 3: paddr=00050020 vaddr=48000020 size=891e0h (561632) map
I (240) esp_image: segment 4: paddr=000d9208 vaddr=4ff03960 size=0d400h ( 54272) load
I (251) esp_image: segment 5: paddr=000e6610 vaddr=4ff10d80 size=03f4ch ( 16204) load
I (259) boot: Loaded app from partition at offset 0x10000
I (260) boot: Disabling RNG early entropy source...
I (271) hex_psram: vendor id    : 0x0d (AP)
I (271) hex_psram: Latency      : 0x01 (Fixed)
I (271) hex_psram: DriveStr.    : 0x00 (25 Ohm)
I (272) hex_psram: dev id       : 0x03 (generation 4)
I (277) hex_psram: density      : 0x07 (256 Mbit)
I (281) hex_psram: good-die     : 0x06 (Pass)
I (285) hex_psram: SRF          : 0x02 (Slow Refresh)
I (290) hex_psram: BurstType    : 0x00 ( Wrap)
I (294) hex_psram: BurstLen     : 0x03 (2048 Byte)
I (299) hex_psram: BitMode      : 0x01 (X16 Mode)
I (303) hex_psram: Readlatency  : 0x04 (14 cycles@Fixed)
I (308) hex_psram: DriveStrength: 0x00 (1/1)
I (312) MSPI Timing: Enter psram timing tuning
I esp_psram: Found 32MB PSRAM device
I esp_psram: Speed: 200MHz
I (506) mmu_psram: .rodata xip on psram
I (550) mmu_psram: .text xip on psram
I (551) hex_psram: psram CS IO is dedicated
I (551) cpu_start: Multicore app
I (999) esp_psram: SPI SRAM memory test OK
I (1008) cpu_start: GPIO 38 and 37 are used as console UART I/O pins
I (1008) cpu_start: Pro cpu start user code
I (1009) cpu_start: cpu freq: 360000000 Hz
I (1011) app_init: Application information:
I (1015) app_init: Project name:     STXYfs
I (1018) app_init: App version:      1
I (1022) app_init: Compile time:     Jun 29 2026 20:08:56
I (1027) app_init: ELF file SHA256:  440af1623...
I (1031) app_init: ESP-IDF:          v6.0.1
I (1035) efuse_init: Min chip rev:     v0.0
I (1039) efuse_init: Max chip rev:     v1.99 
I (1043) efuse_init: Chip rev:         v1.3
I (1047) heap_init: Initializing. RAM available for dynamic allocation:
I (1054) heap_init: At 4FF16C60 len 00024360 (144 KiB): RETENT_RAM
I (1060) heap_init: At 4FF3AFC0 len 00004BF0 (18 KiB): RAM
I (1065) heap_init: At 4FF40000 len 00040000 (256 KiB): RAM
I (1070) heap_init: At 30100068 len 00001F98 (7 KiB): SPM
I (1075) esp_psram: Adding pool of 31936K of PSRAM memory to heap allocator
I (1082) esp_psram: Adding pool of 27K of PSRAM memory gap generated due to end address alignment of irom to the heap allocator
I (1093) esp_psram: Adding pool of 14K of PSRAM memory gap generated due to end address alignment of drom to the heap allocator
I (1104) spi_flash: detected chip: gd
I (1108) spi_flash: flash io: qio
I (1111) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (1117) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (1124) main_task: Started on CPU0
I (1128) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (1135) main_task: Calling app_main()
I (1138) STXYfs: Setting LDO_V04 to 3.3v
I (1142) STXYfs: Set LDO_V04 to 3.3v OK.
I (1146) STXYfs: Timer created OK.
I (1149) SD_Routines: Initializing TF card...
I (1356) SD_Routines: Card mounted successfully
I (1356) SD_Routines: Listing files
I (1356) SD_Routines: Card info
Name: SS08G
Type: SDHC
Speed: 10.00 MHz (limit: 10.00 MHz)
Size: 7580MB
CSD: ver=2, sector_size=512, capacity=15523840 read_bl_len=9
SSR: bus_width=4
📁 [System Volume Information]
  📄 WPSettings.dat (12 bytes)
String WPSettings.dat looking for endng .mvxy)
I (1577) sd_tree: Skipping non .mvxy file: WPSettings.dat
  📄 IndexerVolumeGuid (76 bytes)
String IndexerVolumeGuid looking for endng .mvxy)
I (1678) sd_tree: Skipping non .mvxy file: IndexerVolumeGuid
📄 bracketNema8.mvxy (50 bytes)
String bracketNema8.mvxy looking for endng .mvxy)
I (1783) sd_tree: Adding filename: /sdcard/bracketNema8.mvxy to Array index 0
] (1790) SD_Routines: Read move coords: [0.0 15.5
] (1790) SD_Routines: Read move coords: [-8.0 7.5
] (1795) SD_Routines: Read move coords: [8.0 7.5
] (1799) SD_Routines: Read move coords: [8.0 23.5
] (1804) SD_Routines: Read move coords: [-8.0 23.5
I (1809) sd_tree: Added: 5 moves
📄 ESP32-P4_NANO Dimensions.jpg (276575 bytes)
String ESP32-P4_NANO Dimensions.jpg looking for endng .mvxy)
I (1912) sd_tree: Skipping non .mvxy file: ESP32-P4_NANO Dimensions.jpg
📄 ESP32-P4_NANO pins.jpg (349785 bytes)
String ESP32-P4_NANO pins.jpg looking for endng .mvxy)
I (2017) sd_tree: Skipping non .mvxy file: ESP32-P4_NANO pins.jpg
📄 ESP32-P4_NANO.jpg (351315 bytes)
String ESP32-P4_NANO.jpg looking for endng .mvxy)
I (2128) sd_tree: Skipping non .mvxy file: ESP32-P4_NANO.jpg
📄 GearboxNema8.mvxy (34 bytes)
String GearboxNema8.mvxy looking for endng .mvxy)
I (2234) sd_tree: Adding filename: /sdcard/GearboxNema8.mvxy to Array index 1
] (2238) SD_Routines: Read move coords: [0.0 21.0
] (2242) SD_Routines: Read move coords: [20.5 13.50
] (2246) SD_Routines: Read move coords: [32.25 10.5
I (2251) sd_tree: Added: 3 moves
📄 TF Card example code.txt (4518 bytes)
String TF Card example code.txt looking for endng .mvxy)
I (2358) sd_tree: Skipping non .mvxy file: TF Card example code.txt
I (2362) SD_Routines: List of filename found on SD card:
I (2367) SD_Routines: Filename[0] = /sdcard/bracketNema8.mvxy
I (2372) SD_Routines: Filename[1] = /sdcard/GearboxNema8.mvxy
I (2378) STXYfs: lvgl_port_init done
I (2378) LVGL: Starting LVGL task
I (2381) esp_i2c_init: I2C initialized successfully on port 1 (SCL GPIO: 8, SDA GPIO: 7)
I (2392) esp_enable_dsi_phy_power: MIPI DSI PHY Powered on
W (2397) esp_i2c_init: I2C already initialized.
I (2403) esp_display_new_with_handles: Install (RPi 5inch) MIPI DSI LCD control panel
I (2448) ili9881c: ID1: 0x98, ID2: 0x81, ID3: 0x1c
I (2769) esp_display_new_with_handles: Raspberry Pi 5inch Touch Display V2 initialized
W (2770) esp_i2c_init: I2C already initialized.
I (2770) lcd_touch_new: I2C bus handle: 0x480ce244
I (2774) GT911: I2C address initialization procedure skipped - using default GT9xx setup
I (2782) GT911: TouchPad_ID:0x39,0x31,0x31
I (2785) GT911: TouchPad_Config_Version:65
W (2797) esp_i2c_init: I2C already initialized.
I (2797) init_lcd: Raspberry Pi 5inch Touch Display V2 initialized successfully.
I (3301) STXYfs: Run application
I (3326) STXYfs: Set mode of stepper GPIO pins
I (3326) STXYfs: LVGL objects setup
I (3385) STXYfs: winfiles setup
I (3387) STXYfs: Filelist setup

Thank you for the code! Its strange the error code 5. I’ll try running on my side if I can. Just to be sure, can you monitor the memory usage? Another thing is trying to use different memories for lvgl and the SD code, just to test use the iram for the lvgl draw buffer. DMA can be very sensitive.

The project was developed from an example by Yasir K. Qureshi to adapt the esp32-p4 code to drive an RPi 5" V2 LCD. Looking through the code it does not appear to specifically declare an lvgl buffer. I don’t know what would be used by default.
I could try something like:

 	int buffer_size = LCD_H_RES * LCD_V_RES /2;
  buf = (uint32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
  buf1 =(uint32_t *)heap_caps_malloc(buffer_size, MALLOC_CAP_SPIRAM);
  assert(buf);
  assert(buf1);


etc....

Any suggestions?

You probably have something like this if you’re using ESP-IDF:

static esp_err_t lcd_panel_init(esp_lcd_panel_handle_t *handle)
{
    ESP_LOGI(TAG, "Initialize RGB panel");
    const esp_lcd_rgb_panel_config_t conf =
    {
        .clk_src = LCD_CLK_SRC_DEFAULT,
        .timings = LCD_PANEL_TIMING(),
        .data_width = 16,
        .num_fbs = LCD_RGB_BUFFER_NUMS,
        .bounce_buffer_size_px = LCD_H_RES * LCD_RGB_BOUNCE_BUFFER_HEIGHT,
        .hsync_gpio_num = LCD_GPIO_HSYNC,
        .vsync_gpio_num = LCD_GPIO_VSYNC,
        .de_gpio_num = LCD_GPIO_DE,
        .pclk_gpio_num = LCD_GPIO_PCLK,
        .disp_gpio_num = LCD_GPIO_DISP,
        .data_gpio_nums = LCD_GPIO_DATA(),
        .flags.fb_in_psram = 1,
    };

Here you can configure the framebuffer, including how many framebuffers to use and whether they’re allocated in PSRAM. Check the panel initialization.