Page 1 of 1

Make LVGL/esp_lvgl_port to switch/reinitiate between big and little framebuffers

Posted: Mon Jan 05, 2026 11:39 pm
by dmitrij999
Hello everyone!

Usually, the LVGL buffers are located to PSRAM, and this is a bottleneck especially on ESP32-S3 and RGB displays.
If you perform OTA update (at least I tried the Arduino OTA implementation and not tested the ESP-IDF implementation yet, like, esp_ota_begin/esp_ota_end), you can see stripes on the top of interface, which looks like crosstalks and other image artifacts.

I suppose that the problem is related to non-cached Flash access during OTA update and blocking between Flash and PSRAM, hence, ESP LCD module cannot get the buffer access in time.

Therefore, I think of making LVGL switching between PSRAM buffer (full buffer) and little buffer that can be fit into SRAM. I suppose to use a little buffer to show the OTA update progress.
How do I switch buffers for LVGL/esp_lvgl_port?

Here is the my example for LVGL, display and touch init

Code: Select all

#include <stdio.h>
#include "display_ui_init.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "driver/i2c.h"
#include "driver/i2c_master.h"
#include "driver/spi_master.h"
#include "esp_timer.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_check.h"
#include "esp_event.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_lvgl_port.h"
#include "esp_timer.h"

#include "lvgl.h"
#include "lv_demos.h"

#include "esp_lcd_nv3041a.h"
#include "esp_lcd_touch_gt911.h"
#include "ui.h"
#include "screens.h"
#include "actions.h"


static const char *TAG = "example";
static SemaphoreHandle_t lvgl_mux = NULL;

#define LCD_HOST    SPI2_HOST
#define TOUCH_HOST  I2C_NUM_1

#define LCD_BIT_PER_PIXEL                       (16)

#define EXAMPLE_LCD_H_RES                       480
#define EXAMPLE_LCD_V_RES                       272

#define EXAMPLE_LCD_BK_LIGHT_ON_LEVEL           1
#define EXAMPLE_LCD_BK_LIGHT_OFF_LEVEL          !EXAMPLE_LCD_BK_LIGHT_ON_LEVEL
#define EXAMPLE_PIN_NUM_LCD_CS                  (GPIO_NUM_45)
#define EXAMPLE_PIN_NUM_LCD_PCLK                (GPIO_NUM_47)
#define EXAMPLE_PIN_NUM_LCD_DATA0               (GPIO_NUM_21)
#define EXAMPLE_PIN_NUM_LCD_DATA1               (GPIO_NUM_48)
#define EXAMPLE_PIN_NUM_LCD_DATA2               (GPIO_NUM_40)
#define EXAMPLE_PIN_NUM_LCD_DATA3               (GPIO_NUM_39)
#define EXAMPLE_PIN_NUM_LCD_RST                 (-1)
#define EXAMPLE_PIN_NUM_BK_LIGHT                (GPIO_NUM_1)

#define EXAMPLE_PIN_NUM_TOUCH_SCL               (GPIO_NUM_4)
#define EXAMPLE_PIN_NUM_TOUCH_SDA               (GPIO_NUM_8)
#define EXAMPLE_PIN_NUM_TOUCH_RST               (GPIO_NUM_38)
#define EXAMPLE_PIN_NUM_TOUCH_INT               (GPIO_NUM_3)

esp_lcd_touch_handle_t tp = NULL;

static esp_lcd_panel_handle_t lcd_panel = NULL;

static i2c_master_bus_handle_t my_bus = NULL;
static esp_lcd_panel_io_handle_t touch_io_handle = NULL;
static esp_lcd_touch_handle_t touch_handle = NULL;

esp_lcd_panel_io_handle_t io_handle = NULL;

/* LVGL display and touch */
static lv_display_t * lvgl_disp = NULL;
static lv_indev_t * lvgl_touch_indev = NULL;

static bool _display_ui_initialized = false;
static bool _display_bsp_initialized = false;

SemaphoreHandle_t xGuiSemaphore;

static esp_timer_handle_t eez_tick_timer_handle;

typedef enum {
    UI_SCREEN_MAIN,
    UI_SCREEN_OFF_CHARGER,
    UI_SCREEN_SLEEP,
    UI_SCREEN_SHUTDOWN,
    UI_SCREEN_REBOOT
} ui_screen_t;

static ui_screen_t pending_screen;
static bool screen_change_pending = false;

static void (*_on_display_touched)() = NULL;

