LVGL config optimization to increase FPS

Description

Hi all, i’m trying to use a 1024x600 sized screen with double frame buffer, i did all the optimizations possible with esp lcd rgb library, i managed to get 15 fps.
In sdkconfig i checked “lvgl minimal configuration”, i’m trying to see if there’s something i can do with my code (eg the flush_cb) or my configuration to get more fps.

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

esp32-s3-wroom1-n16r8, i’ am using the esp-idf v5.0 so the gcc is the 11 version

What LVGL version are you using?

v.8.2.0

What do you want to achieve?

I would like to achieve 21 fps, the maximum possible (because my refresh rate is at the moment 21 Hz)

What have you tried so far?

I tried different configurations in lvgl and also tried to write a different flush_cb, but without success

Code to reproduce

Here i add my lcd.h and lcd.c

#pragma once


#include "lvgl.h"
#include "utils.h"
#include "esp_timer.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_lcd_touch.h"
#include "esp_lcd_touch_gt911.h"
#include "esp_lvgl_port.h"


// SE DOBBIAMO AGGIORNARE GRANDI QUANTITA' DI PIXEL, DOUBLE BUFFER E' PERFETTO PERCHE' NON VEDO MALE NIENTE MA HO POCHI FPS DI DEFAULT
// SE DOBBIAMO AGGIORNARE PICCOLE QUANTITA' DI PIXEL E LA SCHERMATA E' PER LA MAGGIOR PARTE STATICA, ALLORA UTILIZZIAMO OBBLIGATORIAMENTE DUE BOUNCE BUFFER (CON QUESTA MODALITà POSSIAMO ALZARE MHZ)

// se CONFIG_EXAMPLE_DOUBLE_FB è 1 sto usando un double frame buffer, altrimenti sto utilizzando il bounce buffer
#define CONFIG_EXAMPLE_DOUBLE_FB								1
#define CONFIG_EXAMPLE_AVOID_TEAR_EFFECT_WITH_SEM				0

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////// Please update the following configuration according to your LCD spec //////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#define SETTE_POLLICI 0
#define DIECI_POLLICI 1

#define SELECTED_TFT DIECI_POLLICI

#if SELECTED_TFT == SETTE_POLLICI

// TODO VEDERE COME VA CON IL SETTE POLLICI BOUNCE BUFFER
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ     (16 * 1000 * 1000)
#define EXAMPLE_LCD_H_RES              800
#define EXAMPLE_LCD_V_RES              480
#define HSYNC_BACK_PORCH 43
#define HSYNC_FRONT_PORCH 8
#define HSYNC_PULSE_WIDTH 4
#define VSYNC_BACK_PORCH 12
#define VSYNC_FRONT_PORCH 37
#define VSYNC_PULSE_WIDTH 37

#define MS_BETWEEN_FRAMES 30



#elif SELECTED_TFT == DIECI_POLLICI

#if CONFIG_EXAMPLE_DOUBLE_FB
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ     (16 * 1000 * 1000)
#else
#define EXAMPLE_LCD_PIXEL_CLOCK_HZ     (30 * 1000 * 1000)
#endif


#define EXAMPLE_LCD_H_RES              1024
#define EXAMPLE_LCD_V_RES              600
#define HSYNC_BACK_PORCH 160
#define HSYNC_FRONT_PORCH 16
#define HSYNC_PULSE_WIDTH 1
#define VSYNC_BACK_PORCH 23
#define VSYNC_FRONT_PORCH 12
#define VSYNC_PULSE_WIDTH 1

#if CONFIG_EXAMPLE_DOUBLE_FB
#define MS_BETWEEN_FRAMES 38
#else
#define MS_BETWEEN_FRAMES 16 // lo schermo può andare a 60 fps a livello teorico
#endif
#endif


#define EXAMPLE_PIN_NUM_HSYNC          6
#define EXAMPLE_PIN_NUM_VSYNC          7
#define EXAMPLE_PIN_NUM_DE             15
#define EXAMPLE_PIN_NUM_PCLK           21

#define EXAMPLE_PIN_NUM_DATA0          42 // B3
#define EXAMPLE_PIN_NUM_DATA1          44 // B4
#define EXAMPLE_PIN_NUM_DATA2          43 // B5
#define EXAMPLE_PIN_NUM_DATA3          2 // B6
#define EXAMPLE_PIN_NUM_DATA4          1 // B7



#define EXAMPLE_PIN_NUM_DATA5          14 // G2
#define EXAMPLE_PIN_NUM_DATA6          46 // G3
#define EXAMPLE_PIN_NUM_DATA7          38 // G4
#define EXAMPLE_PIN_NUM_DATA8          39 // G5
#define EXAMPLE_PIN_NUM_DATA9          40 // G6
#define EXAMPLE_PIN_NUM_DATA10         41 // G7

#define EXAMPLE_PIN_NUM_DATA11         47  // R3
#define EXAMPLE_PIN_NUM_DATA12         48  // R4
#define EXAMPLE_PIN_NUM_DATA13         45 // R5
#define EXAMPLE_PIN_NUM_DATA14         0 // R6
#define EXAMPLE_PIN_NUM_DATA15         17 // R7

#define EXAMPLE_PIN_NUM_DISP_EN        -1

#define EXAMPLE_LVGL_TICK_PERIOD_MS    2

extern pthread_mutex_t lvgl_mutex;

void increase_lvgl_tick(void *arg);
void example_lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map);
bool example_on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data);

void lcd_init();

void lvgl_task(void *pvParameters);
#include "lcd.h"
#include "i2c.h"


static const char *TAG = "LCD SCREEN";

#include "driver/gpio.h"
#include "utils.h"
#include "esp_system.h"

#include "esp_heap_caps.h"
#include "esp_system.h"



void *buf1 = NULL;
void *buf2 = NULL;

pthread_mutex_t lvgl_mutex;

// we use two semaphores to sync the VSYNC event and the LVGL task, to avoid potential tearing effect
#if CONFIG_EXAMPLE_AVOID_TEAR_EFFECT_WITH_SEM
SemaphoreHandle_t sem_vsync_end;
SemaphoreHandle_t sem_gui_ready;
#endif


bool example_on_vsync_event(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_data)
{
	BaseType_t high_task_awoken = pdFALSE;
#if CONFIG_EXAMPLE_AVOID_TEAR_EFFECT_WITH_SEM
	if (xSemaphoreTakeFromISR(sem_gui_ready, &high_task_awoken) == pdTRUE) {
		xSemaphoreGiveFromISR(sem_vsync_end, &high_task_awoken);
	}
#endif
	return high_task_awoken == pdTRUE;
}



void example_lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
{
	
	esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data;
	int offsetx1 = area->x1;
	int offsetx2 = area->x2;
	int offsety1 = area->y1;
	int offsety2 = area->y2;
	
	
#if CONFIG_EXAMPLE_AVOID_TEAR_EFFECT_WITH_SEM
	xSemaphoreGive(sem_gui_ready);
	xSemaphoreTake(sem_vsync_end, portMAX_DELAY);
#endif
	
	esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
	lv_disp_flush_ready(drv); 
}

void increase_lvgl_tick(void *arg)
{
	/* Tell LVGL how many milliseconds has elapsed */
	lv_tick_inc(EXAMPLE_LVGL_TICK_PERIOD_MS);
}



void lcd_init()
{
	
	static lv_disp_draw_buf_t disp_buf; // contains internal graphic buffer(s) called draw buffer(s)
	static lv_disp_drv_t disp_drv; // contains callback functions

#if CONFIG_EXAMPLE_AVOID_TEAR_EFFECT_WITH_SEM
	ESP_LOGI(TAG, "Create semaphores");
	sem_vsync_end = xSemaphoreCreateBinary();
	assert(sem_vsync_end);
	sem_gui_ready = xSemaphoreCreateBinary();
	assert(sem_gui_ready);
#endif
	

	ESP_LOGI(TAG, "Install RGB LCD panel driver");
	esp_lcd_panel_handle_t panel_handle = NULL;
	esp_lcd_rgb_panel_config_t panel_config = {
		.data_width = 16,
		.psram_trans_align = 64,
		.clk_src = LCD_CLK_SRC_DEFAULT,
#if CONFIG_EXAMPLE_DOUBLE_FB == 0
.bounce_buffer_size_px = 10 * EXAMPLE_LCD_H_RES,
#endif
		.disp_gpio_num = EXAMPLE_PIN_NUM_DISP_EN,
		.pclk_gpio_num = EXAMPLE_PIN_NUM_PCLK,
		.vsync_gpio_num = EXAMPLE_PIN_NUM_VSYNC,
		.hsync_gpio_num = EXAMPLE_PIN_NUM_HSYNC,
		.de_gpio_num = EXAMPLE_PIN_NUM_DE,
		.data_gpio_nums = {
		EXAMPLE_PIN_NUM_DATA0,
		EXAMPLE_PIN_NUM_DATA1,
		EXAMPLE_PIN_NUM_DATA2,
		EXAMPLE_PIN_NUM_DATA3,
		EXAMPLE_PIN_NUM_DATA4,
		EXAMPLE_PIN_NUM_DATA5,
		EXAMPLE_PIN_NUM_DATA6,
		EXAMPLE_PIN_NUM_DATA7,
		EXAMPLE_PIN_NUM_DATA8,
		EXAMPLE_PIN_NUM_DATA9,
		EXAMPLE_PIN_NUM_DATA10,
		EXAMPLE_PIN_NUM_DATA11,
		EXAMPLE_PIN_NUM_DATA12,
		EXAMPLE_PIN_NUM_DATA13,
		EXAMPLE_PIN_NUM_DATA14,
		EXAMPLE_PIN_NUM_DATA15,
	},
		.timings = {
		.pclk_hz = EXAMPLE_LCD_PIXEL_CLOCK_HZ,
		.h_res = EXAMPLE_LCD_H_RES,
		.v_res = EXAMPLE_LCD_V_RES,
		// The following parameters should refer to LCD spec
		.hsync_back_porch = HSYNC_BACK_PORCH,
		.hsync_front_porch = HSYNC_FRONT_PORCH,
		.hsync_pulse_width = HSYNC_PULSE_WIDTH,
			
		.vsync_back_porch = VSYNC_BACK_PORCH,
		.vsync_front_porch = VSYNC_FRONT_PORCH,
		.vsync_pulse_width = VSYNC_PULSE_WIDTH,	
	},

		.flags.fb_in_psram = true,
#if CONFIG_EXAMPLE_DOUBLE_FB
		.flags.double_fb = true,
#else
		.flags.double_fb = false,
#endif 
		
	};
	ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));

	ESP_LOGI(TAG, "Register event callbacks");
	esp_lcd_rgb_panel_event_callbacks_t cbs = {
		.on_vsync = example_on_vsync_event,
	};
	ESP_ERROR_CHECK(esp_lcd_rgb_panel_register_event_callbacks(panel_handle, &cbs, &disp_drv));

	ESP_LOGI(TAG, "Initialize RGB LCD panel");
	ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
	
	
	ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));


	
	// LIVELLO LVGL IN POI, DUNQUE CONFIGURO IL TOUCH
	ESP_LOGI(TAG, "Initialize LVGL library");
	lv_init();
	
	

	ESP_LOGI(TAG, "Allocate separate LVGL draw buffers from PSRAM");
	
	/*
	
	*/
	
#if CONFIG_EXAMPLE_DOUBLE_FB
	ESP_LOGI(TAG, "Use frame buffers as LVGL draw buffers");

	ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2));

	// initialize LVGL draw buffers
	lv_disp_draw_buf_init(&disp_buf, buf1, buf2, EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES);
#else
	buf1 = heap_caps_malloc(EXAMPLE_LCD_H_RES * 100 * sizeof(lv_color_t), MALLOC_CAP_DMA | MALLOC_CAP_32BIT);
	assert(buf1);
	// initialize LVGL draw buffers
	lv_disp_draw_buf_init(&disp_buf, buf1, NULL, EXAMPLE_LCD_H_RES * 100);
#endif
	
	
	ESP_LOGI(TAG, "Register display driver to LVGL");
	lv_disp_drv_init(&disp_drv);
	disp_drv.hor_res = EXAMPLE_LCD_H_RES;
	disp_drv.ver_res = EXAMPLE_LCD_V_RES;
	disp_drv.flush_cb = example_lvgl_flush_cb;
	disp_drv.draw_buf = &disp_buf;
	disp_drv.user_data = panel_handle;
#if CONFIG_EXAMPLE_DOUBLE_FB
	disp_drv.direct_mode = true;
	disp_drv.full_refresh = true; // the full_refresh mode can maintain the synchronization between the two frame buffers
#else
	disp_drv.direct_mode = false;
	disp_drv.full_refresh = false; 
#endif
	lv_disp_t *disp = lv_disp_drv_register(&disp_drv);
	
	
