Elecrow 5" - ESP IDF 5.3 (Screen shakes)

I’m trying to get the 5" Elecrow display (SKU: DIS0705H) working, but it’s not functioning properly.

The problem is that the example provided by Elecrow is an old example (ESP IDF 4) and uses Arduino libraries.

I’m trying to make it work properly using Expressif components, avoiding the workaround of using Arduino libraries.

The code that displays the screen is below:

f_elecrow_5In.c:

#include "f_elecrow_5in.h"
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

#include "esp_lcd_panel_rgb.h"
#include "esp_lcd_panel_ops.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_heap_caps.h"

esp_err_t elecrow_5in_init(esp_lcd_panel_handle_t *out_panel)
{
    esp_lcd_rgb_panel_config_t cfg = {
        .clk_src = LCD_CLK_SRC_PLL160M,
        .data_width = 16,
        .bits_per_pixel = 16,

        .de_gpio_num    = GPIO_NUM_40,
        .vsync_gpio_num = GPIO_NUM_41,
        .hsync_gpio_num = GPIO_NUM_39,
        .pclk_gpio_num  = GPIO_NUM_0,

        .disp_gpio_num  = GPIO_NUM_38,   // DISP/ENABLE
        .num_fbs = 1,

        .data_gpio_nums = {
            // B0..B4
            GPIO_NUM_8,  GPIO_NUM_3,  GPIO_NUM_46, GPIO_NUM_9,  GPIO_NUM_1,
            // G0..G5
            GPIO_NUM_5,  GPIO_NUM_6,  GPIO_NUM_7,  GPIO_NUM_15, GPIO_NUM_16, GPIO_NUM_4,
            // R0..R4
            GPIO_NUM_45, GPIO_NUM_48, GPIO_NUM_47, GPIO_NUM_21, GPIO_NUM_14
        },

        .bounce_buffer_size_px = 10 * 800,
        .sram_trans_align = 64,
        .psram_trans_align = 64,

        .flags = {
            .fb_in_psram = 1,
            .disp_active_low = 0,
        },

        .timings = {
            .pclk_hz = 15000000,
            .h_res = 800,
            .v_res = 480,

            .hsync_front_porch = 210,
            .hsync_pulse_width = 4,
            .hsync_back_porch  = 43,

            .vsync_front_porch = 22,
            .vsync_pulse_width = 4,
            .vsync_back_porch  = 12,

            .flags = {
                .pclk_active_neg = 1,
                .pclk_idle_high  = 0,
                .de_idle_high    = 0,
                .hsync_idle_low  = 0,
                .vsync_idle_low  = 0,
            },
            
        },
    };

    esp_lcd_panel_handle_t panel = NULL;
    esp_err_t err = esp_lcd_new_rgb_panel(&cfg, &panel);
    if (err != ESP_OK) return err;
    if ((err = esp_lcd_panel_reset(panel)) != ESP_OK) return err;
    if ((err = esp_lcd_panel_init(panel)) != ESP_OK) return err;
    if ((err = esp_lcd_panel_disp_on_off(panel, true)) != ESP_OK) return err;

    *out_panel = panel;
    return ESP_OK;
}

f_lvgl_port.c:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_timer.h"
#include "esp_log.h"
#include "touch.h"
#include "lvgl_port.h"
#include "lvgl.h"
#include "esp_lcd_panel_ops.h"
#include "touch_calib.h"

static const char *TAG = "LVGL_PORT";
SemaphoreHandle_t xSemaphoreDisplay;

static esp_lcd_panel_handle_t s_panel = NULL;
static void lvgl_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) {
        int x1 = area->x1;
        int y1 = area->y1;
        int x2 = area->x2 + 1;
        int y2 = area->y2 + 1;
        esp_lcd_panel_draw_bitmap(s_panel, x1, y1, x2, y2, color_p);
        lv_disp_flush_ready(disp_drv);
}
static void lv_tick_task(void *arg) {
    (void)arg;
    lv_tick_inc(2);
}

void f_updateScreen(void *arg){
        while (1) {
                lock();
                        lv_timer_handler();
                unlock();        
            vTaskDelay(pdMS_TO_TICKS(20));
        }
}

static void lvgl_touch_read_cb(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
    (void)indev_drv;

    if (touch_calib_is_calibrating()) {
        data->state = LV_INDEV_STATE_REL;
        return;
    }

    uint16_t rx, ry;
    if (!touch_get_xy(&rx, &ry)) {
        data->state = LV_INDEV_STATE_REL;
        return;
    }

    int x = 0, y = 0;

    if (!touch_calib_apply(rx, ry, &x, &y, 800, 480)) {
        // Fallback (se ainda não calibrado) — você pode deixar REL pra evitar cliques errados:
        // data->state = LV_INDEV_STATE_REL; return;

        // ou manter um map bruto provisório:
        x = 0; y = 0;
    }

    data->state = LV_INDEV_STATE_PR;
    data->point.x = x;
    data->point.y = y;

    // Debug:
    //printf("raw(%u,%u) -> px(%d,%d)\n", rx, ry, x, y);
}



