ILI9488 display issues using ESP32S3 parallel 3.5 touch

Hey everyone, I have been working with the ESP32S3 tft parallel with touch 3.5 for about a month now and been using LVGL with it for about a month as well. I am trying to create my own custom board so I can set it up in squareline, but I am running into some issues with initalizing/configuring the ILI9488 LCD driver. I can get some/very little of the actual graphics(like a lvgl keyboard/slider… etc) to display but its really static and not well pixalated. I have been on this issue for about two weeks now and cannot seem to get past it. I am using as dependacys the esp_lvgl_port, esp_lcd_ili9488 and the esp_lcd_touch_ft5x06. I know my device works because I got the ESP-32 Tux git lib working and operational.

Attached below are the majority of my files, the esp32_s3.c file is where the magic happens so focus on that. Please let me know if I need to provide more information on anything, the picture should clear up most of the confusion.

Note: I can drastically change the lcd outcome by messing with: buffer_size for lvgl port, pclk_hz for pixel clock, and max_transfer_bytes for i80 bus. I believe the issue is in these three but honestly I am kinda stumped.
img_0747 is the closest thing i have gotten to the end result, img_0748 is what i normally get which might provide more information

edit: forgot to mention my environment is VScode using ESP-IDF v5.0.2, and here is my monitor log : I (24) boot: ESP-IDF v5.0.2 2nd stage bootloader
I (25) boot: compile time 11:41:41
I (25) boot: chip revision: v0.1
I (27) boot.esp32s3: Boot SPI Speed : 80MHz
I (31) boot.esp32s3: SPI Mode : DIO
I (36) boot.esp32s3: SPI Flash Size : 8MB
I (41) boot: Enabling RNG early entropy source…
I (46) boot: Partition Table:
I (50) boot: ## Label Usage Type ST Offset Length
I (57) boot: 0 nvs WiFi data 01 02 00009000 00004000
I (64) boot: 1 phy_init RF data 01 01 0000f000 00001000
I (72) boot: 2 factory factory app 00 00 00010000 00200000
I (79) boot: End of partition table
I (84) esp_image: segment 0: paddr=00010020 vaddr=3c050020 size=11df4h ( 73204) map
I (105) esp_image: segment 1: paddr=00021e1c vaddr=3fc93b00 size=02a1ch ( 10780) load
I (108) esp_image: segment 2: paddr=00024840 vaddr=40374000 size=0b7d8h ( 47064) load
I (121) esp_image: segment 3: paddr=00030020 vaddr=42000020 size=4ea8ch (322188) map
I (180) esp_image: segment 4: paddr=0007eab4 vaddr=4037f7d8 size=042f8h ( 17144) load
I (190) boot: Loaded app from partition at offset 0x10000
I (190) boot: Disabling RNG early entropy source…
I (202) esp_psram: Found 2MB PSRAM device
I (202) esp_psram: Speed: 80MHz
I (248) mmu_psram: Instructions copied and mapped to SPIRAM
I (267) mmu_psram: Read only data copied and mapped to SPIRAM
I (267) cpu_start: Pro cpu up.
I (268) cpu_start: Starting app cpu, entry point is 0x40375484
0x40375484: call_start_cpu1 at C:/Users/Admin/Espressif/esp-idf/components/esp_system/port/cpu_start.c:141

