Hi everyone, I’m starting a project using LVGL with the ESP32-S3 on the ESP-IDF platform. I want to implement LVGL without using esp_lvgl_port (i.e., using only the native LVGL library). Everything works fine when running the sample example, but when implementing it myself and separating the driver logic like this, I encounter errors related to the buffer—or at least, that’s my guess. I’d like to ask if anyone here has tried using esp_lcd_panel_rgb directly with LVGL? How do you handle the flush operation and buffer management?
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/lock.h>
#include <sys/param.h>
#include "disp_port.h"
#include "lvgl.h"
#include "demos/lv_demos.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_lcd_touch_gt911.h"
#include "driver/i2c_master.h"
#include "driver/gpio.h"
#include "driver/ledc.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
// TAG DEBUG
// LCD SIZE
#define MY_DISP_HOR_RES 800
#define MY_DISP_VER_RES 400
// LED BACKGROUND
#define LCD_BACKLIGHT_LEDC_TIMER LEDC_TIMER_0
#define LCD_BACKLIGHT_LEDC_CHANNEL LEDC_CHANNEL_0
#define LCD_PIN_BACKLIGHT 2 // light background
#define EXAMPLE_LVGL_TASK_MAX_DELAY_MS 500
#define EXAMPLE_LVGL_TASK_MIN_DELAY_MS 1000 / CONFIG_FREERTOS_HZ
static const char *TAG = "LCD";
static esp_lcd_panel_handle_t panel_handle = NULL;
static esp_timer_handle_t lvgl_tick_timer = NULL;
static lv_display_t *display = NULL;
static _lock_t lvgl_api_lock;
static TaskHandle_t lvgl_task_handle;
static void *buf1 = NULL;
static void *buf2 = NULL;
// TASK RUNNING LVGL
static void example_lvgl_port_task(void *arg)
{
ESP_LOGI(TAG, "Starting LVGL task");
uint32_t time_till_next_ms = 0;
while (1)
{
// lv_timer_handler() runs animations, input handling, and screen refresh scheduling.
_lock_acquire(&lvgl_api_lock);
time_till_next_ms = lv_timer_handler();
_lock_release(&lvgl_api_lock);
// in case of task watch dog timeout
time_till_next_ms = MAX(time_till_next_ms, EXAMPLE_LVGL_TASK_MIN_DELAY_MS);
// in case of lvgl display not ready yet
time_till_next_ms = MIN(time_till_next_ms, EXAMPLE_LVGL_TASK_MAX_DELAY_MS);
usleep(1000 * time_till_next_ms);
}
}
static bool example_on_frame_buf_complete(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *event_data, void *user_ctx)
{
(void)panel;
(void)event_data;
(void)user_ctx;
BaseType_t need_yield = pdFALSE;
if (lvgl_task_handle)
{
vTaskNotifyGiveFromISR(lvgl_task_handle, &need_yield);
}
return need_yield == pdTRUE;
}
static void example_lvgl_flush_wait_cb(lv_display_t *disp)
{
// In direct-mode double buffering, lv_display_flush_cb() only tells the driver
// which frame buffer should be displayed next. LVGL must then wait until the
// driver finishes switching buffers before it renders the next frame.
if (lv_display_flush_is_last(disp))
{
// Wait until the previous frame buffer is no longer referenced by DMA.
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
}
lv_display_flush_ready(disp);
}
static void lvgl_flush_cb(lv_display_t *display, const lv_area_t *area, uint8_t *px_map)
{
esp_lcd_panel_handle_t panel = (esp_lcd_panel_handle_t)lv_display_get_user_data(display);
int offsetx1 = area->x1;
int offsetx2 = area->x2;
int offsety1 = area->y1;
int offsety2 = area->y2;
if (!lv_display_flush_is_last(display))
{
// LVGL may split one frame into several dirty rectangles. In direct mode,
// switch the hardware frame buffer only after the last rectangle is done.
lv_display_flush_ready(display);
return;
}
offsetx1 = 0;
offsety1 = 0;
offsetx2 = MY_DISP_HOR_RES - 1;
offsety2 = MY_DISP_VER_RES - 1;
// Clear any stale completion from the previous frame before waiting for this frame.
ulTaskNotifyTake(pdTRUE, 0);
esp_lcd_panel_draw_bitmap(panel, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, px_map);
// lv_disp_flush_ready(display);
}
static void lvgl_inc_tick(void *arg)
{
(void)arg;
lv_tick_inc(1);
}
static void register_panel_lcd(void)
{
// SET BACK LIGHT ON LCD
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_LOW_SPEED_MODE,
.duty_resolution = LEDC_TIMER_8_BIT,
.timer_num = LCD_BACKLIGHT_LEDC_TIMER,
.freq_hz = 4000,
.clk_cfg = LEDC_USE_RC_FAST_CLK,
};
ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));
ledc_channel_config_t ledc_channel = {
.gpio_num = LCD_PIN_BACKLIGHT,
.speed_mode = LEDC_LOW_SPEED_MODE,
.channel = LCD_BACKLIGHT_LEDC_CHANNEL,
.intr_type = LEDC_INTR_DISABLE,
.timer_sel = LCD_BACKLIGHT_LEDC_TIMER,
.duty = 0,
.hpoint = 0,
};
ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));
ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_BACKLIGHT_LEDC_CHANNEL, 255));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_BACKLIGHT_LEDC_CHANNEL));
ESP_LOGI(TAG, "Backlight initialized");
// REGISTER LCD PORT CONTROL
const esp_lcd_rgb_panel_config_t panel_config = {
.data_width = 16,
.num_fbs = 2,
.bounce_buffer_size_px = 800 * 10,
.clk_src = LCD_CLK_SRC_PLL160M,
.timings = {
.pclk_hz = (16 * 1000000),
.h_res = MY_DISP_HOR_RES,
.v_res = MY_DISP_VER_RES,
.hsync_pulse_width = 4,
.hsync_back_porch = 8,
.hsync_front_porch = 8,
.vsync_pulse_width = 4,
.vsync_back_porch = 8,
.vsync_front_porch = 8,
.flags = {
.hsync_idle_low = true,
.vsync_idle_low = true,
.de_idle_high = false,
.pclk_active_neg = true,
.pclk_idle_high = false,
},
},
.hsync_gpio_num = GPIO_NUM_39,
.vsync_gpio_num = GPIO_NUM_41,
.de_gpio_num = GPIO_NUM_40,
.pclk_gpio_num = GPIO_NUM_42,
.data_gpio_nums = {
GPIO_NUM_8,
GPIO_NUM_3,
GPIO_NUM_46,
GPIO_NUM_9,
GPIO_NUM_1, // B0-B4
GPIO_NUM_5,
GPIO_NUM_6,
GPIO_NUM_7,
GPIO_NUM_15,
GPIO_NUM_16,
GPIO_NUM_4, // G0-G5
GPIO_NUM_45,
GPIO_NUM_48,
GPIO_NUM_47,
GPIO_NUM_21,
GPIO_NUM_14, // R0-R4
},
.disp_gpio_num = GPIO_NUM_NC,
// .flags = {
// .disp_active_low = false,
// .fb_in_psram = true,
// },
.flags.fb_in_psram = true,
};
ESP_ERROR_CHECK(esp_lcd_new_rgb_panel(&panel_config, &panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
ESP_LOGI(TAG, "register panel lcd success");
}
void register_disp_port_lvgl(void)
{
// REGISTER PANEL
register_panel_lcd();
// INIT LVGL
lv_init();
// TAKE BUFFER
ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(panel_handle, 2, &buf1, &buf2));
if (!buf1 || !buf2)
printf("buffer not available\r\n");
else
printf("buffer get success\r\n");
display = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
lv_display_set_user_data(display, panel_handle);
lv_display_set_color_format(display, LV_COLOR_FORMAT_RGB565);
// ADDING BUFFER IN LVGL
lv_display_set_buffers(display, buf1, buf2, MY_DISP_HOR_RES * MY_DISP_VER_RES * 2, LV_DISPLAY_RENDER_MODE_DIRECT);
// SET FLUSH CALLBACK
lv_display_set_flush_cb(display, lvgl_flush_cb);
lv_display_set_flush_wait_cb(display, example_lvgl_flush_wait_cb);
// EVENT CB PANEL
esp_lcd_rgb_panel_event_callbacks_t cbs = {
.on_frame_buf_complete = example_on_frame_buf_complete,
};
ESP_ERROR_CHECK(esp_lcd_rgb_panel_register_event_callbacks(panel_handle, &cbs, display));
// REGISTER TIMER FOR INC
const esp_timer_create_args_t lvgl_tick_timer_args = {.callback = &lvgl_inc_tick, .name = "lvgl_tick"};
ESP_ERROR_CHECK(esp_timer_create(&lvgl_tick_timer_args, &lvgl_tick_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(lvgl_tick_timer, 1000));
ESP_LOGI(TAG, "Create LVGL task");
// xTaskCreate(example_lvgl_port_task, "LVGL", 8921, NULL, 5, &lvgl_task_handle);
xTaskCreatePinnedToCore(
example_lvgl_port_task,
"LVGL",
8921,
NULL,
4,
&lvgl_task_handle,
1);
// Build the demo UI once the display pipeline is ready.
ESP_LOGI(TAG, "Display LVGL UI");
_lock_acquire(&lvgl_api_lock);
lv_demo_benchmark();
_lock_release(&lvgl_api_lock);
}
// END
Screenshot and/or video
The images below show the results after I reduced the actual screen size and the buffer size.
Environment
- MCU/MPU/Board: esp32s3 in JC8048W5500
- LVGL version: 9.6