void lvgl_port_init(esp_lcd_panel_handle_t panel) {
        s_panel = panel;

        lv_init();

        // // 1) buffers do LVGL (recomendo INTERNAL+DMA)
        // static lv_disp_draw_buf_t draw_buf;

        // // Use um buffer relativamente pequeno; 800 * 40 pixels funciona muito bem.
        // const int HOR = 800;
        // const int BUF_LINES =  20; // quanto mais linhas, mais RAM usada mas menos chamadas de flush (e.g. 800*240 = full screen)
        // static lv_color_t *buf1 = NULL;

        // buf1 = heap_caps_malloc(HOR * BUF_LINES * sizeof(lv_color_t),  MALLOC_CAP_DMA);
        // // É um teste (pode dar problema ou ficar lento)
        // //buf1 = heap_caps_malloc(HOR * BUF_LINES * sizeof(lv_color_t), MALLOC_CAP_SPIRAM);

        // assert(buf1);

        // lv_disp_draw_buf_init(&draw_buf, buf1, NULL, HOR * BUF_LINES);



        // 1) buffers do LVGL (DMA)
static lv_disp_draw_buf_t draw_buf;

const int HOR = 800;
const int BUF_LINES = 20;                 // 20~40 costuma ser bom
const size_t buf_pixels = HOR * BUF_LINES;

static lv_color_t *buf1 = NULL;
static lv_color_t *buf2 = NULL;

buf1 = heap_caps_malloc(buf_pixels * sizeof(lv_color_t), MALLOC_CAP_DMA);
buf2 = heap_caps_malloc(buf_pixels * sizeof(lv_color_t), MALLOC_CAP_DMA);

assert(buf1 && buf2);

lv_disp_draw_buf_init(&draw_buf, buf1, buf2, buf_pixels);




        // 2) driver de display
        static lv_disp_drv_t disp_drv;
        lv_disp_drv_init(&disp_drv);

        disp_drv.hor_res = 800;
        disp_drv.ver_res = 480;
        disp_drv.flush_cb = lvgl_flush_cb;
        disp_drv.draw_buf = &draw_buf;

        lv_disp_drv_register(&disp_drv);
            
        static lv_indev_drv_t indev_drv;
        lv_indev_drv_init(&indev_drv);
        indev_drv.type = LV_INDEV_TYPE_POINTER;
        indev_drv.read_cb = lvgl_touch_read_cb;
        lv_indev_drv_register(&indev_drv);

        // 3) Tick 1ms via esp_timer
        const esp_timer_create_args_t tick_args = {
            .callback = &lv_tick_task,
            .name = "lv_tick"
        };

        esp_timer_handle_t tick_timer = NULL;
        ESP_ERROR_CHECK(esp_timer_create(&tick_args, &tick_timer));
        ESP_ERROR_CHECK(esp_timer_start_periodic(tick_timer, 2 * 1000));

        xSemaphoreDisplay = xSemaphoreCreateBinary();
        xSemaphoreGive(xSemaphoreDisplay);

        //xTaskCreatePinnedToCoreWithCaps(f_updateScreen,"lvgl", 3200, NULL, tskIDLE_PRIORITY+4, NULL, 1, MALLOC_CAP_SPIRAM);
        xTaskCreatePinnedToCore(f_updateScreen, "lvgl", 3200, NULL, tskIDLE_PRIORITY+4, NULL, 1);
        xTaskCreatePinnedToCore(touch_init, "touch", 4096, NULL, tskIDLE_PRIORITY+1, NULL, 1);
        ESP_LOGI(TAG, "LVGL init OK");
}

void lock(){if (xSemaphoreDisplay!=NULL){xSemaphoreTake(xSemaphoreDisplay, portMAX_DELAY);}}
void unlock(){if (xSemaphoreDisplay!=NULL){xSemaphoreGive(xSemaphoreDisplay);}}

The issue is that the display correctly shows the screen drawn in LVGL.

That in itself isn’t the problem.

The issue is that the screen flickers constantly.

I’ve already tried everything with the help of AI (ChatGPT) and I can’t fix this flickering screen problem.

Does anyone have any idea what might be happening?

@Elecrow @kisvegabor

Hi Allan!

