[FreeRTOS] Avoid refreshing whole screen

Hi folks,

Description

I’m using an ATSAME70Q21 MCU connected to two 320x240 pixel displays with ST7789V drivers via SPI using DMA at 70 MHz. The system uses FreeRTOS and integrates LVGL.

There are two main FreeRTOS tasks (for graphics usage):

  • LVGL_Task - responsible for calling lv_timer_handler() periodically
  • ChartUi_RefreshTask - responsible for refreshing bars.

I want to visualize data using 16 bars, each consisting of 3 rectangles. But as you can see in the GIF below, when bar goes up, then whole screen is is updated, when the bar goes down, only the area of bar is redrawn. This has a crucial impact on FPS in my case. Could you let me know if there is a possibility to avoid such a behavior?

Configuration

#define LV_DEF_REFR_PERIOD  66      /*[ms]*/
#define LV_USE_OS   LV_OS_FREERTOS

I’m using LV_DISPLAY_RENDER_MODE_PARTIAL with 2 buffers, each covers 10% of screen (320 * 240 / 10).

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

MCU: ATSAME70Q21 (Cortex-M7, clock 300MHz)
Compiler: gcc version 10.3.1 20210824 (release) (GNU Arm Embedded Toolchain 10.3-2021.10)

What do you want to achieve?

I’d like to achieve that bars on the below GIF are being updated smoothly (10-15 fps would be good enough).

What have you tried so far?

Code to reproduce

Add the relevant code snippets here.

LVGL_Task:

void LVGL_Init(void)
{
    xTaskCreate(LVGL_Task, "lvgl", 2048, NULL, tskIDLE_PRIORITY + 2, NULL);
}

static void LVGL_Task(void *pvParameters)
{
    (void)pvParameters;

    /* Initialize LVGL */
    LVGL_Lock();
    lv_init();
    lv_tick_set_cb(xTaskGetTickCount);
    LVGL_Unlock();

    while (true) {
        /* The task running lv_timer_handler should have lower priority than that running `lv_tick_inc` */
        LVGL_Lock();
        lv_timer_handler();
        LVGL_Unlock();
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

ChartUi_RefreshTask:

static void ChartUi_RefreshTask(void *p)
{
    TickType_t xLastWakeTime;

    (void)p;

	xLastWakeTime = xTaskGetTickCount();

    while (1) {
        LVGL_Lock();
        ChartUi_RefreshBars();
        LVGL_Unlock();
        xTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(66));
    }
}

Screenshot and/or video

Nagrywanie 2025-09-17 142040

Firstly is didn’t think that FreeRTOS ran on Windows…

Secondly areas of update are on a per widget basis. so if you have the entire screen filled with sliders and you are updating all of the sliders between each call to lv_task_handler of course the entire screen might get redrawn. That because you have caused an update to every single widget to occur.

When dealing with an MCU there is a balancing act that has to be handled. That balancing act is memory and CPU use. It is going to be more CPU intensive in order to perform the calculations needed to locate and identify only the exact areas of a widget that need to get updated and to only update those very specific areas. The locations of those areas would need to be stored. Is it worth doing that extra work and consuming that extra memory? in 99% of the use cases it is not. while it might take a little bit more time to redraw the entire widget the amount of memory saved is more than likely worth it. The rendering time is also able to be mitigated by using DMA memory and double buffering…

running 2 tasks one that handles updating the widgets and another that handles calling lv_refresh_now is counter productive and will actually slow things down because of the context switching. A better solution is to use FreeRTOS messages and from one task you read the data from where ever it is that it is being collected from to set the values for the widgets. That task doesn’t actually set the widgets. it passes the data via a FreeRTOS message to a task that handles everything LVGL related. Well almost everything. There should be a 3rd task that only serves the purpose of incrementing the tick count in LVGL.

ticks in LVGL are a millisecond resolution. ticks in FreeRTOS are not. There are helper macros/functions in FreeRTOS that will convert a FreeRTOS tick into a millisecond time amount. It doesn’t appear that you are using those macros/functions and that could be causing an issue.

The other thing is by changing the refresh timeout to 66 you are changing the polling rates that LVGL has for updating the display and reading input from touch pads etc. If you want the best performance I personally would leave that set at 33 milliseconds and call lv_task_handler when you task comes full circle and make a call to lv_refr_now after each widget gets updated.

The MCU you are using would also be helpful to know. It would also be helpful to know what you environment is. Arduino IDE, ESP-IDF build system, Platform IO, etc…

So here is an example of using arc sliders in a manner that is similar to what you are attempting to do…

In the video the resolution is 450 x 450 x 24. There are a total of 56 arc widgets being used (4 layers 14 arcs wide). The arcs overlap each other and all of the arcs have a background opacity of 0. The color, opacity, start angle, stop angle and value are randomly generated each loop of the code. This is a really important test because of the large amount of calculation that is being done due to the alpha blending and the generation of new colors that are seen when the arcs overlap each other.

Now another important thing is this is running in Python with LVGL compiled as a shared library (DLL) using CFFI to access the DLL. I am using CFFI to attached to DLL files in Windows to handle the creation of the window and drawing the buffer to that window. Python is 200 times slower to execute than C code.

My point is you need to manage when work is being done and how it is being done.

In your specific use cause it is going to be best if you create 2 buffers that are the full display size. Keep the mode set to partial and to use DMA memory for the buffers and you transfer the buffers using DMA. The reason why this is better is because there is going to be far less overhead writing a single full display frame buffer than having to do all of the extra computations going back and forth filling small buffers. LVGL will hammer out how much of the buffers are going to be used and if the entire buffer doesn’t need to be used then it won’t use the whole thing. But when it does need to use it instead of having to cut it up into smaller chunks it is able to handle it all in a single go.

If you have memory constraints that will not allow you to use a full size frame buffer then you need to manage when the work is being done. Update a single widget and then immediately call lv_refr_now which will render only that single widget. Set you frame buffer size so it is large enough to hold the entire area of a single widget.