#define EXAMPLE_LVGL_TICK_PERIOD_MS    10
#define EXAMPLE_LVGL_TASK_MAX_DELAY_MS 500
#define EXAMPLE_LVGL_TASK_MIN_DELAY_MS 1
#define EXAMPLE_LVGL_TASK_STACK_SIZE   (4 * 1024)
#define EXAMPLE_LVGL_TASK_PRIORITY     2

#if LVGL_VERSION_MAJOR >= 9
#define SEND_BUF_SIZE ((EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES \
    * LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565)) / 8)
#else
#define SEND_BUF_SIZE ((EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES \
    * 2) / 10)
#endif

static bool _show_notification = false;

bool get_var_show_notification() { return _show_notification; }
void set_var_show_notification(bool value) { _show_notification = value; }

void display_set_on_display_touched_cb(void (*cb)()) {
    _on_display_touched = cb;
}

void display_set_on_off_state(bool enabled) {
    if (!_display_ui_initialized) {
        ESP_LOGE(TAG, "display not initialized"); 
        return;
    }

    if (enabled) {
        esp_lcd_panel_disp_on_off(lcd_panel, true);
    } else {
        esp_lcd_panel_disp_on_off(lcd_panel, false);
    }

}

static bool IRAM_ATTR example_notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io, esp_lcd_panel_io_event_data_t *edata, void *user_ctx)
{
#if LVGL_VERSION_MAJOR >= 9
    lv_display_t *disp_driver = (lv_display_t *)user_ctx;
#else 
    lv_disp_drv_t *disp_driver = (lv_disp_drv_t *)user_ctx;
#endif

    lv_disp_flush_ready(disp_driver);
    return false;
}

#if LVGL_VERSION_MAJOR >= 9
// static void example_lvgl_flush_cb(lv_display_t *drv, const lv_area_t *area, lv_color_t *color_map)
static void example_lvgl_flush_cb(lv_display_t *drv, const lv_area_t *area, uint8_t *color_map)
{
    esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t)lv_display_get_user_data(drv);
#else
static 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;
#endif

    const int offsetx1 = area->x1;
    const int offsetx2 = area->x2;
    const int offsety1 = area->y1;
    const int offsety2 = area->y2;
    lv_draw_sw_rgb565_swap(color_map, lv_area_get_size(area));
    esp_lcd_panel_draw_bitmap(panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);
}

void process_coordinates(esp_lcd_touch_handle_t tp, uint16_t *x, uint16_t *y, uint16_t *strength, uint8_t *point_num, uint8_t max_point_num) {
    ESP_LOGD(TAG, "Touch detected at %u/%u - (%u,%u)", (*point_num), max_point_num, (*x), (*y));
    if (_on_display_touched) _on_display_touched();
}

#if LVGL_VERSION_MAJOR < 9
static void example_lvgl_update_cb(lv_disp_drv_t *drv)
{
    esp_lcd_panel_handle_t panel_handle = (esp_lcd_panel_handle_t) drv->user_data;

    switch (drv->rotated) {
    case LV_DISPLAY_ROTATION_0:
        esp_lcd_panel_swap_xy(panel_handle, false);
        esp_lcd_panel_mirror(panel_handle, true, false);
        break;
    case LV_DISPLAY_ROTATION_90:
        esp_lcd_panel_swap_xy(panel_handle, true);
        esp_lcd_panel_mirror(panel_handle, true, true);
        break;
    case LV_DISPLAY_ROTATION_180:
        esp_lcd_panel_swap_xy(panel_handle, false);
        esp_lcd_panel_mirror(panel_handle, false, true);
        break;
    case LV_DISPLAY_ROTATION_270:
        esp_lcd_panel_swap_xy(panel_handle, true);
        esp_lcd_panel_mirror(panel_handle, false, false);
        break;
    }
}
#endif