I did an electronic voting machine with an Elecrow 5 inch display. I also struggled there with some flickering and tearing of the screens but at the end I managed to get a good configuration that worked more or less stable.

You can take a look here: GitHub - giobauermeister/urna-esp32s3-lvgl: Electronic Voting Machine with ESP32-S3, ESP-IDF and LVGL (WIP)

Can you have a look and see if any configuration is different than yours?

Let’s continue discussing if you don’t find anything that helps.

In order to have a decent refresh rate using a display that has no internal memory you will need to use the second core to handle the transmitting of the buffers and to handle keeping the buffer data in sync. When all of this work gets handled by a single core it will impact performance in a very large way.

You can get it done using one core it’s just that the performance is going to suffer because of it and you need to make sure that you are keeping the buffers and the calls to lv_flush_ready in sync. This can be tricky to do because of having to use the vsync callback function which gets called after each time the buffer gets sent to the display. With an RGB display this happens in a never ending loop so a single buffer may be sent more than one time but you only want to call lv_flush_ready only after the first time the buffer data has been sent. With how the code is written in the ESP-IDF with the RGB driver there is no way easy way to identify what buffer has just finished being transmitted from inside of the vsync callback function. So the choices end up becoming either a blocking call that waits until the data has been sent or you need to “hack” the RGB driver to some degree in order to get the information that is needed from inside of the vsync callback. The blocking route is seen in Espressif’s examples on how to use the RGB driver on their GitHub repo. Personally I don’t like this method especially when dealing with a display that is larger than 320x320 in resolution because that blocking will cause a significant amount of latency resulting in a really low refresh rate of the display.

specifically this code is wrong…

static void lvgl_flush_cb(lv_display_t *disp, const lv_area_t *area, uint8_t *color_p) {
    esp_lcd_panel_draw_bitmap(panel_handle, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_p);
    lv_display_flush_ready(disp);
}

you cannot call lv_display_flush_ready from inside of the flush callback function. This is because the buffer data is being transmitted using a DMA buffer so the call to esp_lcd_panel_draw_bitmap is not a blocking call. It immediately returns before the data has even been sent. so you end up telling LVGL that the buffer has transmitted when in fact it has not.

You need to register a callback function on the vsync for the display and that is where you would call lv_display_flush_ready. you need to make sure that the flush ready function only gets called the first time the display buffer is transmitted after esp_lcd_panel_draw_bitmap is called.That’s the part that is the tricky part without having to use freeRTOS to cause a blocking condition to wait until that happens.

2 Likes

I really wanted to thank you for taking the time to help me.

It’s not the first time you’ve saved my life.

I have no problem using the second processor, considering I’m using an ESP32 S3 with two processors.

I didn’t use it because I wasn’t aware of that particular detail.

I will study the observations you mentioned in conjunction with AI.

I downloaded your example and tried very hard to apply it to my use case.

I couldn’t implement it at all.

The example works, compiles, and runs…

However, when I apply it to my screen, it becomes very shaky…

In addition, it has a huge incompatibility with LVGL 8.4 x 9.X.

I solved it, and it got a little better by adding a traffic light before each update.

Lock() and Unlock() still have a slow screen…

#include "display.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "f_elecrow_5in.h"
#include "esp_lcd_panel_ops.h"   // draw_bitmap
#include "driver/gpio.h"
#include "esp_heap_caps.h"
#include <assert.h>
#include "lvgl_port.h"
#include "lvgl.h"
#include "touch.h"
#include "sq/ui.h"
#include "touch_calib.h"
#include "f_wifi.h"
#include "f_configfile.h"
#include "lcd_test.h"


static const char *TAG = "DISPLAY";

#include "lvgl_port.h"  // declare lvgl_port_init()

void f_SetupDisplay() {
    ESP_LOGI(TAG, "Initializing display...");
    gpio_reset_pin(GPIO_NUM_2);
    gpio_set_direction(GPIO_NUM_2, GPIO_MODE_OUTPUT);
    gpio_set_level(GPIO_NUM_2, 1);    // backlight ON só depois do init
    vTaskDelay(pdMS_TO_TICKS(20));

    esp_lcd_panel_handle_t panel = NULL;
    ESP_ERROR_CHECK(elecrow_5in_init(&panel));
    lvgl_port_init(panel);
    touch_calib_run_if_needed(800, 480);
    ESP_LOGW(TAG, "Display setup complete, LVGL task is running in background.");   
    ui_init();
}

