Dropdown list does not close after selection

Description

Basic / simple drop-down menu does not close once item is selected !?
Looks ok with online simulator, so I assume I’m doing something wrong…

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

Two different configurations:

1/ custom board with STM32U5A5 + 320x480 LCD screen (ST7796S controller / RGB565 swapped) + I2C touch screen ; screen is connected to MCU with parallel bus, and I’m using MCU’s FMC hardware to write data to the LCD ; note that it’s a bare-metal application, there is no operating system.
Compiler is arm-none-eabi-gcc (GNU Toolchain for the Arm Architecture 11.2-2022.02 (arm-11.14)) 11.2.1 20220111

2/ simulator of above custom board, running on Linux Ubuntu ; simulator is quite accurate and simulates all required MCU & peripherals hardware. So I also have a simulator for the LCD display controller, touch screen, and all other chips on the custom board…
Compiler is gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0

Both configurations show same issue.

What LVGL version are you using?

9.3.0

What do you want to achieve?

Once an item is selected, I would like the dropdown menu to close and the items list to be hidden.
I expect screen area behind the drop down list to be refreshed.

What have you tried so far?

It’s the first time I’m using LVGL, so I started to have it compile with my project, and “connect” it to the the LCD and to the touch screen => OK
Then I created a basic button + callback => OK
Then I added a switch + callback => OK.
Then I added a simple dropdown menu => this issue.

Since my issue occurs on both actual board and on simulator, I focused on debugging with simulator.
Simulator adds a huge amount of powerful tools.

I’m a unit test psychopath, so I wrote a unit test for my issue.
The scenario is:
1/ get the pixel color where the dropdown menu list should pop
2/ open dropdown menu (finger touch & release)
3/ select item #2 (finger touch & release)
4/ get the same pixel and assert it has the same color (screen refreshed)

I noticed the dropdown menu list should close when user removes his finger (RELEASE) from the wanted item, so I start to debug when I ask the touch screen simulator to remove the finger from the selected item.

Also I updated my LCD simulator, and it’s now saving its internal pixels buffer after each selected area memory write complete. It adds some red dots around the selected pixel area so I can see which pixel area was written.

My unit test:

TEST(test_lvgl_dropdown)
{
    test_helper_console_enter_string("trace app.lvgl debug");
    test_core_run_ms(50U);

    uint8_t red1, green1, blue1;
    stub_lcd_get_pixel(100U, 200U, &red1, &green1, &blue1);
    test_info("TEST", "red=0x%02X green=0x%02X blue=0x%02X",
        (unsigned)red1,
        (unsigned)green1,
        (unsigned)blue1);

    /* Put finger on screen on dropdown menu */
    stub_touch_press(0U, 80U, 140U);
    test_core_run_ms(250U);

    /* Remove finger, should open dropdown menu */
    stub_touch_release(0U);
    test_core_run_ms(250U);

    /* Put finger on screen, on 2nd item */
    stub_touch_press(0U, 80U, 210U);
    test_core_run_ms(250U);

    if( test_debugger_is_attached() )
        test_debugger_trigger_breakpoint();

    /* Remove finger from 2nd line */
    stub_touch_release(0U);
    test_core_run_ms(250U);

    uint8_t red2, green2, blue2;
    stub_lcd_get_pixel(100U, 200U, &red2, &green2, &blue2);
    test_info("TEST", "red=0x%02X green=0x%02X blue=0x%02X",
        (unsigned)red2,
        (unsigned)green2,
        (unsigned)blue2);
    CU_ASSERT(red1   == red2  );
    CU_ASSERT(green1 == green2);
    CU_ASSERT(blue1  == blue2 );
}

Screenshot and/or video

Above unit test makes LVGL perform 11 flush requests.

Touching screen on dropdown menu:

Steps 1 to 5 : finger is touching the screen
Steps 6 to 9 : finger has been removed from screen

Touching 2 item in menu list

Step 10 : finger is touching item #2 on list (Banana)
Step 11 : finger has been removed from screen

Selected item (Banana) is visible on dropdown box.
But there is no refresh of the 3-items list.
I expect this area to be invalidated & updated on the LCD.

Code to reproduce

Here are extracts of my code…

Global variables