static esp_err_t app_display_init(esp_lcd_panel_handle_t *lp, esp_lcd_panel_io_handle_t * io_handle)
{
    esp_err_t ret = ESP_OK;

    ESP_LOGI(TAG, "Turn off LCD backlight");

    
    gpio_config_t bk_gpio_config = {
        .pin_bit_mask = 1ULL << EXAMPLE_PIN_NUM_BK_LIGHT,
        .mode = GPIO_MODE_OUTPUT
    };

    const spi_bus_config_t buscfg = NV3041A_PANEL_BUS_QSPI_CONFIG(EXAMPLE_PIN_NUM_LCD_PCLK,
                                                                EXAMPLE_PIN_NUM_LCD_DATA0,
                                                                EXAMPLE_PIN_NUM_LCD_DATA1,
                                                                EXAMPLE_PIN_NUM_LCD_DATA2,
                                                                EXAMPLE_PIN_NUM_LCD_DATA3,
                                                                EXAMPLE_LCD_H_RES * EXAMPLE_LCD_V_RES * LCD_BIT_PER_PIXEL / 8);

    const esp_lcd_panel_io_spi_config_t io_config = NV3041A_PANEL_IO_QSPI_CONFIG(EXAMPLE_PIN_NUM_LCD_CS, NULL, NULL);

    nv3041a_vendor_config_t vendor_config = {
        .flags = {
            .use_qspi_interface = 1,
        },
    };

    const esp_lcd_panel_dev_config_t panel_config = {
            .reset_gpio_num = EXAMPLE_PIN_NUM_LCD_RST,
            .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
            // .data_endian = LCD_RGB_DATA_ENDIAN_BIG,
            .bits_per_pixel = LCD_BIT_PER_PIXEL,
            .vendor_config = &vendor_config,
        };

    ESP_GOTO_ON_ERROR(
        gpio_config(&bk_gpio_config), 
        display_init_gpio_init_failed, 
        TAG, 
        "Cannot init GPIO for display backlight!"
    );
    

    ESP_LOGI(TAG, "Initialize SPI bus");
    
    

    ESP_GOTO_ON_ERROR(
        spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO), 
        display_init_spi_init_failed, 
        TAG, 
        "Cannot init display SPI!"
    );
    

    ESP_LOGI(TAG, "Install panel IO");

    ESP_GOTO_ON_ERROR(
        esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, io_handle), 
        display_init_lcd_attach_spi_failed, 
        TAG, 
        "Cannot attach SPI to display!"
    );


    ESP_LOGI(TAG, "Install NV3041A panel driver");
    ESP_GOTO_ON_ERROR(
        esp_lcd_new_panel_nv3041a(*io_handle, &panel_config, lp), 
        display_init_panel_driver_init_failed, 
        TAG, 
        "Cannot init display driver!"
    );

    ESP_GOTO_ON_ERROR(esp_lcd_panel_reset(*lp), display_init_panel_setup_failed, TAG, "Cannot reset panel!");
    ESP_GOTO_ON_ERROR(esp_lcd_panel_init(*lp), display_init_panel_setup_failed, TAG, "Cannot init panel!");
    ESP_GOTO_ON_ERROR(esp_lcd_panel_disp_on_off(*lp, true), display_init_panel_setup_failed, TAG, "Cannot set panel on/off!");

    ESP_LOGI(TAG, "Turn on LCD backlight");
    gpio_set_level(EXAMPLE_PIN_NUM_BK_LIGHT, EXAMPLE_LCD_BK_LIGHT_ON_LEVEL);

    return ESP_OK;
    
display_init_panel_setup_failed:
    if (*lp)
        esp_lcd_panel_del(*lp);
display_init_panel_driver_init_failed:
    esp_lcd_panel_io_del((*io_handle));
display_init_lcd_attach_spi_failed:
    spi_bus_free(LCD_HOST);
display_init_spi_init_failed:
    gpio_reset_pin((gpio_num_t)EXAMPLE_PIN_NUM_BK_LIGHT);
display_init_gpio_init_failed:
    return ret;
}

