Upgrading Lvgl 8.3 to 9.x: Buggy and Laggy Display

Note: I’ve narrowed the issue posted here down - can someone close/delete that topic ?

Description

I have a Viewesmart display which worked fine with Lvgl 8.3 lib … until I redesigned most of the ui using fancy lvgl 9 additions, assuming that moving to lvgl 9 would be trival. What that was a dumb move! Turns out that migration can be quite tricky, and I haven’t been able to run 9+ properly so far. The display turns on but the colors are wrong the the animation super slow and glitchy. Since the hw implementation can be quite complicated, I could really use some help - thank you very much!

What MCU/Processor/Board and compiler are you using?

esp32s3
1.5" amoled with co5300 qspi driver
esp-idf chaintool

What do you want to achieve?

Getting LVGL 9 to work!

What have you tried so far?

Tried so many howtos I don’t even know how to list all of them. Basically the code has been ported from 8.3 to 9.x using the new syntax and new way to flush the buffers. Something is working since the display turns on and shows some sort of spinner!
What worked so far is protecting the lvgl thread with a mutex to keep the watchdog from crashing the system every 30s.

Code to reproduce

See attached file: the main.c code
knob_display.c (6.7 KB)

Screenshot and/or video

So after a lot of experimenting, I managed to swap the colors and fix the glitches using a rounder_cb.
Here is the fix to swap colors - to be pasted just before drawing the buffer in flush_cb:

lv_draw_sw_rgb565_swap(color_map, (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1));

For the rounder_cb implementation, check the attached source code.

The only issue left is that it’s soooooooooooo slow, to a point it’s barely usable. Looks like something is hijaking the flush_cb before it can print stuff on the display. Any idea what it could be ? Here my updated code:

knob_display_v9.c (9.0 KB)

Hi, I see that this topic from back in May, just can’t see what year.
I’m trying to do something similar on a similar screen with the same driver (waveshare rp2350 1.43" amoled).

I’m a noob when it comes to microcontrollers etc, but I can at least tell you why the performance is so bad. The screen works on an spi bus which has a fairly limited bandwidth, which is a problem on screens of this resolution.

There are 2 tricks you can do, the first you’re probably doing already (writing directly to DMA), the other one is dirty rectangles. If you can divide your interface in chunks, you can redraw only the rectangle that’s dirty (out of date).
Personally I’m going with a segmented rpm dial (it’s for a motorcycle), enforce a redraw rate of 20 FPS, and then only redraw sprcific regions at that rate. Temperature and battery voltage don’t need to be redrawn so often (maybe once every 5 seconds unless there’s a sudden change).

TL;Dr: budget the limited bandwidth on the spi bus.

Also, thanks a lot for sharing your code, this will save me a lot of work!

try this code

// LVGL display rounder callback for CO5300
static void rounder_event_cb(lv_event_t *e)
{
    lv_event_code_t code = lv_event_get_code(e);
    if (code == LV_EVENT_INVALIDATE_AREA) {
        lv_area_t *area = (lv_area_t *)lv_event_get_param(e);
        if (area) {
            // Round coordinates for CO5300 display optimization
            area->x1 = (area->x1) & ~1; // Round down to even
            area->x2 = (area->x2) | 1;  // Round up to odd
            area->y1 = (area->y1) & ~1; // Round down to even  
            area->y2 = (area->y2) | 1;  // Round up to odd
        }
    }
}

 lvgl_disp = lvgl_port_add_disp(&disp_cfg);
    if (lvgl_disp == NULL)
    {
        ESP_LOGE(TAG, "Failed to add display to LVGL port");
        return ESP_FAIL;
    }
    
    // Add rounder event callback for CO5300 optimization
    lv_display_add_event_cb(lvgl_disp, rounder_event_cb, LV_EVENT_INVALIDATE_AREA, NULL);
    ESP_LOGI(TAG, "Added rounder event callback for CO5300 optimization");

you can look LVGL Benchmarking video:
https://www.youtube.com/watch?v=jtXmBxqtBEE
Github:GitHub - lvgl/lv_port_viewe_knob_15_espidf

Uploading: bf9dc8e2-5c4f-4e4a-a075-bbbee0633197.png…

@fbiego Although it’s an old topic can you share some performance optimization tricks with the new comers.

We also have a trips and trick section for ESP32 here: Tips and tricks - LVGL 9.4 documentation

We would be happy to add more tips if needed.

I too am struggling with this a bit. I’m using the RP2350-based https://www.waveshare.com/2inch-capacitive-touch-lcd.htm which has an ST7789T3.

The vendor code uses a possibly modified version of v8, whereas I am running v9.4. CPU clock is at 200MHz and everything is running on a single core.

To test speeds, I set a GPIO pin high when the display callback is run, and then set it low after the SPI DMA is complete. A full screen update (320x240 RGB565) takes ~30-40ms at 100MHz SPI clock.

You need to use the RGB565 swap function for this display. You can save a bit of time using direct render mode, but you can’t use lv_draw_sw_rgb565_swap directly because this render mode only updates small areas of the buffer and the function expects a linear array: lv_draw_sw_rgb565_swap doesn't work with DIRECT render mode · Issue #9084 · lvgl/lvgl · GitHub

Instead do something like this:

    // swap channels in RGB565
    size_t row_size = area->x2 - area->x1 + 1;
    for (int i=area->y1; i<=area->y2; i++) {
        uint8_t *row = px_map + ((LCD_WIDTH * i) + area->x1) * 2;
        lv_draw_sw_rgb565_swap(row, row_size);
    }

Flushing to areas smaller than the full display is tricky, because DMA can’t use a stride or anything like that to transfer a small window. You only really have three options:

  1. Flush the whole display regardless of how small an area was updated
  2. Run multiple DMA SPI transactions, one per row that was updated
  3. memcpy the window of data into a separate buffer, then use DMA on that

Option 1 takes around 16ms as outlined above. Option 2 kind of defeats the purpose of DMA, so it’s not great. Option 3 is risky depending on the update size - you may run out of RAM depending on how large the area is, so you may need to fall back to just doing a full screen update if the window is large enough.

For option 1, you must only redraw on the final flush call, because on the non-final calls the rendering has not finished and you will get weird artifacts on the screen. Something like this will do:

    // redraw the whole display
    const uint16_t x1 = 0;
    const uint16_t x2 = LCD_WIDTH-1;
    const uint16_t y1 = 0;
    const uint16_t y2 = LCD_HEIGHT-1;

    if (lv_display_flush_is_last(disp)) {
        lcd_set_window(x1, y1, x2+1, y2+1);
        transfer_count = LCD_WIDTH * LCD_HEIGHT * 2;
    } else {
        lv_disp_flush_ready(tmp_disp_handle);
        return;
    }

LVGL also seems to render a lot slower when a debugger is attached (J-Link) or has been previously attached. I have no idea why, but my frame render times can go from 100ms to 10ms or less (with a spinner) just by hard resetting the chip to disconnect the debugger.

If I just disconnect the debugger, render times are still very slow. Power cycling or hard resetting improves the speed a lot.