Taking control of LVGL updating the display

In Micropython if the ili9XXX display driver base class is used it will automatically start the event loop using the default parameters. One of those parameters isfreq which is a factory that gets used to determine the timer callback duration. The default value is 25 and the expression that gets used is 1000 / freq which comes out to 40. so the event loop only gets run every 40 milliseconds.

The event loop can be shut down and a new one started by doing the following. This only applies if asynchronous is set to False for the driver.

import lv_utils

# assume display driver is already loaded
driver.event_loop.deinit()
driver.event_loop = lv_utils.event_loop(freq=100)

This will update every 10 milliseconds. I would not have it go any faster than that because of how long the display update takes and also because of the sheer number of ISR’s taking place. The event loop is not the most efficient because of the timer and not being able to allocate memory in an ISR. So the event loop schedules a call to another function when it is convenient for MicroPython to do so. That could be right away or it could be a while before it happens.

If you want to take control of when the display gets updated you are able to pretty easily. I wrote this for use with an ESP32

import lvgl as lv
import sys
import _thread
import time


##############################################################################
def default_exception_sink(e):
    sys.print_exception(e)


class event_loop(object):

    _current_instance = None

    def __init__(self, *args, **kwargs):
        if not lv.is_initialized():
            lv.init()

        if event_loop._current_instance is not None:
            self.__dict__.update(event_loop._current_instance.__dict__)
            if not self.is_running():
                self._start()

            return

        self.delay = 0
        self._exit_event = False
        self.timer_lock = _thread.allocate_lock()
        event_loop._current_instance = self
        self._start()

    def _start(self):
        _thread.start_new_thread(self.loop, ())

    def deinit(self):
        self._exit_event = True
        if self.timer_lock.locked():
            self.timer_lock.release()

    def update(self):
        if self.timer_lock.locked():
            self.timer_lock.release()

    @staticmethod
    def is_running():
        self = event_loop._current_instance
        if self is None:
            return False

        return self._exit_event is False

    def loop(self):
        self.timer_lock.acquire()
        self._exit_event = False
        self.delay = time.ticks_ms()

        while not self._exit_event:
            self.timer_lock.acquire()
            curr_time = time.ticks_ms()
            try:
                lv.tick_inc(time.ticks_diff(curr_time, self.delay))
                lv.task_handler()
            except Exception as e:
                default_exception_sink(e)
                self.deinit()

            self.delay = curr_time


sys.modules['lv_utils'] = sys.modules[__name__]

Place that code into a python source file of it’s own. Import that module before you import the driver. This will override the event_loop in lv_utils.

Again this is for the ESP32. This uses threading to handle the loop. what I did was I made a function that could be imported from __main__ that I could call from anywhere in the UI when I wanted to update the display.

def update_display():
    driver.event_loop.update()

Now since this is run in its own thread you could call update in the main program loop.

By changing the event loop to the one above I am able to achieve smooth UI updates for progressive changes. Like the ramping up of the speed of a motor and using LVGL to display that speed. The widget no longer makes large jumps as seen on the display.

If you start the event loop before initializing the driver, you will not need to deinit-reinit it and it will also work for async=True.

LVGL is not thread safe, so this is dangerous.
lv.task_handler() must not be called while some other LVGL function is running.

I have not had any issues with the threads at all. Everything is working fine.

The other thing is on the ESP32 you don’t know which core of the processor the ISR is going to get triggered on so there is a risk using the original design.

The fact that you didn’t experience issues does not prove there is no problem.
It’s definitely wrong to use multi-threading without proper locking. At some point you will experience instability and crashes.

This is not true.
On Micropython we guarantee that everything is pinned to the same core, including ISRs.
See: esp32: Pin MicroPython tasks to a specific core. · micropython/micropython@995f9cf · GitHub

So in general, when using lv_micropython everything by default is running on the same thread and no locking is needed.

1 Like