Using lvgl without esp_lvgl_port

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

Hi @OWlchanMagican ,

Check this topic: Display ESP32 - ESP32-8048S043 - Screen shake - #2 by halyssonJr

This component was implemented using only the LVGL API.

:slight_smile:

Hello. Thank you very much for the instructions, I double-checked the PSRAM configurations and realized I had set them up incorrectly from the start. For anyone reading this later: if you encounter a similar issue, make sure your PSRAM configuration is correct if you intend to use it for the LVGL buffer.

And here is my PSRAM configuration in sdkconfig for anyone who wishes to refer to it (I took it from the sample example mentioned above).

#

# ESP PSRAM

#

CONFIG_SPIRAM=y

#

# SPI RAM config

#

# CONFIG_SPIRAM_MODE_QUAD is not set

CONFIG_SPIRAM_MODE_OCT=y

# default:

CONFIG_SPIRAM_TYPE_AUTO=y

# default:

# CONFIG_SPIRAM_TYPE_ESPPSRAM64 is not set

# default:

CONFIG_SPIRAM_CLK_IO=30

# default:

CONFIG_SPIRAM_CS_IO=26

# CONFIG_SPIRAM_XIP_FROM_PSRAM is not set

CONFIG_SPIRAM_FETCH_INSTRUCTIONS=y

CONFIG_SPIRAM_RODATA=y

CONFIG_SPIRAM_SPEED_80M=y

# CONFIG_SPIRAM_SPEED_40M is not set

# default:

CONFIG_SPIRAM_SPEED=80

# CONFIG_SPIRAM_ECC_ENABLE is not set

CONFIG_SPIRAM_BOOT_HW_INIT=y

# default:

CONFIG_SPIRAM_BOOT_INIT=y

# default:

CONFIG_SPIRAM_PRE_CONFIGURE_MEMORY_PROTECTION=y

# default:

# CONFIG_SPIRAM_IGNORE_NOTFOUND is not set

# default:

# CONFIG_SPIRAM_USE_MEMMAP is not set

# default:

# CONFIG_SPIRAM_USE_CAPS_ALLOC is not set

# default:

CONFIG_SPIRAM_USE_MALLOC=y

CONFIG_SPIRAM_MEMTEST=y

# default:

CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384

# default:

# CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP is not set

# default:

CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=32768

# default:

# CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY is not set

# default:

# CONFIG_SPIRAM_ALLOW_NOINIT_SEG_EXTERNAL_MEMORY is not set

# end of SPI RAM config

# end of ESP PSRAM

#

# ESP Ringbuf