Slow text updates when updating slider with lv_slider_set_value

Description

I am in my first LVGL project and trying to start with the ebike tutorial from SquareLine Studio. It’s beautiful stuff! I am running it with an ESP32 and ILI9488 inside the Arduino environment. After getting LVGL set up and tft_eSPI setup (at least to the best of my ability), I’m able to get everything running. For processing new data, I made a timer job that I call every 50 ms that just calls “lv_label_set_text_static” and points to some C character arrays in my code to update the GUI. As long as I do just that, all the numbers on the GUI update really quickly.

But in the ebike demo there are two sliders. If I call “lv_slider_set_value” to set the value on one of them inside that same timer, then all of the text updates get really janky and stuttery. I’m trying to figure out if there is anything I can do to get rid of that jankiness. For now, I’ve split my timer job into 2 timers… a fast one that runs every 50ms for the text fields, and a “mid” one that runs every 500ms for one of the sliders. But even with that setup, its obvious that when the mid timer fires, the text updates still get really slow.

Now, I may have tons of things enabled from SquareLine studio that I don’t need… I’m not using a touchscreen so perhaps there are events or things I can disable to speed it up? Or are there properties on elements which may be making the slider update slow such as scrollable stuff or any extra properties that I don’t need?

I was running on v8.2.0 but before writing this post I tried upgrading v8.3.5 but it has the same result.

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

ESP32 dev kit inside the Arduino environment. Also hooked up to an ILI9488 using setup 21.

What LVGL version are you using?

I’ve tried v8.2.0 and v8.3.5

What do you want to achieve?

All I really need is to find a way to speed up the slider update. Right now I’m calling lv_slider_set_value with new data obtained in the background. But if there is a faster way to do it that can ignore other properties that might solve my issue.

What have you tried so far?

I’ve tried moving the calls to lv_slider_set_value into slower timers, but that isn’t really helping much. I’ve also tried calling it with animation on and off. Didn’t seem to make a difference.

Code to reproduce

The full code from SquareLine has a ton of files so I can’t post something that can completely compile here. However it is the ebike demo if that helps.

/* Inside my main program I set up my timers like this:*/
  display_update_task_fast = lv_timer_create(update_display_values_fast,   50, NULL);
  display_update_task_mid  = lv_timer_create(update_display_values_mid,   500, NULL);
  lv_timer_ready(display_update_task_fast);
  lv_timer_ready(display_update_task_mid);

// Then, my fast and mid timers looks like this, although I'd be perfectly fine if I could have them run
// all in one timer job. This was my attempt at a workaround.
void update_display_values_fast( lv_timer_t * task ) {
  // Update our global character arrays for text with the values from the CAN bus
  dtostrf(torque_cmd,      1, 1, torque_cmd_str);
  dtostrf(eff_torque,      1, 1, eff_torque_str);
  dtostrf(motor_rpm,       1, 0, motor_rpm_str);
  
  // Update the labels on the GUI pointing to our own memory
  lv_label_set_text_static(ui_Label_Torque_Req_Value,      torque_cmd_str);
  lv_label_set_text_static(ui_Label_Torque_Eff_Value,      eff_torque_str);
  lv_label_set_text_static(ui_Speed_Number_1,              motor_rpm_str);
}

void update_display_values_mid( lv_timer_t * task ) {
  // Update sliders
  lv_slider_set_value(ui_Slider_Speed,  (int)motor_rpm,  LV_ANIM_OFF);
}

Screenshot and/or video

Can you show me how torque_cmd_str, eff_roque_str and motor_rpm_str are declared, and what numbers you expect for torque_cmd, eff_torque, motor_rpm.

they are just floats:

float  torque_cmd;
float  eff_torque;
float  motor_rpm;

and the numbers i expect from them are just floats near zero (nothing terribly big). Then the Arduino function dtostrf converts them to a character array (since the sprintf function in Arduino-land doesn’t have floating point support by default).