void StatusMQTT(bool status) {
        if (status){
                lock();
                lv_obj_clear_flag(ui_imgMQTT, LV_OBJ_FLAG_HIDDEN);
                unlock();
        }else{
                lock();
                lv_obj_add_flag(ui_imgMQTT, LV_OBJ_FLAG_HIDDEN);
                unlock();
        }
}
void StatusRS485(bool status) {
        if (status){
                lock();
                lv_obj_clear_flag(ui_imgSerial, LV_OBJ_FLAG_HIDDEN);
                lv_label_set_text(ui_lblStatus, "");
                unlock();
        }else{
                lock();
                lv_obj_add_flag(ui_imgSerial, LV_OBJ_FLAG_HIDDEN);
                lv_label_set_text(ui_lblStatus, "RS485 Disconnected...");
                lv_label_set_text(ui_lblProfundidade, "-1");
                lv_label_set_text(ui_lblTemperatura, "-1");
                unlock();
        }
}

void StatusWifi_img(bool status) {
        if (status){
                lock();
                lv_obj_clear_flag(ui_imgWifiOn, LV_OBJ_FLAG_HIDDEN);
                lv_obj_add_flag(ui_imgWifiOff, LV_OBJ_FLAG_HIDDEN);
                unlock();
        }else{
                lock();
                lv_obj_clear_flag(ui_imgWifiOff, LV_OBJ_FLAG_HIDDEN);
                lv_obj_add_flag(ui_imgWifiOn, LV_OBJ_FLAG_HIDDEN);
                unlock();
        }
}

void f_progressoDisplay(const char * texto, int tempo){
        lock();
        lv_label_set_text(ui_lblStartingStatus, texto);
        unlock();
        vTaskDelay(tempo / portTICK_PERIOD_MS);
}

void f_DisplayIP(char *ip) {
        lock();
        lv_label_set_text(ui_lblIP, ip);
        unlock();
}

void f_DisplayClienteConectado(void *parameters){
        lock();
        lv_label_set_text(ui_lblIP, "Cliente Conectado");
        unlock();
}

void setupConfigDisplay() {
        Dados_Wifi_t DadosWifi;
        esp_err_t valida = f_DadosWifi(&DadosWifi);
        if (valida == ESP_OK && DadosWifi.Mode == Station){
                lock();
                lv_label_set_text(ui_lblMode,  "Station");
                lv_label_set_text(ui_lblSSID, DadosWifi.SSID);
                unlock();
        }else {
                lock();
                lv_label_set_text(ui_lblMode,  "Access Point");
                lv_label_set_text(ui_lblSSID, "EnervisionDivers / Pass: 12345678" );
                lv_label_set_text(ui_lblIP, "192.168.1.1" );
                unlock();
        }

}

I haven’t been able to apply these modifications yet.

I’ll study how to implement them more thoroughly. The first tests I did resulted in many errors.

RGB displays are the hardest to get running right. You have to do a lot of tinkering to get things just right and to get a refresh speed that you are happy with.

espressif has a really good example of how to achieve an artifact free UI.

here is where you can specifically see how they have gone about setting up the flush function and the vsync callback.

vsync callback

LVGL flush callback

Here is the registering of the vsync callback

Here is the setting up of the FreeRTOS related bits

NOTE: Make sure you select the ESP-IDF version that matches the one you are using. the examples are different from one version to the next. The reason why I used version 5.2 is because it shows the more complicated use of FreeRTOS with respect to handling the frame buffers.

There are some code changes that can be made that would further improve the performance by using both cores on the ESP32 but that is going to be really advanced code to do that.

1 Like

I spent quite a bit of time pulling my hair when I was writing a driver for the RGB displays. It took me several months to put together a driver that uses both cores to achieve the best possible speed.

also if you are familiar with Python you can save yourself a lot of effort with having to deal with the driver aspects of things. I put together a MicroPython binding where LVGL is built in and all of the driver stuff is already done. All you have to do is start the driver by passing your pins to it and away you go.

You always save my life!

I’ll study some examples.

Thank you so much for your attention!!!

I’d like to thank @kdschlosser for their help; you always save my life.

I managed to get the 4.3" RGB display working perfectly without screen flickering on the ESP IDF 5.3 with LVGL.

The code that achieved the result is below:

Yes, it was necessary to add a v_sinc callback function and align semaphores to indicate that it was finished/ready.

This way, LVGL doesn’t keep sending flushes while there are still pending issues.

f_display43.c:

#include "f_display43.h"
#include "f_panelConfig.h"

#include <stdint.h>
#include <stdbool.h>

#include "esp_log.h"
#include "esp_check.h"
#include "driver/gpio.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"

static const char *TAG = "f_display43";

static esp_lcd_panel_handle_t s_panel = NULL;
static uint16_t *s_framebuffer = NULL;

static SemaphoreHandle_t s_sem_vsync = NULL;
static volatile bool s_flush_pending = false;

static inline uint16_t rgb565(uint8_t r, uint8_t g, uint8_t b) {
    return (uint16_t)(((r & 0xF8) << 8) |
                      ((g & 0xFC) << 3) |
                      ((b & 0xF8) >> 3));
}

static bool IRAM_ATTR rgb_vsync_cb(esp_lcd_panel_handle_t panel, const esp_lcd_rgb_panel_event_data_t *edata, void *user_ctx) {
        (void)panel;
        (void)edata;
        (void)user_ctx;

        if (!s_flush_pending || s_sem_vsync == NULL) {
            return false;
        }

        BaseType_t hp_task_woken = pdFALSE;
        xSemaphoreGiveFromISR(s_sem_vsync, &hp_task_woken);
        return (hp_task_woken == pdTRUE);
}

static void backlight_init(void) {
        const gpio_config_t io_conf = {
            .pin_bit_mask = 1ULL << LCD_BKLT_GPIO,
            .mode = GPIO_MODE_OUTPUT,
            .pull_up_en = GPIO_PULLUP_DISABLE,
            .pull_down_en = GPIO_PULLDOWN_DISABLE,
            .intr_type = GPIO_INTR_DISABLE
        };

        ESP_ERROR_CHECK(gpio_config(&io_conf));
        gpio_set_level(LCD_BKLT_GPIO, 0);
}

static void backlight_on(void) {
    gpio_set_level(LCD_BKLT_GPIO, 1);
}

static void fill_screen(uint16_t color) {
    if (s_framebuffer == NULL) {return;}
    const size_t total_pixels = LCD_H_RES * LCD_V_RES;
    for (size_t i = 0; i < total_pixels; i++) {
        s_framebuffer[i] = color;
    }
}

static void draw_test_pattern(void) {
        if (s_framebuffer == NULL) {
            return;
        }

        const uint16_t colors[] = {
            rgb565(255, 255, 255), // branco
            rgb565(255, 255,   0), // amarelo
            rgb565(  0, 255, 255), // ciano
            rgb565(  0, 255,   0), // verde
            rgb565(255,   0, 255), // magenta
            rgb565(255,   0,   0), // vermelho
            rgb565(  0,   0, 255), // azul
            rgb565(  0,   0,   0)  // preto
        };

        const int bar_count = sizeof(colors) / sizeof(colors[0]);
        const int bar_width = LCD_H_RES / bar_count;

        for (int y = 0; y < LCD_V_RES; y++) {
            for (int x = 0; x < LCD_H_RES; x++) {
                int idx = x / bar_width;
                if (idx >= bar_count) {
                    idx = bar_count - 1;
                }
                s_framebuffer[y * LCD_H_RES + x] = colors[idx];
            }
        }

        for (int x = 0; x < LCD_H_RES; x++) {
            s_framebuffer[x] = rgb565(255, 255, 255);
            s_framebuffer[(LCD_V_RES - 1) * LCD_H_RES + x] = rgb565(255, 255, 255);
        }

        for (int y = 0; y < LCD_V_RES; y++) {
            s_framebuffer[y * LCD_H_RES] = rgb565(255, 255, 255);
            s_framebuffer[y * LCD_H_RES + (LCD_H_RES - 1)] = rgb565(255, 255, 255);
        }
}

esp_err_t f_setupDisplay(void) {
        if (s_panel != NULL) {ESP_LOGW(TAG, "Display ja inicializado.");return ESP_OK;}
        backlight_init();
        
        esp_lcd_rgb_panel_config_t panel_config = f_panelConfig_get();
        ESP_RETURN_ON_ERROR(esp_lcd_new_rgb_panel(&panel_config, &s_panel), TAG, "esp_lcd_new_rgb_panel falhou");
        ESP_RETURN_ON_ERROR(esp_lcd_panel_reset(s_panel), TAG, "esp_lcd_panel_reset falhou");
        ESP_RETURN_ON_ERROR(esp_lcd_panel_init(s_panel), TAG, "esp_lcd_panel_init falhou");

        if (s_sem_vsync == NULL) {
            s_sem_vsync = xSemaphoreCreateBinary();
            ESP_RETURN_ON_FALSE(s_sem_vsync != NULL, ESP_ERR_NO_MEM, TAG, "Falha ao criar s_sem_vsync");
        }
        s_flush_pending = false;
        xSemaphoreTake(s_sem_vsync, 0);

        esp_lcd_rgb_panel_event_callbacks_t cbs = {
            // .on_bounce_frame_finish = rgb_bounce_frame_finish_cb,
            .on_vsync = rgb_vsync_cb,
        };
        ESP_RETURN_ON_ERROR(esp_lcd_rgb_panel_register_event_callbacks(s_panel, &cbs, NULL), TAG, "esp_lcd_rgb_panel_register_event_callbacks falhou");
        void *fb0 = NULL;
        ESP_RETURN_ON_ERROR(esp_lcd_rgb_panel_get_frame_buffer(s_panel, 1, &fb0), TAG,"esp_lcd_rgb_panel_get_frame_buffer falhou");
        s_framebuffer = (uint16_t *)fb0;
        backlight_on();
        fill_screen(rgb565(0, 0, 0));
        // draw_test_pattern();
        ESP_LOGI(TAG, "Display 4.3 inicializado com sucesso (%dx%d).", LCD_H_RES, LCD_V_RES);
        return ESP_OK;
}

void f_display43_begin_flush_sync(void) {
    s_flush_pending = true;
    if (s_sem_vsync != NULL) {
        xSemaphoreTake(s_sem_vsync, 0);
    }
}

bool f_display43_wait_flush_release(TickType_t timeout_ticks) {
    if (s_sem_vsync == NULL) {
        return false;
    }
    if (xSemaphoreTake(s_sem_vsync, timeout_ticks) != pdTRUE) {
        return false;
    }
    s_flush_pending = false;
    return true;
}

esp_lcd_panel_handle_t f_display43_get_panel(void) {return s_panel;}
uint16_t *f_display43_get_framebuffer(void)        {return s_framebuffer; }

f_init-lvgl.c

#include "f_init-lvgl.h"
#include "f_display43.h"
#include "f_panelConfig.h"

#include <string.h>

#include "esp_log.h"
#include "esp_check.h"
#include "esp_timer.h"
#include "esp_heap_caps.h"

#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "f_init_lvgl";

#define LVGL_TICK_PERIOD_MS      20
#define LVGL_TASK_PERIOD_MS      30
#define LVGL_BUF_LINES           100

static SemaphoreHandle_t s_lvgl_mutex = NULL;
static esp_timer_handle_t s_lvgl_tick_timer = NULL;
static TaskHandle_t s_lvgl_task_handle = NULL;

static lv_disp_draw_buf_t s_draw_buf;
static lv_color_t *s_buf1 = NULL;
static lv_color_t *s_buf2 = NULL;
static lv_disp_drv_t s_disp_drv;
static lv_disp_t *s_disp = NULL;

static bool s_lvgl_initialized = false;


static void lvgl_tick_cb(void *arg) {
    (void)arg;
    lv_tick_inc(LVGL_TICK_PERIOD_MS);
}

static void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map) {
        esp_lcd_panel_handle_t panel = f_display43_get_panel();
        if (panel == NULL) { lv_disp_flush_ready(drv);return;}
        f_display43_begin_flush_sync();
        if (!f_display43_wait_flush_release(pdMS_TO_TICKS(100))) {ESP_LOGW(TAG, "Timeout esperando VSYNC para flush");}
        esp_err_t err = esp_lcd_panel_draw_bitmap(panel, area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_map);
        if (err != ESP_OK) {ESP_LOGE(TAG, "esp_lcd_panel_draw_bitmap falhou: %s", esp_err_to_name(err));}
        lv_disp_flush_ready(drv);
}

static void lvgl_port_task(void *arg) {
    (void)arg;
    while (1) {
        lock();
        lv_timer_handler();
        unlock();
        vTaskDelay(pdMS_TO_TICKS(LVGL_TASK_PERIOD_MS));
    }
}

void lock(void) { if (s_lvgl_mutex == NULL) { return; } xSemaphoreTake(s_lvgl_mutex, portMAX_DELAY); }
void unlock(void) { if (s_lvgl_mutex != NULL) { xSemaphoreGive(s_lvgl_mutex); } }

lv_disp_t *f_lvgl_get_display(void) { return s_disp;}

esp_err_t f_init_lvgl(void) {
        if (s_lvgl_initialized) {
            ESP_LOGW(TAG, "LVGL ja inicializado.");
            return ESP_OK;
        }

        ESP_RETURN_ON_ERROR(f_setupDisplay(), TAG, "f_setupDisplay falhou");

        if (s_lvgl_mutex == NULL) {
            s_lvgl_mutex = xSemaphoreCreateMutex();
            ESP_RETURN_ON_FALSE(s_lvgl_mutex != NULL, ESP_ERR_NO_MEM, TAG, "Falha ao criar mutex do LVGL");
        }

        lv_init();

        const size_t buf_pixels = LCD_H_RES * LVGL_BUF_LINES;
        const size_t buf_bytes = buf_pixels * sizeof(lv_color_t);

        s_buf1 = heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
        s_buf2 = heap_caps_malloc(buf_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);

        ESP_RETURN_ON_FALSE(s_buf1 != NULL, ESP_ERR_NO_MEM, TAG, "Falha ao alocar s_buf1");
        ESP_RETURN_ON_FALSE(s_buf2 != NULL, ESP_ERR_NO_MEM, TAG, "Falha ao alocar s_buf2");

        memset(s_buf1, 0, buf_bytes);
        memset(s_buf2, 0, buf_bytes);

        lv_disp_draw_buf_init(&s_draw_buf, s_buf1, s_buf2, buf_pixels);

        lv_disp_drv_init(&s_disp_drv);
        s_disp_drv.hor_res = LCD_H_RES;
        s_disp_drv.ver_res = LCD_V_RES;
        s_disp_drv.flush_cb = lvgl_flush_cb;
        s_disp_drv.draw_buf = &s_draw_buf;
        s_disp_drv.full_refresh = 0;

        s_disp = lv_disp_drv_register(&s_disp_drv);
        ESP_RETURN_ON_FALSE(s_disp != NULL, ESP_FAIL, TAG, "Falha ao registrar display no LVGL");

        const esp_timer_create_args_t tick_args = {
            .callback = lvgl_tick_cb,
            .arg = NULL,
            .dispatch_method = ESP_TIMER_TASK,
            .name = "lvgl_tick"
        };

        ESP_RETURN_ON_ERROR(esp_timer_create(&tick_args, &s_lvgl_tick_timer), TAG, "esp_timer_create falhou");
        ESP_RETURN_ON_ERROR(esp_timer_start_periodic(s_lvgl_tick_timer, LVGL_TICK_PERIOD_MS * 1000), TAG, "esp_timer_start_periodic falhou");

        BaseType_t task_ok = xTaskCreatePinnedToCore(lvgl_port_task, "lvgl_task", 3000, NULL, 7, &s_lvgl_task_handle, 0);
        ESP_RETURN_ON_FALSE(task_ok == pdPASS, ESP_FAIL, TAG, "Falha ao criar task do LVGL");

        s_lvgl_initialized = true;

        

        ESP_LOGI(TAG, "LVGL inicializado com sucesso.");
        ESP_LOGI(TAG, "Resolucao: %dx%d", LCD_H_RES, LCD_V_RES);
        ESP_LOGI(TAG, "Buffer lines: %d", LVGL_BUF_LINES);
        ESP_LOGI(TAG, "Heap internal livre: %u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));
        ESP_LOGI(TAG, "Heap PSRAM livre: %u", (unsigned)heap_caps_get_free_size(MALLOC_CAP_SPIRAM));

        return ESP_OK;
}


1 Like

@kdschlosser Is any possibility for people who work under PlatformIO with arduino framework to obtain acces to vsync event?

Also my second dream is a “getscanline” from RGB driver

I got a many different this HMI panels like:

  • SUNTON ESP328048S070
  • CrowPanel 7 inch, 4.3 inch
  • VIEVE 7" and 4.3" as well
  • and many, many others…

As I noticed CrowPanel 7" got the RGB driver EK9716BD3+EK73002ACGB, the SUNTON 7" board , VIEWE 7" also working on the EK9716.

Exist any possibility to direct communication between ESP32 and RGB driver?

I know the Arduino IDE’s form of the ESP-IDF (SDK) is a highly modified form of the IDF and it is also incomplete. To what extent the incompleteness is i could not tell you because I don’t use it and I am not that familiar with it. I do also know that the one provided for the Arduino IDE adds additional overhead due to the wrapping of functions to make it compatible with the Arduino IDE.

The ESP-IDF actually has a very well built build system that makes compiling a project using the ESP-IDF directly pretty simple to do. There are files that you need to make to define the project and they get placed in a folder and then using python you run a script in the ESP-IDF and it takes over from there and puts all of the pieces together for you including collecting any requirements your project might have on external ESP32 component libraries…

You will have more access to things that occur with the drivers and how to set things up. The display “driver” libraries that are available for the Arduino IDE and in fact not driver libraries. They are UI frameworks and the drivers that come with them are written so they work with that specific framework. Sure you can hobble together something that will work using that framework with LVGL but it adds a lot of unneeded complexity and things are not always going to be handled the way they should be and that will lead to a degradation in performance.

The makers of the the ESP32 have put together a good working system where you can download drivers that are purpose written for the ESP32 and they are for specific display IC’s. It is not a package of display drivers where the code is written so a single entry point will work for a bunch of different display IC’s. This has the advantage of you using only what is needed to get things working with your specific display. No extra code or complexity.

Take a look at the ESP component registry and do a search for display you will see there are 283 results returned. That’s a pretty large number of drivers that are available.

1 Like

I do not believe it is possible to get the current scan line with the RGB driver. The reason why is the data that is being sent to the display is being done without the use of the CPU. It is being carried out by the DMA controller. The transmitting of the data is carried out by the hardware and not by software. the hardware is basically handed a memory address and the number of bytes that needs to be transmitted along with timing values and the GPIO’s the data needs to be transmitted on. The DMA controller only has the ability to trigger an interrupt once the data has finished being sent. There is no other way for the controller to pass information like what line is being transmitted without triggering an interrupt and doing that for each line on a display would have a very large amount of overhead and it would cause visual artifacts to appear on the display…

1 Like

Thank you kindly for the answers.

So I’ll try be creative how to obtain what I want:

  1. Vsync - simply interrupt on the LCD Vsync polarity (usually falling) edge of Vsync GPIO

  2. Actual scanline:
    Frame create is all the time this same timing which was stored for LCD bus programmed. So simply on the Vsync event running timer with LCD bus frequency or multiple value of LCD bus frequency. Prescaler should be have value of ((H_front_porch + H_back_porch + H_pulse_width + H_resolution -1) * (Freq_of_timer / LCD_bus_freq )).
    Timer_CNT should UP counting from 0 to (V_front_porch + V_back_porch + V_pulse_width + V_resolution -1)
    It should be gave stable actual scanline with all porches and V pulse width what is very helpfull for 3rd additional things like: True doublebuffering.

  3. True doublebuffering: ps_malloc(2nd_framebuffer_size) and simple rewrite all GDMA descriptors of lines address. That will be in this case just 480 descriptors overwrite what is really not time consume trick. All V back porches and V pulse width take arround 80-120 lines which is many times too much for such a simple operation.

  4. Moreover - how you overwrite GDMA descriptors pattern you can obtain many curious effects, like wavig, bouncing, Y zoom and so on.

Must start try after finish my curent project wit this SUNTON board.

If my assumptions are wrong, please correct me so I don’t waste my time.

Best regards

When I started programming embedded systems, like everyone else of course, I was using the Arduino IDE.

When I went to write my first complex and professional program, I started running into the various problems that the Arduino IDE brings with it.

The first thing that really annoyed me was the compilation, then the mess caused by the library being bundled with other programs, and lastly the absurd abstraction that the Arduino libraries create.

It was a difficult time in my life; I had to switch my computer to Linux and decided whether to use Platform.IO or ESP-IDF. A good friend of mine used Platform.IO and highly praised it (I even used it a few times), and yet, I thought it was a “patch… of the Arduino IDE”.

I made the decision to use ESP-IDF.

It was a long learning curve, it wasn’t easy, I asked everyone for help all the time and used chatgpt a lot to help…

It took me 6 months to build my own common-use ESP32 component that includes WifiManager/WebServer/File Manager/Tasks/Memory/Avanced OTA System. I built my own framework that I use in all my ESP32 products.

And I tell you, it was the best thing I ever did. Today I understand the ESP-IDF functionality much better, as well as the use of the components and the entire structure that the manufacturer’s SDK provides. I’ve learned how to handle example code found online.

Today I can build any product quickly and easily… Switching to ESP-IDF was the best thing I ever did.

I recommend you do the same.

Samples for my Framework:

And code:

2 Likes

@allacmc thanks for reply. I understand the best API provided was with develop IDE by producer of ESP. Also I notice every one examples of LVGL for ESP32 are done in ESP-IDF. It’s not coincidence just it’s the best way for full use all possibilities of ESP32. Espressif also binding IDF with arduino platfom, just it is very, very slow. From many years I still wait for releasse IDF 5.1 (probably) for arduino platform what could gave me access to Vsync callback.

I install ESP-IDF for VScode, it work perfectly and most probably in the near future I’ll deeper learning of ESP-IDF.

Probably after I’ll finish my proof of concept theories stored in before post.

Pleasse note getscanline is impossible even in ESP-IDF, I’m not sure about second frame buffer switching on the Vsync event as well.

Without a doubt, my transition to ESP-IDF is inevitable.

Cheers

The ESP-IDF is version 5.3 and improvements and fixes are released daily.

They have a very dedicated development team, most of whom are Brazilian.

The GitHub repository is very up-to-date with many examples.

Not migrating is a real mistake.

However, if you want to switch to ESP-IDF and are worried it won’t work:

There’s an ESP-IDF component called “Arduino” that makes any Arduino library compatible with ESP-IDF.

I personally don’t like it; I think it’s a workaround, but it exists and it works. I even attended a presentation about it.