static esp_err_t app_touch_init(esp_lcd_panel_io_handle_t *tp_io,
                                esp_lcd_touch_handle_t *tp)
{
    esp_err_t ret = ESP_OK;

    ESP_LOGI(TAG, "Initialize I2C master bus");

    i2c_master_bus_handle_t i2c_bus = NULL;

    const i2c_master_bus_config_t bus_cfg = {
            .i2c_port = TOUCH_HOST,   // обычно I2C_NUM_0
            .sda_io_num = EXAMPLE_PIN_NUM_TOUCH_SDA,
            .scl_io_num = EXAMPLE_PIN_NUM_TOUCH_SCL,
            .clk_source = I2C_CLK_SRC_DEFAULT,
            .glitch_ignore_cnt = 7,
            .flags = {
                .enable_internal_pullup = true
            }
        };

    esp_lcd_panel_io_i2c_config_t tp_io_config =
        ESP_LCD_TOUCH_IO_I2C_GT911_CONFIG();
    tp_io_config.scl_speed_hz = 400000;

    const esp_lcd_touch_config_t tp_cfg = {
        .x_max = EXAMPLE_LCD_H_RES,
        .y_max = EXAMPLE_LCD_V_RES,
        .rst_gpio_num = EXAMPLE_PIN_NUM_TOUCH_RST,
        .int_gpio_num = EXAMPLE_PIN_NUM_TOUCH_INT,
        .levels = {
            .reset = 0,
            .interrupt = 0,
        },
        .flags = {
            .swap_xy = 0,
            .mirror_x = 1,
            .mirror_y = 1,
        },
        .process_coordinates = process_coordinates,
    };

    ESP_GOTO_ON_ERROR(
        i2c_new_master_bus(&bus_cfg, &i2c_bus),
        err,
        TAG,
        "Failed to create I2C master bus"
    );

    ESP_LOGI(TAG, "Create I2C panel IO for GT911");
    
    ESP_GOTO_ON_ERROR(
        esp_lcd_new_panel_io_i2c(i2c_bus, &tp_io_config, tp_io),
        err,
        TAG,
        "Failed to create panel IO"
    );

    ESP_LOGI(TAG, "Initialize GT911 touch controller");

    ESP_GOTO_ON_ERROR(
        esp_lcd_touch_new_i2c_gt911(*tp_io, &tp_cfg, tp),
        err,
        TAG,
        "Failed to init GT911"
    );
    
    return ESP_OK;

err:
    if (*tp_io) {
        esp_lcd_panel_io_del(*tp_io);
        *tp_io = NULL;
    }
    if (i2c_bus) {
        i2c_del_master_bus(i2c_bus);
    }
    return ret;
}

static esp_err_t app_lvgl_init(esp_lcd_panel_handle_t *lp, esp_lcd_panel_io_handle_t *io_handle, esp_lcd_touch_handle_t tp,
    lv_display_t **lv_disp, lv_indev_t **lv_touch_indev) {
    ESP_LOGI(TAG, "Initialize LVGL");
    lvgl_port_cfg_t lvgl_cfg = ESP_LVGL_PORT_INIT_CONFIG();
    lvgl_cfg.task_stack = 32768;
    lvgl_cfg.task_affinity = 1;
    lvgl_cfg.timer_period_ms = EXAMPLE_LVGL_TICK_PERIOD_MS;
    ESP_RETURN_ON_ERROR(lvgl_port_init(&lvgl_cfg), TAG, "LVGL port initialization failed");

    ESP_LOGI(TAG, "LVGL port initialized");

    const lvgl_port_display_cfg_t disp_cfg = {
        .io_handle = *io_handle,
        .panel_handle = *lp,
        .buffer_size = SEND_BUF_SIZE,
        .double_buffer = false,
        .trans_size = 16384,
        .hres = EXAMPLE_LCD_H_RES,
        .vres = EXAMPLE_LCD_V_RES,
        .monochrome = false,
        // .color_format = LV_COLOR_FORMAT_RGB565,
#if LVGL_VERSION_MAJOR >= 9
        .color_format = LV_COLOR_FORMAT_RGB565,
#endif
        .flags = {
            .buff_dma = false,
            .buff_spiram = true,
#if LVGL_VERSION_MAJOR >= 9
            // .swap_bytes = true,
#endif
        }
    };


    // lvgl_port_lock(0);
    *lv_disp = lvgl_port_add_disp(&disp_cfg);
    // lvgl_port_unlock();

    ESP_LOGI(TAG, "LVGL port display added");

    // lvgl_disp = lvgl_port_add_disp(&disp_cfg);

    // lvgl_port_lock(0);
    
    // esp_lcd_panel_io_register_event_callbacks(
    //     *io_handle,
    //     &(esp_lcd_panel_io_callbacks_t) {
	// 		example_notify_lvgl_flush_ready
	// 	},
    //     *lv_disp
    // );
    // lvgl_port_unlock();
    
#if LVGL_VERSION_MAJOR >= 9
    lvgl_port_lock(0);

    lv_display_set_user_data(*lv_disp, *lp);
    lv_display_set_flush_cb(*lv_disp, example_lvgl_flush_cb);

    lvgl_port_unlock();

    ESP_LOGI(TAG, "LVGL port callbacks set");
#else
    // lv_obj_set_user_data(*lv_disp, *lp);
    (*lv_disp)->driver->user_data = (*lp);
    (*lv_disp)->driver->flush_cb = example_lvgl_flush_cb;
    (*lv_disp)->driver->drv_update_cb = example_lvgl_update_cb;
#endif

    lv_disp_set_rotation(*lv_disp, LV_DISPLAY_ROTATION_180);

    /* Add touch input (for selected screen) */
    const lvgl_port_touch_cfg_t touch_cfg = {
        .disp = *lv_disp,
        .handle = tp,
    };
    *lv_touch_indev = lvgl_port_add_touch(&touch_cfg);
    assert(*lv_touch_indev != NULL);
    lv_indev_enable(*lv_touch_indev, true);

    ESP_LOGI(TAG, "LVGL port touch added");

    esp_lcd_panel_invert_color((*lp), false);

    ESP_LOGI(TAG, "LCD colors inverted");

    return ESP_OK;
}