I (0) cpu_start: App cpu up.
I (452) esp_psram: SPI SRAM memory test OK
I (461) cpu_start: Pro cpu start user code
I (461) cpu_start: cpu freq: 160000000 Hz
I (461) cpu_start: Application information:
I (464) cpu_start: Project name: s_p_7
I (469) cpu_start: App version: v5.0.2-dirty
I (474) cpu_start: Compile time: Jun 21 2023 17:27:14
I (480) cpu_start: ELF file SHA256: 2855e8eaebfc4e9d…
I (486) cpu_start: ESP-IDF: v5.0.2
I (491) cpu_start: Min chip rev: v0.0
I (496) cpu_start: Max chip rev: v0.99
I (501) cpu_start: Chip rev: v0.1
I (506) heap_init: Initializing. RAM available for dynamic allocation:
I (513) heap_init: At 3FC97498 len 00052278 (328 KiB): DRAM
I (519) heap_init: At 3FCE9710 len 00005724 (21 KiB): STACK/DRAM
I (526) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (532) heap_init: At 600FE010 len 00001FF0 (7 KiB): RTCRAM
I (538) esp_psram: Adding pool of 1600K of PSRAM memory to heap allocator
I (546) spi_flash: detected chip: gd
I (550) spi_flash: flash io: dio
W (554) spi_flash: Detected size(16384k) larger than the size in the binary image header(8192k). Using the size in the binary image header.
I (567) sleep: Configure to isolate all GPIO pins in sleep state
I (574) sleep: Enable automatic switching of GPIO sleep configuration
I (581) cpu_start: Starting scheduler on PRO CPU.
I (0) cpu_start: Starting scheduler on APP CPU.
I (591) esp_psram: Reserving pool of 32K of internal memory for DMA/internal allocations
I (601) LVGL: Starting LVGL task
I (601) gpio: GPIO[45]| InputEn: 0| OutputEn: 1| OpenDrain: 0| Pullup: 0| Pulldown: 0| Intr:0
I (621) ESP32S3: Initialize Intel 8080 bus
I (621) ESP32S3: Install panel IO
I (631) ESP32S3: Install LCD driver of ili9488
I (631) ili9488: Configuring for RGB color order
I (641) ili9488: new ili9488 panel @0x3fca5934
I (641) ili9488: Sending SW_RESET to display
I (671) ili9488: Initializing ILI9488
I (871) ili9488: Initialization complete
I (971) ESP32S3: Turn on LCD backlight
After LCD |I (5981) ESP-EXAMPLE: Example initialization done.

esp32_s3.h (5.1 KB)

esp32_s3.c (9.0 KB)

Appears that the screen is flipped and some color settings is off.
Find the following line in the .ino file
tft.setRotation( 3 ); /* Landscape orientation, flipped */
change the value into something different, appears that you have a 2 or 4 set, the 1 and 3 are for landscape.
Try to open the file lv_config.h and set the first IF to 1, and change the LV_COLOR_DEPTH to 16
Probably you will have an error on compile, you need to set the same value in the project properties in SquareLine Studio.

unfortunately I am using VScode with the ESP-IDF extention and not using the LGFX library, so I don’t have an .ino file and .setRotation is from LGFX. The lv_conf.h is correct, first line is set to 1 and color_depth is set to 16, which matches squarelines settings

Sorry, I can’t help you…I’m now also experimenting this new tool for first time :frowning:
For sure someone can help you better than me :slight_smile:

being able to look at the code would help greatly. It looks like you might have an issue in your flush function, If you are not using double buffering and DMA memory it could be you are not calling lv_disp_flush_ready in the flush function. I would really need to look at the code to be able to help you out.

No problem yesnoj, I am still learning as well. Kdschlosser, the example code is attached. the two files are esp32_s3.c/.h. The .h file has all the settings like H res and pin layout, the .c file has the logic/bus install/lcd config etc. The files are hidden in between the two pictures above.

edit: I have tried using both lvgl_port and just regular lvgl library, both come to the same conclusion. I am also using a double buffer as well

I see where one problem is. Your buffer sizes are not aligning properly.

LVGL looks at buffer sizing with respect to the size of lv_color_t and you have set the display driver to a byte buffer size.

you want to use a buffer that is 1/10th the size of the entire display in bytes. but you tell LVGL the buffer size by dividing the buffer byte size by sizeof(lv_color_t)

so the ESP driver is going to look at the buffer size in bytes.
((horizontal * vertical) / 10) * sizeof(lv.color_t)

and when telling LVGL the buffer size
((horizontal * vertical) / 10)

That is the first thing I noticed. I would have to dig into that port code for LVGL to see how it is collecting the buffer(s) from the ESP display driver. I do not see you passing it nor do I see a flush function so i am assuming that portion is being handled by the code in the LVGL port.

double buffer is only going to be useful if you instruct the ESP driver to use DMA memory and you would have to define a custom callback function that gets passed to the ESP driver that would get called when the transfer from a buffer has completed. inside of that function you would call lv_disp_flush_ready which tells LVGL that the buffer is available to be written to.

So I’ve tried this route before and matched the buffersize with max_transfer_bytes but it does not change much.Here is the lvgl_port code for where it interacts with the buffer:

lv_disp_t *lvgl_port_add_disp(const lvgl_port_display_cfg_t *disp_cfg)
    esp_err_t ret = ESP_OK;
    lv_disp_t *disp = NULL;
    lv_color_t *buf1 = NULL;
    lv_color_t *buf2 = NULL;
    assert(disp_cfg != NULL);
    assert(disp_cfg->io_handle != NULL);
    assert(disp_cfg->panel_handle != NULL);
    assert(disp_cfg->buffer_size > 0);
    assert(disp_cfg->hres > 0);
    assert(disp_cfg->vres > 0);

    /* Display context */
    lvgl_port_display_ctx_t *disp_ctx = malloc(sizeof(lvgl_port_display_ctx_t));
    ESP_GOTO_ON_FALSE(disp_ctx, ESP_ERR_NO_MEM, err, TAG, "Not enough memory for display context allocation!");
    disp_ctx->io_handle = disp_cfg->io_handle;
    disp_ctx->panel_handle = disp_cfg->panel_handle;
    disp_ctx->rotation.swap_xy = disp_cfg->rotation.swap_xy;
    disp_ctx->rotation.mirror_x = disp_cfg->rotation.mirror_x;
    disp_ctx->rotation.mirror_y = disp_cfg->rotation.mirror_y;

    uint32_t buff_caps = MALLOC_CAP_DEFAULT;
    if (disp_cfg->flags.buff_dma && disp_cfg->flags.buff_spiram) {
        ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "Alloc DMA capable buffer in SPIRAM is not supported!");
    } else if (disp_cfg->flags.buff_dma) {
        buff_caps = MALLOC_CAP_DMA;
    } else if (disp_cfg->flags.buff_spiram) {
        buff_caps = MALLOC_CAP_SPIRAM;

    /* alloc draw buffers used by LVGL */
    /* it's recommended to choose the size of the draw buffer(s) to be at least 1/10 screen sized */
    buf1 = heap_caps_malloc(disp_cfg->buffer_size * sizeof(lv_color_t), buff_caps);
    ESP_GOTO_ON_FALSE(buf1, ESP_ERR_NO_MEM, err, TAG, "Not enough memory for LVGL buffer (buf1) allocation!");
    if (disp_cfg->double_buffer) {
        buf2 = heap_caps_malloc(disp_cfg->buffer_size * sizeof(lv_color_t), buff_caps);
        ESP_GOTO_ON_FALSE(buf2, ESP_ERR_NO_MEM, err, TAG, "Not enough memory for LVGL buffer (buf2) allocation!");
    lv_disp_draw_buf_t *disp_buf = malloc(sizeof(lv_disp_draw_buf_t));
    ESP_GOTO_ON_FALSE(disp_buf, ESP_ERR_NO_MEM, err, TAG, "Not enough memory for LVGL display buffer allocation!");

    /* initialize LVGL draw buffers */
    lv_disp_draw_buf_init(disp_buf, buf1, buf2, disp_cfg->buffer_size);

    ESP_LOGD(TAG, "Register display driver to LVGL");
    disp_ctx->disp_drv.hor_res = disp_cfg->hres;
    disp_ctx->disp_drv.ver_res = disp_cfg->vres;
    disp_ctx->disp_drv.flush_cb = lvgl_port_flush_callback;
    disp_ctx->disp_drv.drv_update_cb = lvgl_port_update_callback;
    disp_ctx->disp_drv.draw_buf = disp_buf;
    disp_ctx->disp_drv.user_data = disp_ctx;

    /* Register done callback */
    const esp_lcd_panel_io_callbacks_t cbs = {
        .on_color_trans_done = lvgl_port_flush_ready_callback,
    esp_lcd_panel_io_register_event_callbacks(disp_ctx->io_handle, &cbs, &disp_ctx->disp_drv);

    /* Monochrome display settings */
    if (disp_cfg->monochrome) {
        /* When using monochromatic display, there must be used full bufer! */
        ESP_GOTO_ON_FALSE((disp_cfg->hres * disp_cfg->vres == disp_cfg->buffer_size), ESP_ERR_INVALID_ARG, err, TAG, "Monochromatic display must using full buffer!");

        disp_ctx->disp_drv.full_refresh = 1;
        disp_ctx->disp_drv.set_px_cb = lvgl_port_pix_monochrome_callback;

    disp = lv_disp_drv_register(&disp_ctx->disp_drv);

    if (ret != ESP_OK) {
        if (buf1) {
        if (buf2) {
        if (disp_ctx) {

    return disp;

. Note: As i mentioned before, I followed esp-idf/examples/peripherals/lcd/i80_controller/main/i80_controller_example_main.c at 03d4fa28694ee15ccfd5a97447575de2d1655026 · espressif/esp-idf · GitHub and made my own lvgl_flush and buffer etc… but nothing changed

do me a favor and add 3 back ticks before and after your code. ``` it keeps the formatting correct.

appreciate that, did not know how to keep it all in the correct format.

also you still have an incorrect alignment of the display buffer size.

you need to change

        .max_transfer_bytes =  /*4092,*/ BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t) *2, // BSP_LCD_H_RES * 40 * sizeof(uint16_t),


size_t buffer_size = (BSP_LCD_H_RES * BSP_LCD_V_RES)/10;//(size_t)(BSP_LCD_H_RES * BSP_LCD_V_RES * 2.25 + 0.5);

they are both wrong.

        .max_transfer_bytes =  /*4092,*/ ((BSP_LCD_H_RES * BSP_LCD_V_RES) / 10) * sizeof(uint16_t), // BSP_LCD_H_RES * 40 * sizeof(uint16_t),
size_t buffer_size = ((BSP_LCD_H_RES * BSP_LCD_V_RES) / 10) * sizeof(uint16_t);//(size_t)(BSP_LCD_H_RES * BSP_LCD_V_RES * 2.25 + 0.5);

remember the ESP display driver deals with the buffer size being in bytes. How you had it the buffer for the ESP driver was 1/2 the size of the buffer for LVGL. You were also telling the ESP driver the maximum transfer size was twice the display resolution. Too big. It can only transfer one buffer at a time so tell it the size of a buffer.

no worries m8. it just makes it easier to read is all.

OH! are you setting the macro LVGL_PORT_HANDLE_FLUSH_READY to 1? you need to have that set to 1 otherwise it is not going to register the transfer done callback.

1 Like

so, unfortunately, when I match the buffer_size in disp_cfg to the max transfer byte, i get this error : assert failed: panel_io_i80_tx_color esp_lcd_panel_io_i80.c:450 (color_size <= (bus->num_dma_nodes * DMA_DESCRIPTOR_BUFFER_MAX_SIZE) && "color bytes too long, enlarge max_transfer_bytes") which is why I had max_transfer_bytes a bit bigger than buffer_size. The error is telling me to increase max_transfer_bytes. Currently the two variables are .max_transfer_bytes =((BSP_LCD_H_RES * BSP_LCD_V_RES)/10 ) * sizeof(uint16_t), and .buffer_size =((BSP_LCD_H_RES * BSP_LCD_V_RES)/10 ) * sizeof(uint16_t),

here is where that is in the code of lvgl_port: #if (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 4)) || (ESP_IDF_VERSION == ESP_IDF_VERSION_VAL(5, 0, 0)) #define LVGL_PORT_HANDLE_FLUSH_READY 0 #else #define LVGL_PORT_HANDLE_FLUSH_READY 1 #endif i am using esp-idf 5.0.2 so I should be good, and LVGL 8.+

I need to see the flush function in the LVGL port

static void lvgl_port_flush_callback(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_map)
    assert(drv != NULL);
    lvgl_port_display_ctx_t *disp_ctx = (lvgl_port_display_ctx_t *)drv->user_data;
    assert(disp_ctx != NULL);

    const int offsetx1 = area->x1;
    const int offsetx2 = area->x2;
    const int offsety1 = area->y1;
    const int offsety2 = area->y2;
    // copy a buffer's content to a specific area of the display
    esp_lcd_panel_draw_bitmap(disp_ctx->panel_handle, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, color_map);

This is very similar, almost identical to one’s ive seen online
edit: what I did to fix the error above(buffer_size) was have max_transfer_bytes as :

.max_transfer_bytes =((BSP_LCD_H_RES * BSP_LCD_V_RES)/10 ) * sizeof(uint16_t)
.buffer_size =((BSP_LCD_H_RES * BSP_LCD_V_RES)/10 )

since it is already being multiplied by sizeof(lv_color_t) here:

buf1 = heap_caps_malloc(disp_cfg->buffer_size * sizeof(lv_color_t), buff_caps);

not .buffer_size you want buffer_size

Try the attached source file.

esp32_s3.c (9.0 KB)