Skip to content

LVGL Animations run at Double Speed with task_handler module #549

@StephenDone

Description

@StephenDone

Hi @kdschlosser,

Describe the bug
LVGL screen load animations run at double speed.
For example a lv.SCREEN_LOAD_ANIM.MOVE_LEFT specified with a 3000ms period, completes in just over 1500ms.

Expected behavior
LVGL animations should take close to the specified period to complete.

Test script. Substitute code for your own display initialisation before running. Note the 3000ms animation time...

# Create a simple screen with a centred, coloured label
def create_screen(name, colour):
    screen = lv.obj()

    label = lv.label(screen)
    label.set_style_text_color(colour, 0)
    label.set_style_text_font(lv.font_montserrat_16, 0)
    label.set_text(name)
    label.center()

    return screen

# Time how long it takes to load the screen using the animation
load_ticks = time.ticks_ms()
def show_anim_time():
    global load_ticks

    now_ticks = time.ticks_ms()
    anim_ticks = time.ticks_diff( now_ticks, load_ticks )
    print(f'animation took {anim_ticks} ms')
    load_ticks = now_ticks

# Called when the current screen has loaded. Loads the next screen.
def screen_loaded_cb(e, next_screen):
    show_anim_time()
    lv.screen_load_anim(next_screen, lv.SCREEN_LOAD_ANIM.MOVE_LEFT, 3000, 0, False)

display = CreateKnobTouch_Display(color_space=lv.COLOR_FORMAT.RGB565)
display.set_backlight(100)
display.set_rotation(lv.DISPLAY_ROTATION._0)

screen1 = create_screen('Screen 1', lv.color_hex(0xFF0000))
screen2 = create_screen('Screen 2', lv.color_hex(0x00FF00))

# Add callbacks to the screens for the screen loaded event
screen1.add_event_cb(lambda e: screen_loaded_cb(e, screen2), lv.EVENT.SCREEN_LOADED, None)
screen2.add_event_cb(lambda e: screen_loaded_cb(e, screen1), lv.EVENT.SCREEN_LOADED, None)

# Load the first screen
lv.screen_load(screen1)

# Start handling lvgl events
task_handler = task_handler.TaskHandler()

Test script output with standard task_handler module...

C:\repos\ftms\device_root>mpremote mount -l . run C:\repos\ftms\sd_knobtouch\mpremote\remote_path.py repl
Local directory . is mounted at /remote
sys.path=['', '/patch', '/lib', '/remote', '/remote/patch', '/remote/lib', '.frozen']
Connected to MicroPython at COM7
Use Ctrl-] or Ctrl-x to exit this shell
>
MicroPython 78ff170de9-dirty on 2026-03-30; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import task_handler_test
Loading init module: _st77916_init_type3
Initializing Waveshare ESP32-S3 1.8 inch knob display - ST77916
animation took 1415 ms
>>> animation took 1590 ms
animation took 1502 ms
animation took 1542 ms
animation took 1543 ms
animation took 1503 ms
animation took 1542 ms
animation took 1543 ms
animation took 1503 ms
animation took 1542 ms
animation took 1543 ms
animation took 1503 ms
animation took 1542 ms

Cause of the problem
The TaskHandler class appears to be double counting time. I.e. Calling lv.tick_inc() with twice the total period during a given period of time...

On line 167 it is called with a number of ticks corresponding to the timer interval...

166    def _timer_cb(self, _):
167       lv.tick_inc(self.duration)  ##### <<<<<<
168        if self._running:
169            return

...the task handler is then scheduled to run. And within the task handler, lv.tick_inc(ticks_diff) is called twice more, but with a ticks_diff value.

Commenting out line 167 results in the following test script output...

C:\repos\ftms\device_root>mpremote mount -l . run C:\repos\ftms\sd_knobtouch\mpremote\remote_path.py repl
Local directory . is mounted at /remote
sys.path=['', '/patch', '/lib', '/remote', '/remote/patch', '/remote/lib', '.frozen']
Connected to MicroPython at COM7
Use Ctrl-] or Ctrl-x to exit this shell
>
MicroPython 78ff170de9-dirty on 2026-03-30; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
>>> import task_handler_test
Loading init module: _st77916_init_type3
Initializing Waveshare ESP32-S3 1.8 inch knob display - ST77916
animation took 1398 ms
>>> animation took 3060 ms
animation took 3020 ms
animation took 3025 ms
animation took 3031 ms
animation took 3037 ms
animation took 3006 ms
animation took 3030 ms
animation took 2999 ms
animation took 3004 ms
animation took 3009 ms
animation took 3009 ms
animation took 3009 ms
animation took 3009 ms
animation took 3009 ms

...just a guess. But was line 167 the original code and then the more complex ticks_diff code was added, but the original was never removed?

**Exact make and model number of the MCU that you are compiling for or the firmware is running on. **

ESP32-S3 R8

MicroPython 78ff170de9-dirty on 2026-03-30; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3

For ESP32 MCU's The PSRAM and FLASH SPI type, quad SPI or octal SPI.

  • PSRAM: octal
  • FLASH: quad

I need to know the OS and OS version of the machine that compiled the binary. If using a VM then I need to know the OS and OS version the VM is running. (WSL == VM)

  • OS: Windows 11 with WSL 2 Ubuntu.

Build Command

python3 make.py esp32 --flash-size=16 
BOARD=ESP32_GENERIC_S3 
BOARD_VARIANT=SPIRAM_OCT 
DISPLAY=st77916 
INDEV=cst816s 
CONFIG_SPIRAM_XIP_FROM_PSRAM=y 
CONFIG_FREERTOS_USE_TRACE_FACILITY=y 
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y 
CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions