Slow flush calls (at max time interval) even when changing content faster

Using ESP_LVGL_port I noticed that the flush is done way slower than expected. I see SPI activity every 500ms even if I change content at a higher rate.

Verified that the 5ms timer tick is working and lv_timer_handler() is getting called (returns 0xFFFFFFFF).
However I am changing the content from a FreeRTOS timer task

ESP IDF 5.0.1 on an ESP32S3 with LVGL 8.3

void ui_test(TimerHandle_t puiTimer)
{   
    static int teststage = 0;
    switch(teststage)
    {
        case 0:
            lv_label_set_text(ui_TestItem, ".");
            break;
        case 1:
            lv_label_set_text(ui_TestItem, "..");
            break;
        case 2:
            lv_label_set_text(ui_TestItem, "...");
            break;
        default:
        case 3:
            lv_label_set_text(ui_TestItem, "....");
            break;
    }
    teststage++;
    if(teststage>3)
        teststage = 0;
}


void ui_TestScreen_init(void)
{
    lv_disp_t * dispp = lv_disp_get_default();
    lv_theme_t * theme = lv_theme_default_init(dispp, lv_palette_main(LV_PALETTE_BLUE), lv_palette_main(LV_PALETTE_RED), false, LV_FONT_DEFAULT);
    lv_disp_set_theme(dispp, theme);

    ui_TestScreen = lv_obj_create(NULL);
    lv_obj_clear_flag(ui_TestScreen, LV_OBJ_FLAG_SCROLLABLE);      /// Flags

    ui_TestItem = lv_label_create(ui_TestScreen);
    lv_obj_set_width(ui_TestItem, 32);
    lv_obj_set_height(ui_TestItem, 32);
    lv_obj_set_x(ui_TestItem, 320/2 - 16);
    lv_obj_set_y(ui_TestItem, 50);
    lv_label_set_text(ui_TestItem, "B");

    lv_disp_load_scr(ui_TestScreen);
}

And somewhere in my startup I call:

    ui_TestScreen_init();
    uiTimer = xTimerCreate("UI_TestLoop", 25 /* ticks of 10ms */, pdTRUE, NULL, ui_test);
    if (uiTimer)
        xTimerStart(uiTimer, 0);    

What I observe is that ui_test gets called every 250ms, yet I only see SPI activity every 500ms
And looking at the screen, I see ‘animations’ are skipped: only . and … are shown, … and … are missing

Now, if I change task_max_sleep_ms from 500ms to 250ms the update changes to 250ms

The esp_lvgl_port code is here:

The function influenced by the 500ms sleep is the one calling lv_timer_handler() :

static void lvgl_port_task(void *arg)
{
    uint32_t task_delay_ms = lvgl_port_ctx.task_max_sleep_ms;

    ESP_LOGI(TAG, "Starting LVGL task");
    lvgl_port_ctx.running = true;
    while (lvgl_port_ctx.running) {
        if (lvgl_port_lock(0)) {
            task_delay_ms = lv_timer_handler();
            lvgl_port_unlock();
        }
        
        //debugprint("taskdelay %d ms", task_delay_ms);

        if (task_delay_ms > lvgl_port_ctx.task_max_sleep_ms) {
            task_delay_ms = lvgl_port_ctx.task_max_sleep_ms;
        } else if (task_delay_ms < 1) {
            task_delay_ms = 1;
        }
        vTaskDelay(pdMS_TO_TICKS(task_delay_ms));
    }

    lvgl_port_task_deinit();

    /* Close task */
    vTaskDelete( NULL );
}

As written above, task_delay_ms is set to 0xFFFFFFFF by lv_timer_handler() and then capped to 500ms
Most probably I am missing some understanding of the concepts?

EDIT:
CONFIG_LV_DISP_DEF_REFR_PERIOD=30

pointers appreciated!

or LV_DEF_REFR_PERIOD in lv_conf.h. The default is set to 33 which is 33 milliseconds. this is when LVGL will actually redraw the display if there are changes that need to be made.

I also suggest setting up a custom tick increment function. Place that function into it’s own header file. all the function needs to do is return the millisecond runtime. then you set these macros in lv_conf.h

#define LV_TICK_CUSTOM_INCLUDE "YOUR_HEADER_FILE_HERE.h"         /*Header for the system time function*/
#define LV_TICK_CUSTOM_SYS_TIME_EXPR (YOUR_FUNCTION_CALL_HERE())    /*Expression evaluating to current system time in ms*/

You call UIloop from interrupt. = total nonse. Try change mind.

@kdschlosser the system tick works but your suggestion makes much sense as improvement, thanks