static struct {
    struct debug_t         dbg;
    struct ticks_t         timer;
    struct touch_gesture_t gesture, gesture_old;
    lv_display_t*          disp;
    lv_indev_t*            indev;
    uint16_t               buffer1[DDM_COMPONENT_CUSTOMIZE_LCD_WIDTH * DDM_COMPONENT_CUSTOMIZE_LCD_HEIGHT];
    uint16_t               buffer2[DDM_COMPONENT_CUSTOMIZE_LCD_WIDTH * DDM_COMPONENT_CUSTOMIZE_LCD_HEIGHT];
} ctxt;

LVGL initialization

static void lvgl_init(void)
{
    lv_obj_t* screen;
    lv_obj_t* widget;

    /* Default state */
    ticks_schedule(&ctxt.timer, 0U);
    ctxt.gesture_old.what = TOUCH_GESTURE_TYPE_NONE;

    /* Initialize LVGL */
    lv_init();
    ctxt.disp = lv_display_create(DDM_COMPONENT_CUSTOMIZE_LCD_WIDTH, DDM_COMPONENT_CUSTOMIZE_LCD_HEIGHT);
    lv_display_set_color_format(ctxt.disp, LV_COLOR_FORMAT_RGB565);
    lv_display_set_flush_cb(ctxt.disp, lvgl_on_flush_start);
    lv_display_set_buffers(ctxt.disp, &ctxt.buffer1[0], &ctxt.buffer2[0], sizeof(ctxt.buffer1), LV_DISPLAY_RENDER_MODE_PARTIAL);

    /* Create input device */
    ctxt.indev = lv_indev_create();
    lv_indev_set_type(ctxt.indev, LV_INDEV_TYPE_POINTER);
    lv_indev_set_mode(ctxt.indev, LV_INDEV_MODE_EVENT);
    lv_indev_set_read_cb(ctxt.indev, lvgl_on_read_input);

    /* Set custom 1ms ticks handler */
    ticks_set_handler(lvgl_on_ticks);

    /* Get active screen, needed for widgets creation */
    screen = lv_screen_active();

    /* Create a drop-down menu */
    widget = lv_dropdown_create(screen);
    lv_dropdown_set_options(widget, "Apple\n"
                                    "Banana\n"
                                    "Orange");
    lv_obj_set_pos(widget, 10, 120);
}

Flush callback function

My internal functions lcd_select() and lcd_copy() are async and only prepare the job.
LCD actual update is performed when lcd_run() is called.
test_core_yield() is an injected test function required here to let my simulator lives.
Usually it’s not required for simulator code to be such intrusive in firmware code, but it helps a lot here to be minimalist.

static void lvgl_on_flush_start(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map)
{
    debug(&ctxt.dbg, "LVGL: redraw (%"PRIi32",%"PRIi32") -> (%"PRIi32",%"PRIi32")",
        area->x1, area->y1,
        area->x2, area->y2);

    uint16_t w     = area->x2 - area->x1 + 1U;
    uint16_t h     = area->y2 - area->y1 + 1U;
    uint32_t count = (uint32_t)w * (uint32_t)h;

    lv_draw_sw_rgb565_swap(px_map, count);
    lcd_select(area->x1, area->y1, w, h);
    lcd_copy(px_map, count, 0U/*gap*/, true/*restart*/);
    while( !lcd_is_ready() )
    {
#ifndef PLATFORM_target
        test_core_yield();
#endif
        lcd_run();
    }
    lv_display_flush_ready(ctxt.disp);
}

Input device reading


static void lvgl_on_read_input(lv_indev_t* indev, lv_indev_data_t* data)
{
    switch( ctxt.gesture.what )
    {
        case TOUCH_GESTURE_TYPE_PRESS:
        case TOUCH_GESTURE_TYPE_MOVE:
            data->point.x = ctxt.gesture.pos.x;
            data->point.y = ctxt.gesture.pos.y;
            data->state   = LV_INDEV_STATE_PRESSED;
            break;

        case TOUCH_GESTURE_TYPE_RELEASE:
        default:
            data->state = LV_INDEV_STATE_RELEASED;
            break;
    }
}

Ticks handler

This function is called from SysTicks IRQ every millisecond


static void lvgl_on_ticks(void)
{
    lv_tick_inc(1U);
}

Internal function to evaluate if two events reported by touch screen controller are the same.

When a finger is detected, the touch controller on my board triggers lot of redundants events at ~60Hz which is useless for LVGL


static bool is_gesture_change(const struct touch_gesture_t* g1, const struct touch_gesture_t* g2)
{
    return    g1->what  != g2->what
           || g1->pos.x != g2->pos.x
           || g1->pos.y != g2->pos.y;
}

Run function

This function is called from main() as often as possible


static void lvgl_run(void)
{
    if( ticks_is_elapsed(&ctxt.timer) )
    {
        uint32_t next = lv_timer_handler();
        ticks_schedule(&ctxt.timer, next);
    }

    if( touch_pop_event(&ctxt.gesture) && is_gesture_change(&ctxt.gesture, &ctxt.gesture_old) )
    {
        char buf[64];
        debug(&ctxt.dbg, "LVGL: %s", touch_gesture_to_string(&ctxt.gesture, &buf[0], sizeof(buf)));
        lv_indev_read(ctxt.indev);
        ctxt.gesture_old = ctxt.gesture;
    }

    lv_task_handler();
}

Firmware log output

I dump flush requests and touch screen events:
Value in first column is a timestamp (unit is second).

root@dev:/ # trace app.lvgl debug
app.lvgl:       debug
root@dev:/ # 
[2.050] LVGL: press at (80,140)
[2.050] LVGL: redraw (10,120) -> (139,155)
[2.083] LVGL: redraw (10,120) -> (139,155)
[2.116] LVGL: redraw (10,120) -> (139,155)
[2.149] LVGL: redraw (10,120) -> (139,155)
[2.182] LVGL: redraw (10,120) -> (139,155)
[2.300] LVGL: release
[2.300] LVGL: redraw (10,120) -> (139,155)
[2.300] LVGL: redraw (0,0) -> (129,129)
[2.300] LVGL: redraw (10,156) -> (139,265)
[2.399] LVGL: redraw (10,120) -> (139,155)
[2.432] LVGL: redraw (10,120) -> (139,155)
[2.465] LVGL: redraw (10,120) -> (139,155)
[2.550] LVGL: press at (80,210)
[2.550] LVGL: redraw (10,156) -> (139,265)
[2.800] LVGL: release
[2.800] LVGL: redraw (10,156) -> (139,265)
[2.800] LVGL: redraw (10,120) -> (139,155)

Extract of simulator log output.

Probably not very usefull.
You can see commands received by the LCD & touch screen simulators
CASET = Column Address Set (1st command to select pixel area)
RASET = Row Address Set (2nd command to select pixel area)

[2.05000] TEST: red=0xF0 green=0xF4 blue=0xF0
[2.05000] FT6336G: point #0 pressed at (80,140) ; reporting to X1H
[2.05030] LCD: ST7796S cmd `CASET'
[2.05036] LCD: ST7796S SELECT from=(10,0) size=(130,480)
[2.05036] LCD: ST7796S cmd `RASET'
[2.05044] LCD: ST7796S SELECT from=(10,120) size=(130,36)
[2.05046] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.05046] LCD: saving `output/LCD/host/version_test/bmp/001-internal_010-120_130x36.bmp' 320x480
[2.08304] LCD: ST7796S cmd `CASET'
[2.08310] LCD: ST7796S cmd `RASET'
[2.08320] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.08320] LCD: saving `output/LCD/host/version_test/bmp/002-internal_010-120_130x36.bmp' 320x480
[2.11604] LCD: ST7796S cmd `CASET'
[2.11610] LCD: ST7796S cmd `RASET'
[2.11620] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.11620] LCD: saving `output/LCD/host/version_test/bmp/003-internal_010-120_130x36.bmp' 320x480
[2.14904] LCD: ST7796S cmd `CASET'
[2.14910] LCD: ST7796S cmd `RASET'
[2.14920] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.14920] LCD: saving `output/LCD/host/version_test/bmp/004-internal_010-120_130x36.bmp' 320x480
[2.18204] LCD: ST7796S cmd `CASET'
[2.18210] LCD: ST7796S cmd `RASET'
[2.18220] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.18220] LCD: saving `output/LCD/host/version_test/bmp/005-internal_010-120_130x36.bmp' 320x480
[2.30000] FT6336G: point #0 released
[2.30018] LCD: ST7796S cmd `CASET'
[2.30024] LCD: ST7796S cmd `RASET'
[2.30032] LCD: ST7796S SELECT from=(10,156) size=(130,110)
[2.30034] LCD: ST7796S GRAM from=(10x156) size=(130x110) area 100% written
[2.30034] LCD: saving `output/LCD/host/version_test/bmp/006-internal_010-156_130x110.bmp' 320x480
[2.39905] LCD: ST7796S cmd `CASET'
[2.39911] LCD: ST7796S cmd `RASET'
[2.39919] LCD: ST7796S SELECT from=(10,120) size=(130,36)
[2.39921] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.39921] LCD: saving `output/LCD/host/version_test/bmp/007-internal_010-120_130x36.bmp' 320x480
[2.43205] LCD: ST7796S cmd `CASET'
[2.43211] LCD: ST7796S cmd `RASET'
[2.43221] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.43221] LCD: saving `output/LCD/host/version_test/bmp/008-internal_010-120_130x36.bmp' 320x480
[2.46505] LCD: ST7796S cmd `CASET'
[2.46511] LCD: ST7796S cmd `RASET'
[2.46521] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.46521] LCD: saving `output/LCD/host/version_test/bmp/009-internal_010-120_130x36.bmp' 320x480
[2.55000] FT6336G: point #0 pressed at (80,210) ; reporting to X1H
[2.55030] LCD: ST7796S cmd `CASET'
[2.55036] LCD: ST7796S cmd `RASET'
[2.55044] LCD: ST7796S SELECT from=(10,156) size=(130,110)
[2.55046] LCD: ST7796S GRAM from=(10x156) size=(130x110) area 100% written
[2.55046] LCD: saving `output/LCD/host/version_test/bmp/010-internal_010-156_130x110.bmp' 320x480
[2.80000] FT6336G: point #0 released
[2.80018] LCD: ST7796S cmd `CASET'
[2.80024] LCD: ST7796S cmd `RASET'
[2.80032] LCD: ST7796S SELECT from=(10,120) size=(130,36)
[2.80034] LCD: ST7796S GRAM from=(10x120) size=(130x36) area 100% written
[2.80034] LCD: saving `output/LCD/host/version_test/bmp/011-internal_010-120_130x36.bmp' 320x480
[3.05000] TEST: red=0xD8 green=0xDC blue=0xD8

Deep debugging

When running unit test with debugger, I can trace what is happening when touch screen controller reports “finger removed” when finger was on 2nd dropdown list item.

I have traced up to finding invalidating for area (10,156) → (139,265) which is the area of the list with the 3 items.
After that, I’m lost. I don’t have enough LVGL knowledge to figure out why this invalidated area is not flushed to LCD as it is supposed to be.

Call stack:

lv_indev_read
  indev_pointer_proc
    indev_proc_release
      send_event(LV_EVENT_RELEASED
        lv_indev_send_event
          lv_obj_send_event
            event_send_core
              lv_obj_event_base
                lv_dropdown_list_event  LV_EVENT_RELEASED
                  lv_obj_event_base
                    lv_obj_event
                      lv_obj_remove_state
                        update_obj_state lv_dropdownlist_class  0x30 -> 0x10
                          cmp_res = LV_STYLE_STATE_CMP_DIFF_REDRAW
                          lv_obj_invalidate lv_dropdownlist_class   lv_obj.c:923
                            lv_obj_invalidate_area lv_dropdownlist_class {x1 = 10, y1 = 156, x2 = 139, y2 = 265}
                              lv_inv_area

It’s very easy to reproduce this issue, so I’m able to perform any other required test / change / debugging.

Thanks for your help.
Best regards,
Fabrice

remove this line and it should work properly…

    lv_indev_set_mode(ctxt.indev, LV_INDEV_MODE_EVENT);

You are also attempting to call the task handler from the flush function which is what has called the flush function in the first place. It’s not going to work.

The flush function needs to exit properly and not have any looping code in it and you do not want to make any changed to anything in LVGL from that function other than signaling that the flush has finished.

This function…

static void lvgl_on_flush_start(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map)
{
    debug(&ctxt.dbg, "LVGL: redraw (%"PRIi32",%"PRIi32") -> (%"PRIi32",%"PRIi32")",
        area->x1, area->y1,
        area->x2, area->y2);

    uint16_t w     = area->x2 - area->x1 + 1U;
    uint16_t h     = area->y2 - area->y1 + 1U;
    uint32_t count = (uint32_t)w * (uint32_t)h;

    lv_draw_sw_rgb565_swap(px_map, count);
    lcd_select(area->x1, area->y1, w, h);
    lcd_copy(px_map, count, 0U/*gap*/, true/*restart*/);
    while( !lcd_is_ready() )
    {
#ifndef PLATFORM_target
        test_core_yield();
#endif
        lcd_run();
    }
    lv_display_flush_ready(ctxt.disp);
}

should look like this…

static void lvgl_on_flush_start(lv_display_t* disp, const lv_area_t* area, uint8_t* px_map)
{
    debug(&ctxt.dbg, "LVGL: redraw (%"PRIi32",%"PRIi32") -> (%"PRIi32",%"PRIi32")",
        area->x1, area->y1,
        area->x2, area->y2);

    uint16_t w     = area->x2 - area->x1 + 1U;
    uint16_t h     = area->y2 - area->y1 + 1U;
    uint32_t count = (uint32_t)w * (uint32_t)h;

    lv_draw_sw_rgb565_swap(px_map, count);
    lcd_select(area->x1, area->y1, w, h);
    lcd_copy(px_map, count, 0U/*gap*/, true/*restart*/);

    lv_display_flush_ready(ctxt.disp);
}

There are some other issues with your code as well. Using 2 frame buffers and calling flush_ready from the flush callback is going to be a waste of RAM. the only time you should be using double buffering is if you allocate both frame buffers in DMA memory and the flush_ready call would be made from a callback function that is attached to the DMA memory transfer to the display for then the transfer has completed.

If you are going to use tasks or threads and you are wanting to make changes to the UI from different tasks/threads you will need to use semaphores to control access to any LVGL objects. This includes calling the task_handler. The only thing you can do from a different task\thread without using a semaphore is increment the tick in LVGL.

There should be some kind of code for the task_handler that is like this…

for (;;) {
    test_core_yield();
    lv_task_handler();
}

Hi all.

Thanks a lot for your replies & hints.
I did find my bug, and - as I was strongly suspecting it - it was in my code.

As I wrote, my firmware is bare-metal (no operating system, no task, no thread, …).
Every component of my firmware has a xxxx_run() function, and some simple code calls all xxxx_run() functions as often as possible.

My LCD component (which is different from my LVGL component) does follow such scheme.
It has a lcd_run() function, where it handles pending area selection & copy.
If it has no action to perform, then it immediately returns, giving chance for other components to run.

My firmware main code more or less looks like this (it is actually much more elegant):

void main(void)
{
    while( 1 ) {
        foo_run();
        bar_run();
        lvgl_run();
        lcd_run();
        touch_run();
    }
}

MCU is single core, so if foo_run() is being called, then for sure no other *_run() can be called at the same time.
There is no task, no thread, no context switch (except from MCU hardware IRQs).

In my LVGL component, my flush cb calls lcd_select() and lcd_copy(), which call low-level implementation of my hardware-specific LCD driver (here some code for most of Sitronix-like controllers):

static void lcd_sitronix_select(void* c, uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
    struct lcd_sitronix_ctxt_t* ctxt = (struct lcd_sitronix_ctxt_t*)c;
    ctxt->select.x      = x;
    ctxt->select.y      = y;
    ctxt->select.w      = w;
    ctxt->select.h      = h;
    ctxt->select.update = true;
}

static void lcd_sitronix_copy(void* c, const void* pixels, uint32_t count, uint16_t gap, bool restart)
{
    struct lcd_sitronix_ctxt_t* ctxt = (struct lcd_sitronix_ctxt_t*)c;
    ctxt->copy.next.pixels  = (const uint8_t*)pixels;
    ctxt->copy.next.count   = count;
    ctxt->copy.next.w       = ctxt->select.w;
    ctxt->copy.next.gap     = gap;
    ctxt->copy.next.restart = restart;
    ctxt->copy.update       = true;
}

This code only prepares some actions to be performed on the LCD hardware.
When these functions return, no pixel has been updated yet.
The LCD driver component (like all my other components) needs to have its lcd_run() function called.
From there, it will find that its select.update flag is active, and perform all required I/O with the hardware to do it, then clear the flag.
On next run, it will find that its copy.update flag is active, and again perform all required I/O for it and clear the flag.

My bug was simple:

I do must wait for my LCD driver to report end of all pending operations (select & copy) to be finished.
Then I can tell it to LVGL, calling lv_display_flush_ready().
I was calling my own function lcd_is_ready() but this function does not report readiness of pending operations… Ouch !
It reports end of internal LCD initialization procedure (setting pixel format, video gains, hardware rotation, …).

To know when my LCD driver has finished its pending operations (select and copy), correct call is an other function : lcd_is_done().
I was just badly using my own code…
Probably my brain was fooled by the names.

What was happening was actually simple:
1/ LVGL requests some area to be flushed
2/ my code prepares corresponding actions on my LCD driver
3/ my code was wrongly evaluating that those pending LCD actions were done
4/ my code was calling lv_display_flush_ready() too early
5/ next flush request was overwriting pending operation

The flush function needs to exit properly and not have any looping code in it and you do not want to make any changed to anything in LVGL from that function other than signaling that the flush has finished.

This is exactly the way I understand it.

You are also attempting to call the task handler from the flush function which is what has called the flush function in the first place. It’s not going to work.

I probably badly described my environment.
I’m sure it’s not the case and I would not do it.
My lcd_run() call only handles internal low-level hardware-specific LCD operations, and it could not call again any LVGL function.
lv_task_handler() is called only from my “LVGL” component which has its own lvgl_run() handler.
There is no possibility that lv_task_handler() could be called from the flush cb function.

Using 2 frame buffers and calling flush_ready from the flush callback is going to be a waste of RAM […]

That’s a very very good news. \o/
I’m deleting the 2nd useless buffer, saving a lot of RAM.
Thanks.

If you are going to use tasks or threads […]

I think I now do understand how it should operate.
Thanks again a lot for your hints.

Also, When investigating on my bug, I think I found 1 useless (but correct) flush request from LVGL.
That flush request is harmeless because the provided pixel map is correct, but it’s performed at some screen area which has no reason to have been invalidated.
I don’t want to waste anybody’s time, so I’m going to perform more tests.
I’ll create a separate post if needed.

Only for illustration, I post here a picture of the actual hardware.
I reintegrated all widgets I was playing with:

Best regards,
Fabrice

My bad on that. I just looked at the code again and I mixed up the function names. I mistook lcd_run for lvgl_run which is why I had thought you were trying to call lv_task_handler from the flush function.

To explain why you really don’t need this code…

    lv_indev_set_mode(ctxt.indev, LV_INDEV_MODE_EVENT);

LVGL has a built in timer system. This allows both LVGL and the user to perform tasks at specific intervals. This is the reason why the user needs to tell LVGL how many milliseconds has passed between calls to lv_tick_inc. A display will only be updated every 33 milliseconds and the polling of an input device happens at the same interval. By changing the indev mode to “EVENT” this disables the internal polling of the input device that is built into LVGL. It then becomes the users responsibility to handle the polling. Doing this will increase the code complexity for the user which can introduce issues. The EVENT mode is not supposed to be used if polling for input needs to be done. The EVENT mode was designed to be used for input devices that use hardware interrupts to signal input has occurred. Most input devices are still able to be polled even if they offer a hardware interrupt and using the event mode still doesn’t need to be used. On the majority of MCU’s memory allocation should not happen during a hardware interrupt so even when an interrupt takes place for an input device changes would need to be cached and then updated in LVGL from the main loop. This is because LVGL may allocate memory based on the input that has occurred.

If you do not like the 33 millisecond polling period for an input device you are able to change the timer period without needing to use the event mode. This can be done using the lv_indev_get_read_timer function and then using the lv_timer_set_period function to set the period to whatever internal time (in milliseconds) that you want. This allows the internal mechanics in LVGL to function as designed.