static void eez_tick_timer(void * params) {
    
    ui_tick();
    vTaskDelay(1);

    if (screen_change_pending) {
        screen_change_pending = false;

        switch (pending_screen) {
            case UI_SCREEN_MAIN:
                loadScreen(SCREEN_ID_MAIN);
                vTaskDelay(1);             
                break;
            case UI_SCREEN_OFF_CHARGER:
                loadScreen(SCREEN_ID_DEVICE_OFF_CHARGING);
                vTaskDelay(1);
                break;
            case UI_SCREEN_SLEEP:
                loadScreen(SCREEN_ID_DEVICE_COMING_SLEEP);
                vTaskDelay(1);
                break;
            case UI_SCREEN_SHUTDOWN:
                loadScreen(SCREEN_ID_DEVICE_SHUTTING_DOWN);
                vTaskDelay(1);
                break;
            case UI_SCREEN_REBOOT:
                loadScreen(SCREEN_ID_DEVICE_REBOOTING);
                vTaskDelay(1);
                break;
        }
    }
    vTaskDelay(1);
}

void display_bsp_init() {
    if (_display_bsp_initialized) {
        ESP_LOGW(TAG, "Display BSP has been already initialized!");
        return;
    }

    esp_err_t ret = ESP_OK;

    xGuiSemaphore = xSemaphoreCreateMutex();

    ESP_ERROR_CHECK(app_display_init(&lcd_panel, &io_handle));
	assert(lcd_panel != NULL);
	assert(io_handle != NULL);
    ESP_ERROR_CHECK(app_touch_init(&touch_io_handle, &touch_handle));
	assert(touch_handle != NULL);
	assert(touch_io_handle != NULL);
    

    _display_bsp_initialized = true;
}

void display_ui_init(void) {
    if (_display_ui_initialized) {
        ESP_LOGW(TAG, "display_ui_init already initialized");
        return;
    }

    display_bsp_init();

    esp_err_t ret = ESP_OK;

    ESP_LOGI(TAG, "Display LVGL demos");
    
    ESP_ERROR_CHECK(app_lvgl_init(&lcd_panel, &io_handle, touch_handle, &lvgl_disp, &lvgl_touch_indev));

    ESP_LOGI(TAG, "LVGL has been initialized");

    ui_set_lvgl_lock_cb(
        []() {
            lvgl_port_lock(0);
        }
    );

    ui_set_lvgl_unlock_cb(lvgl_port_unlock);

    ui_init();

    vTaskDelay(pdMS_TO_TICKS(100));

    const esp_timer_create_args_t eez_tick_timer_args = {
        .callback = &eez_tick_timer,
        /* argument specified here will be passed to timer callback function */
        //.arg = (void*) periodic_timer,
        .name = "eez tick timer"
    };

    ESP_ERROR_CHECK(esp_timer_create(&eez_tick_timer_args, &eez_tick_timer_handle));
    esp_timer_start_periodic(eez_tick_timer_handle, 100000);

    _display_ui_initialized = true;
    ESP_LOGI(TAG, "LVGL interface init passed");
}

void display_change_screen_main() {
    ESP_LOGI(TAG, "Change screen to main requested");

    pending_screen = UI_SCREEN_MAIN;
    screen_change_pending = true;
}

void display_change_screen_off_charging() {
    ESP_LOGI(TAG, "Change screen to off-charge requested");

    pending_screen = UI_SCREEN_OFF_CHARGER;
    screen_change_pending = true;
}

void display_change_screen_sleep() {
    ESP_LOGI(TAG, "Change screen to sleep requested");

    pending_screen = UI_SCREEN_SLEEP;
    screen_change_pending = true;
}

void display_change_screen_shutdown() {
    ESP_LOGI(TAG, "Change screen to shutdown requested");

    pending_screen = UI_SCREEN_SHUTDOWN;
    screen_change_pending = true;
}

void display_change_screen_rebooting() {
    ESP_LOGI(TAG, "Change screen to reboot requested");

    pending_screen = UI_SCREEN_REBOOT;
    screen_change_pending = true;
}