I’ll also verify that the refresh period that’s set in my config makes it into the actual code. All the other ones do, though, so I’m pretty sure my refresh is set correctly.

I’m trying to figure out in what code path LVGL checks for dirty areas and flush them, I thought it would be lv_timer_handler(), but then the ESP port code makes no sense, or lv_timer_handler() should return a value that results in 33ms refresh time?

@Marian_M what do you mean exactly?

thanks

lv_timer_handler essentially checks any registered timers to see if they have expired. If they have expired the registered callback in the timer gets called to carry out whatever task needs to be carried out. In the case of updating the display this only happens every 33 milliseconds by default. You can optionally call lv_refr_now which will force an update of the display.

For timed ui exist

lv_timer_t *timer = lv_timer_create(backfront, 1000, NULL);

xtimer is rtos for different use.
And refreshing display is based on inc tick as is configured in lv_conf.

LVGL isnt thread safe then use it in more task need …

And your example lacks info what you realy do. And you write SPI , but no line of flush code and drivers. Maybe try start from clean project.

@Marian_M the rest of the ‘missing’ code is in IDF: their LCD and panel driver. The display shows the correct data and I see the SPI data so I’m assuming that part is OK.

But I’ll retry updating with an lv_timer

@kdschlosser yes I saw that lv_timer_handler is checking timers to run, and since I’m not using any it returns 0xFFFFFFFF, that makes sense.
But what code path makes the 33ms display update happen? There’s the timer_tick and lv_timer_handler calls.

Or is this architecture documented somewhere

Try show me link where in IDF is flush callback from lvgl implemented.?

And for your next Q what code path makes the 33ms display you need reformulate it. Normal steps is show screen max possible speed and stay wait for events … changes. No next update arive to time when somethink changes. Changes is based on timers or indevs or your code invoked other way. USW…

33ms or better say lvconf refresh is for animation engine and define max speed for frames in animation … real speed is based on complexicity … for example my conf is set to 60Hz max but when animate full screen this drops to 18FPS …

So what you’re saying that if my change of an lvgl object isn’t done from an lv_timer (or event), it won’t be picked up as a dirty area to flush?

Sadly I have not had time yet to try calling my test code from an lv_timer and my weekend is overbooked so I may not be able to try until next week

I actually have a follow-up question: I saw my issue on the way to try to sync the flush to a hard realtime event - I have the SPI bus shared with another device that has hard realtime requirements, so my idea was to get potential flushes done after that device (then I’m sure it won’t bother me for some time).
I assume I can get that done by calling lv_timer_handler after that. And now you maybe understand why I want to also understand the flow of LVGL flushing.

I use stable releases and i dont see port here, but when arrives i dont like this mix of projects. Nobody then handle versions … And as you can see change or solve for example flush callback is hard undef work for overide.

And flushing and lv_timer is not connected. For simple explain flushing is started with object change, not by timer. And timer start what yiu orogram to callback, gui or not gui.

And for your SPI idea when change start SPI is immediatly ocupied to full render this change.

All refresh settings is for background animation code that initiate changes of object every xx ms defined in conf or more if operation need more time.

Sharing SPI can be handled only in flush CB where your code need wait for free window on SPI or if DMA is used is managed in driver code ESP IDF. But then you don have control over it.

@Marian_M

I use stable releases and i dont see port here, but when arrives i dont like this mix of projects. Nobody then handle versions

Espressif is offering stable LVGL releases through their managed components system in IDF, at least they use original version numbering, unlike PlatformIO which sucks at this (and lags in versions).
I would compare this to a linux package manager

FYI this is the relevant LVGL offering:
https://components.espressif.com/components/lvgl/lvgl

Some interesting observations…

Important note: still using the lv_timer_handler() calls from the Espressif port, which means there is a sleep after each lv_timer_handler() call based on the return value, but capped to 500ms. I kept this because it lets me learn how LVGL behaves:)

The setup: I’m going to update an object on the screen every 100ms
When I say flush happens, I mean I see the SPI traffic go out.

test 1: run my update code in an lv_timer()
outcome: flush happens 1.5ms after every update, in sync (100us jitter)

test 2: run my update code in a FreeRTOS task but also run an empty lv_timer() every 100ms
outcome: flush happens every 100ms but at an offset (62ms after the change)

test 3: run my update code in a FreeRTOS task but also run an empty lv_timer() every 200ms
outcome: flush happens every other display update, every 200ms, again at a fixed offset (62ms after the change)

test 4: run my update code in a FreeRTOS task and call lv_refr_now() after each update. No lv_timer() created
outcome: flush happens 1.5ms after every update, in sync (100us jitter) just like test 1
(lv_timer_handler() still returns 0xFFFFFFFF so flush must be happening from lv_refr_now() directly)