It is worth pointing out though that these numbers are working fine and updating nice and fast as long as I comment out the call to lv_slider_set_value(). If that call isn’t in there, the three variables update as expected and nice and fast. As soon as I add that lv_slider_set_value() call in, then everything gets janky. :frowning:

Sorry, still miss the declaration for torque_cmd_str, eff_roque_str and motor_rpm_str

oh sorry, i must have misread your first reply. Those are character arrays and they are globals.

char torque_cmd_str[8];
char eff_torque_str[8];
char motor_rpm_str[8];

The slider set_value call is taking the motor_rpm float casted to an integer. Presumably that is ok.

Ok, Eight characters seems to be ok, as long as your numbers are really small.
The function dtostrf is a risky one. There is no checking whether the char array is big enough.
So, if someone is declaring a small char array and/or using a large number, this function will lead to unforseen behaviour.

Maybe you could check that by outcommenting the following lines:

  dtostrf(torque_cmd,      1, 1, torque_cmd_str);
  dtostrf(eff_torque,      1, 1, eff_torque_str);
  dtostrf(motor_rpm,       1, 0, motor_rpm_str);

And write the following right after it (e.g.):

 sprintf (torque_cmd_str,   "0.123");
 sprintf (eff_torque_str,   "1.23");
 sprintf (motor_rpm_str,    "12.3");

For the first sight I don’t see any other location which could make any problems.

ok, i commented out the other timers so that only the mid timer is running and I set it to run every 50ms. And the only line that is running in any timer is:

lv_slider_set_value(ui_Slider_Speed,  (int)motor_rpm,  LV_ANIM_OFF);

So that should rule out any potential side effects in other timer code. And even with that one line running, and that motor_rpm value constantly updating, the screen only updates about 4 times per second.

Any ideas on whether there is some code in lv_slider_set_value() that might be taking a long time to run or maybe there is something inside that code that is blocking?

I’m calling lv_timer_handler() every time though the Arduino main loop() call. Is that how things are intended to be done? I don’t have any other code for ticks or anything (I think the instructions had me set LV_TICK_CUSTOM to 1 so that is in there also.

Any ideas on making this slider in the ebike demo update faster would be super appreciated.

I don’t know about the lv_timer_handler. I do not really understand the docs about the lv_timer_handler yet.

Do you have any calling of function delay () within your code?

no, nothing. I just played around with it and added some timing printouts and it looks like with just the text updates in, that timer callback takes about 100ms to run and if I add in a call to lv_slider_set_value() then that makes the callback take about 412ms.

When I look at the Arduino examples, there is just the following in loop:

void loop () {

  lv_task_handler (); /* let the GUI do its work */
  delay (5);
}

BTW, that delay (5) within the loop is strange enough!

Furthermore as I can see in lvgl simulator examples. The lv_timer_handler is called in succession when calling lv_task_handler.
So I really do not see why someone should call lv_timer_handler from outside of lvgl.

How does your loop () look?

lv_task_handler is just calling lv_timer_handler, nothing more, so both is the same.

lv_task_handler simply checks the timers that have been created to see if they have expired or not. If they have expired then the task gets run and the timer gets reset.

There is the LV_DEF_REFR_PERIOD macro that sets the duration for the timer. The default value is 33 milliseconds. So no matter how often you call lv_task_handler the display is only going to get refreshed every 33 milliseconds.

In the lv_display_drv_register function is where the timer gets set. This is easily changed after calling that function

lv_disp_drv_t * driver = lv_disp_drv_t;
lv_disp_drv_init(driver);

lv_disp_t * disp = lv_disp_drv_register(driver);

lv_timer_del(disp->refr_timer);
disp->refr_timer = lv_timer_create(_lv_disp_refr_timer, {new update speed}, disp);

LV_ASSERT_MALLOC(disp->refr_timer);
if(disp->refr_timer == NULL) {
    // error handling here for not being able to allocate the memory for the timer
}

I am not very proficient in C code so the example above is pseudo code and has not been tested at all. I do not know if _lv_disp_refr_timer is in the lvgl header file and you may need to include "/src/core/lv_refr.h" in order to get access to it to set the callback properly.

So when you call lv_task_handler in your code the timer for refreshing the display will be smaller so the display would get refreshed more often.

You should have lv_task_handler being called either using an ISR (hardware timer) or calling it in your main loop. You can optionally set up a function to handle setting the ticks and calling lv_task_handler and then call that function when widgets get updated or once in the main loop and then also after setting a widgets value that you want to update in a faster manner. But if the timer for refreshing the display has not expired when you call the function it will still not update. Setting the timer value too small could result in poor overall performance. if you set the timer value to 5 milliseconds you would want to add a 5 millisecond delay between you set the new value and when lv_task_handler gets called to ensure the widget gets updated. That may provide better performance.

There are 2 animals that are being dealt with. the first one is how much time passes between calls to lv_task_handler and also what the timer is set to for refreshing the display. so if you have the default of 33 milliseconds for the refresh and you have 20 milliseconds between calls to lv_timer_handler it could be 40 milliseconds until the display gets refreshed again.

While 33 milliseconds doesn’t seem like a long amount of time it is if you are updating some text on the display based on where the slider has been set to. dragging the slider is not going to allow for instant updates to the text especially if using an event to update that text. You would be better off collecting the slider value and setting the text in the loop. an event does not stop the running of the loop it adds a task to the handler and when lv_task_handler gets called is when the event callback will get called.

It is better to buffer the value so you can check and see if the value of the slider has changed and if it has then update the text, use a delay to stall for the amount of time you have set the refresh timer to be and then call lv_task_handler.

I just remembered the lv_refr_now function. You can also call that to force an immediate refresh of the display as well. You still don’t want to use an event for maximum performance.

So there are 2 ways to go about improving the speed as noted above.

also when you call lv_task_handler are you updating the time that has passed between calls?

uint32_t last_handler_time = (uint32_t) millis();
uint32_t current_time;

void loop() {
    current_time = (uint32_t) millis();
    lv_tick_inc(current_time - last_handler_time);
    last_handler_time = current_time;
    lv_task_handler();
}

yeah, when I was first writing the code I was following documentation for the task system and not finding any of the _task functions defined in my LVGL code. Grepping through everything found nothing. I eventually found an issue in the LVGL github pages where someone was asking a similar question and a developer said that all of the task stuff had been replaced with the timer system in a recent release. I guess they just haven’t had time to update the documentation yet. Hopefully that helps someone else whose searches lead them here.

That said, we may be talking about different things. I would absolutely LOVE to have my code run at 33 ms. The LVGL code I am calling to update the slider is currently orders of magnitude slower than that so trying to put together code that lets my system run timer jobs faster than 33ms is definitely not my concern at all.

Again, my problem is pretty simple… whenever I call lv_slider_set_value()… that function… that function specifically… takes 100s of milliseconds to run. About 400ms actually. This is for the slider in the ebike demo that comes with SquareLine studio. So no amount of optimizing the calls in the timer system will ever overcome the fact that this LVGL code is taking so long on an ESP32.

So while I appreciate the code and research into how I might be able to run timers even faster (hopefully that helps someone reading this thread in the future), I think if there isn’t some way to speed up what is being run inside lv_slider_set_value(), then how fast I call timers really doesn’t matter for me.

Thanks again for your suggestions.

I don’t think it is a problem of lvgl in principle.
There is a little dirty thing what does all the quirky things.
It should never last so long.
And of course from your first post:

I think what you are having an issue with is how long your code takes before getting back around to running lv_task_handler and or you are not setting the ticks that have passed since the last update.

One of things you have to remember that is LVGL is portable, it will run on a Windows computer, as a web page and also on MCUs. It is not designed to keep time and it has to be told how much time has elapsed. So if you don’t tell LVGL guess what? it’s going to take a seriously long time to do things.

You should be able to update the display by calling lv_refr_now when you change anything in the UI. This is going to work great for updating things that you programmatically set but is not going to work if you are setting a value based on user input. You are at the mercy of 2 timers if you are wanting to update some text based on a widget being controlled by the user. The first timer being the display update and the second is the timer for the touch interface.

Without seeing the entire program we are basically trying to locate a peanut on the floor in a room that has no source of light. LOL. Would eventually get it done but it would take a HUGE amount of time to do. It would be easier if you could share the code. GitHub works great for doing this. we would be able to see exactly where the holdup is.

It is usually the simplest thing that causes the most complex problems. This is because the simplest things tend to get overlooked.

That right there is what I call a life statement and a very true one at that.

This is what you are looking to do… Watch more than the first few seconds.

In order to do this I did away with the timers in LVGL and I am updating the display and also capturing touch input on my programs schedule. There is a lot going on here. Reading of 2 temperature sensors. setting the speed for 2 motors. Reading current draw and 2 different voltages. monitoring the inputs for any problems, updating the UI and also capturing user input in the form of a touch screen.

The reason why I say to watch the video closely is because you will see the one bar moving and at the same time the large temperature reading in the middle is changing and also the temperature bar on the left of the screen.

Well, I don’t think so. I am running using LV_TUCK_CUSTOM as described in the docs here which should be getting the ticks directly from the ESP32: Quick overview — LVGL documentation

And i see it calling my code exactly on schedule and everything working fine, until I uncomment a call to lv_slider_set_value(). With that call in, the display refresh now takes hundreds of milliseconds so everything gets delayed until it finishes.

I’ve walked through all of the lvgl code (or maybe most of it) from calling set_value on a slider and seeing where it is calculating how much screen area to invalidate and adding it to the list of areas that are invalid and checking for overlap, etc. It seems like the more area that needs to be redrawn, the slower the update (which makes sense), but the slider in the example seems particularly expensive. I thought perhaps it was due to it using a png background image so I converted it to just be a rectangular slider without a background image, but it didn’t help. It actually made it a little worse.

I tried this and it had no effect on how long it took to update the screen. Also all of my updates are done programmatically - I don’t have any screen input / touch screen.

I watched your video. What processor and display are you using?

I recorded a video of my issue. You can see it here:

I am using an ESP32 as well. Except I am also running Micropython which handicaps the performance aspect of things big time. Python code can be 200 times slower than C code.

You can set the costom tick all you like but if you do not time how long your code takes to execute before getting back around to calling lv_task_handler LVGL is not going to have any idea how long your code execution took. The only thing that LVGL takes a time measurement on is how long it takes for it’s code to run. If you do not have the other part of the equation then it is going to be a long time before the timers expire in lv_task_handler

as an example.

Say the task handler takes 5 ms to run and your code takes 50 to execute before the task handler is called again. LVGL is only going to time it’s code so if you do not call lv_tick_inc the amount of time between screen refreshes is going to be around 330 milliseconds. This is because LVGL is only going to account for it’s code execution time. In the example I said that is 5 milliseconds. so if the display refreshes every 33 milliseconds how many times would the task handler have to run at 5 milliseconds each time? 6.6 times. if your code execution time is 50 milliseconds and that information is not being given to LVGL then that time is not being used so your loop would have to run 6.6 times before the display refreshed and at 50 milliseconds each time that makes 330 milliseconds.

all you have to do is the following…

uint32_t last_handler_time = (uint32_t) millis();
uint32_t current_time;

void loop() {
    current_time = (uint32_t) millis();
    lv_tick_inc(current_time - last_handler_time);
    last_handler_time = current_time;
    lv_task_handler();
    // execute your code here
}

This will let LVGL know how much time has passed between calls to the task handler.

what is your frame buffer size?

if you have to large a frame buffer it will slow things down a lot.

The frame buffer shouldn’t be larger than 1/4 of width * height * sizeof(lv_color_t)

how fast do you have the transfer speed set to for the SPI? You are going to be limited in the speed that you can do because of the jumper wires and also the pin header. at most you will get 40mhz. If you had a display with the esp32 built into the display you can typically get 80mhz. That is going to transfer the data to the display twice as fast as what you have right now. you can try bumping the speed to 60 and 80 to see if it will work.

Because of the setup you have I would run a frame buffer that is 1/8th of a single frame of data. so

(width * height * sizeof(lv_color_t)) / 8

Are you using DMA memory for the frame buffer?

Also I wanted to mention that the ebike demo is not going to be the best in terms of performance The battery slider is an image that is 106 x 235. That’s going to have a size of 3 * 106 * 235. That means on a redraw 597,840 bits for the one image alone is going to have to be sent. It is going to take anywhere from 15 milliseconds to 20 milliseconds to send that one image alone on a 40mhz SPI speed.

This is the reason why I am using a color for any sliders I want to be fast moving and not an image.

The other thing is you mentioned using the Arduino IDE which means you are using tft-spi or the like to handle the communications with the display. The issue there is going to be the copying of data from one buffer to another buffer and then out of that buffer to the display. It adds an extra step which takes time to complete when you have large items needing to be redrawn on the screen.

What I am seeing in your video looks like it is performing how it should be considering what the UI is and also what the connection to the display is. If you want better performance you might want to consider using a 16 bit display and writing your own driver for the display.

I’m just doing what the developers say to do for Arduino based projects. In the documentation for using the Arduino it specifically says to use LV_TICK_CUSTOM instead of calling lv_tick_inc(). And in the lv_conf.h template it specifically says that it "removes the need to manually update the tick with lv_tick_inc():

/*Use a custom tick source that tells the elapsed time in milliseconds.
 *It removes the need to manually update the tick with `lv_tick_inc()`)*/
#define LV_TICK_CUSTOM 1
#if LV_TICK_CUSTOM
    #define LV_TICK_CUSTOM_INCLUDE "Arduino.h"         /*Header for the system time function*/
    #define LV_TICK_CUSTOM_SYS_TIME_EXPR (millis())    /*Expression evaluating to current system time in ms*/
#endif   /*LV_TICK_CUSTOM*/

It does this by having access to the millis() Arduino function directly so it can calculate the time between calls. And they really mean it too: the code won’t compile with any calls to lv_tick_inc() in there if LV_TICK_CUSTOM is defined as 1 because they actually use a macro to remove the definition of that function.

But, I’m up for trying anything! Perhaps there is a bug in the implementation! So I removed LV_TICK_CUSTOM and changed my code to do the tick increment myself as you suggested. The results were exactly the same.

Right now I’m using what was recommended in the template code which is:

static lv_color_t buf[ screenWidth * screenHeight / 10 ];
...
lv_disp_draw_buf_init( &draw_buf, buf, NULL, screenWidth * screenHeight / 10 );

But if I can adjust something here to make it faster that sounds like a good idea to try.

Yeah, this is kinda what I’ve been thinking all along. When I posted my original issue, I can see that the time it takes to update the screen when calling lv_slider_set_value() is just long.

As I mentioned above, I tried converting the slider to a color based slider only and not use any image. The performance was actually a little worse. As I walk through the code it has more to do with the size of the widget and the number of pixels that are changing than it does with it being image based or color based. At least in my case with my hardware that seems to be true. Which lends itself more to what you described above with my display and the SPI bus and just the amount of time it takes to draw an area of pixels.

I think I’m going to revert to just removing some functionality in the display as a workaround. Another thing I may look into is whether the 2nd core of the ESP32 could be utilized. I’m not sure, but I think all of LVGL is just running on one core of the ESP32, so I wonder if something could be better by utilizing the second core? Probably not if this issue is a data transfer limitation because there isn’t a ton of expensive work that can be done in parallel during the transfer. So my gut tells me it won’t make much difference.

But I hope that someone else’s google search terms can lead them here and help them out if they are in a similar boat. Thanks again.