#if SELECTED_TFT == DIECI_POLLICI
	esp_lcd_touch_handle_t tp;

	/* Initialize touch */
	const esp_lcd_touch_config_t tp_cfg = {
		.x_max = EXAMPLE_LCD_H_RES,
		.y_max = EXAMPLE_LCD_V_RES,
		.rst_gpio_num = -1,
		.int_gpio_num = -1,
		.levels = {
		.reset = 0,
		.interrupt = 0,
	},
		.flags = {
		.swap_xy = 0,
		.mirror_x = 0,
		.mirror_y = 0,
	},
	};
	esp_lcd_panel_io_handle_t tp_io_handle = NULL;
	const esp_lcd_panel_io_i2c_config_t tp_io_config = ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
	ESP_ERROR_CHECK(esp_lcd_new_panel_io_i2c((esp_lcd_i2c_bus_handle_t)I2C_NUM_0, &tp_io_config, &tp_io_handle));
	ESP_ERROR_CHECK(esp_lcd_touch_new_i2c_gt911(tp_io_handle, &tp_cfg, &tp));
	assert(tp);

	/* Add touch input (for selected screen) */
	const lvgl_port_touch_cfg_t touch_cfg = {
		.disp = disp,
		.handle = tp,
	};
	
	lvgl_port_add_touch(&touch_cfg);
	
#endif

	
	
	

	ESP_LOGI(TAG, "Install LVGL tick timer");
	// Tick interface for LVGL (using esp_timer to generate 2ms periodic event)
	const esp_timer_create_args_t lvgl_tick_timer_args = {
		.callback = &increase_lvgl_tick,
		.name = "lvgl_tick"
	};
	esp_timer_handle_t lvgl_tick_timer = NULL;
	ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
	ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, EXAMPLE_LVGL_TICK_PERIOD_MS * 1000));

	//ESP_LOGI(TAG, "Display LVGL Scatter Chart");
}


void lvgl_task(void *pvParameters) {
	for (;;) {
		pthread_mutex_lock(&lvgl_mutex);
		lv_timer_handler();
		pthread_mutex_unlock(&lvgl_mutex);
		// raise the task priority of LVGL and/or reduce the handler period can improve the performance
		vTaskDelay(pdMS_TO_TICKS(MS_BETWEEN_FRAMES));
		// The task running lv_timer_handler should have lower priority than that running `lv_tick_inc`
		
	}
}

Screenshot and/or video

In this video there are 3 screens changing every 5 seconds, as you can see, since i’m using double frame buffer, the screen changes perfectly and a do not get artifacts on the screen, the fact is that to maintain syncronization between screens it is mandatory to set “full_refresh” to true on disp_driv.
video of 1024x600

Thank you in advance for your help and you time.

I’m not an expert on this so I’d wait to see what other users here recommend, but it sounds like it could just be that you’re demanding too much from your chip. My 64 MHz MCU struggles rendering simple widgets on a 400x240 monochrome (which is understandable for a weaker chip and fine for my use case, it really only needs to update at a 1 Hz – 2 Hz rate worst case scenario). But a 1024x600 full color RGB with large animated graphics is a huge leap in rendering complexity. If you haven’t done so yet, sending the buffer via DMA may help a bit.

The display is only going to get updated as fast as the default refresh timer is set to. This time is 33 milliseconds. Now since you are only getting 15 fps the rendering you are doing is taking longer than 33 milliseconds so changing this number may or may not help. That being said I see there is a stall for when lv_task_handler gets called. You have the stall set to 16 milliseconds. this is too close to being a divider of the default amount of 33 and it could be causing a delay in the refresh.

say the screen gets refreshed causing the refresh timer to get reset. It is going to be 33 milliseconds before the refresh gets done again. with 16 milliseconds as a stall amount that is going to place you at 32 milliseconds. If you call lv_task_handler and only 32 milliseconds has passed then guess what? you are going to wait another 16 milliseconds before lv_task_handler gets called again making the total time between refreshes 48 milliseconds. That would cause your frame rate to drop to 20 right out of the gate.

My suggestion is to do away with the thread for calling lv_task_handler and calling it after you make a change inside the main program loop. You can either change the default LVGL refresh timer value to a lower number. Or you can leave the default refresh timer value alone and call lv_refr_now before you call lv_task_handler and that will cause an update to occur without the 33 millisecond timer expiring.

Hello, thank you for your answer, by “lv_task_handler” you mean lvgl_task in my code? i tried to remove the task but obviously it does not work, maybe i am missing or misunderstanding something :smiling_face_with_tear:

lv_task_handler is the same as lv_timer_handler.
(lv_task_handler is just and only calling lv_timer_handler).

Thank you, since i saw this on the library i thought i was missing something, but it just literally a stub to the the other method.

Blockquote
My suggestion is to do away with the thread for calling lv_task_handler and calling it after you make a change inside the main program loop.

What do you mean by that? i have to call periodically the timer to make the widgets work, in fact, if i remove it, i see nothing on the screen, would you show me the changes you suggested in my code?
I just started with lvgl so i may need some help to understand well what i have to do.

Thank you