Conclusion: flushing happens when an actual timer (created with lv_timer_create()) runs (by means of lv_timer_handler()). If no timer runs, flushing depends on the rate at which you call lv_timer_handler().

So my possible solution(s) are:

  • update from an lv_timer
  • call lv_timer_handler at a faster rate (not using its return value)
  • use lv_refr_now()

Thanks @Marian_M and @kdschlosser for your insights, I hope this can help somebody else too and I hope @kdschlosser wants to post his PM below (or allows me to post it) as it contains helpful information for everybody!

Addendum: scope image of timing (old scope w/o USB so actual picture of display, sorry).
The yellow signal at the bottom is a GPIO I’m toggling.
First high->low = start of lv_label_set_text() call
low->high = end of that call + start of lv_refr_now(): took 420us
high->low = end of refr_now(): took 1400us
top signal is SPI bus

Platform = ESP32S3 at 240MHz and SPI bus at 40MHz, refreshing a text label of 32x32 px.
IDF 5.0.1, compiled -Os

I’m including here (with permission) a separate message from @kdschlosser

LVGL has a timer subsystem and everything runs inside of that system. LVGL is blind to how much time as passed and that is the reason why you have to call lv_tick_inc. what you pass to that function is the amount of time that has passed in milliseconds since you last called it. This can be done inside of an ISR (interrupt) as it does not allocate any memory.

It is going to be easier for you to understand the flow of LVGL if you do not use freeRTOS for the time being. It adds another layer of complexity due to LVGL not being thread safe. so to keep things simple for the time being work with a reduced amount of code.

The workflow of LVGL is this. There is a function in LVGL and in that function is the code that handles checking all of the objects to see if there is data that needs to be written to the display. That function is tied to a timer in LVGL. Now remember LVGL has no knowledge of the hardware it is running on so it is not aware of any SDK that is being used. It has no clue of the hardware it is running on is able to run multiple processes or threads. So by design LVGL is not able to do anything without you specifically instructing it to do so. Now you might be instructing it to do something but in a manner that you are unaware of it. Calling lv_task_handler is that manner. Since LVGL is unaware of the hardware it has no clue about how to capture time passed. That is the reason why you must call lv_tick_inc. you are telling LVGL how much time has passed. The function that handles checking the objects for data that needs to be written gets set into a timer. There is also a duration that gets set into that timer as well. So the timer is only going to call the function once that duration has passed. Nothing is going to cause that value to change so nothing in a widget is going to force the display to get updated. NOW… that being said. There are mechanics in place to force the display to get updated. These mechanics are in place for animations where the screen changing needs to happen in less than the default value of 33 milliseconds. You can use those same mechanics to force a display update after you change something in a widget/object.

The reason why LVGL works this way is for performance. could you image if you changed something and then it updated the display immediately. It would make LVGL horrifically slow. So it uses the timer as aa mechanism to group changes together to reduce the number of times it is writing to the display. after all the bottleneck is actually when data gets transmitted to the display so it is better to send multiple changes at one time then it is to do it after each and every single change. There are use cases where this is not ideal and it can cause unwanted jumps to GUI elements. Take the tachometer in a vehicle as an example. The data is being sent in 10ms intervals from the vehicle and if you only update the display say every 30 milliseconds the visual representation if the tachometer is going to make large jumps instead of 3 smaller ones… 30 milliseconds would be fine for things like the gasoline gauge or the oil pressure but not for speed and tachometer. We don’t want to waste time either and since we do not want to draw the gasoline gauge every 10 milliseconds so setting the default refresh timer value to 10 milliseconds would cause unwanted overhead. This is where lv_obj_invalidate and lv_refr_now comes into play. once you change an object’s value and if it has actually changed you would call lv_obj_invalidate(obj) and then lv_refr_now(disp). This will force and update to occur bypassing the 33 millisecond default timeout period. You want to be careful because any changes made to any other object prior to this will also get updated on the display. So if you are OK with some things updating every 30 milliseconds you MUST take care and only update the widgets for those things even if new information has come in DO NOT change the widget until after 30 milliseconds has passed.

Your main loop is time spent so if you have something taking place in your main loop that takes a long time to do this is going to cause updates to the display to happen less frequently. So you may want to periodically call lv_tick_inc and lv_task_handler throughout your loop instead of only once per loop. Remove calling lv_tick_inc and lv_task_handler and print out how long it takes for your loop code to run. Find out where in the loop it would be best to place those calls. You may have to place them in several locations.