Overview
LVGL is available on GitHub: https://github.com/lvgl/lvgl.
Getting LVGL
Clone or Download
If you would like to integrate LVGL yourself you can get it from GitHub: https://github.com/lvgl/lvgl.
You can clone it or Download the latest version as a ZIP.
If you download LVGL yourself you need to save it into your project folder so that it can be compiled with the rest of your source code.
Frameworks and Package Registries
LVGL is also available in various frameworks and registries where most of the integration work is already done:
- Arduino library
- PlatformIO package
- Zephyr library
- ESP-IDF (ESP32) component
- NXP MCUXpresso component
- NuttX library
- RT-Thread RTOS
- CMSIS-Pack
- RIOT OS package
Folder Structure
The graphics library itself is the lvgl directory. It contains several
directories, but to use LVGL, you only need the .c and .h files under
the src directory, plus lvgl/lvgl.h, and lvgl/lv_version.h.
The lvgl directory also contains an examples and a demos
directory. If your project needs examples and/or demos, add these
directories to your project and enable them in lv_conf.h or Kconfig
(LV_BUILD_DEMOS and LV_BUILD_EXAMPLES).
Public API
LVGL's public API is located under include/lvgl.
The canonical way to integrate LVGL is to add an include path pointing at
lvgl/include. You can then include the full public API with:
#include <lvgl/lvgl.h>If setting up lvgl/include as an include path isn't an option for your
project, LVGL's root directory also contains a header file that
includes the full public API, so #include <lvgl/lvgl.h> works there
too. Both paths are fully supported.
Every function of the public API validates its arguments at runtime. If a call
passes an invalid argument, LVGL logs a warning and the function returns without
doing anything instead of running into undefined behavior. This is enabled by
default (LV_USE_CHECK_ARG) and its behaviour can be configured.
See Argument Checking.
Configuration
LVGL has many compile-time settings: color depth, which Widgets are built, GPU and rendering backends, third-party libraries, OS support, and more. They can be set in three ways that can be mixed:
lv_conf.h: a header where#defines are adjusted (the default).- Kconfig: when LVGL is part of a Kconfig-based build, or built standalone with CMake.
- Compiler defines: values passed on the command line (e.g.
-DLV_COLOR_FORMAT_DEFAULT=LV_COLOR_FORMAT_XRGB8888).
When setting up a project for the first time, copy lvgl/lv_conf_template.h to
lv_conf.h next to the lvgl folder, change the first #if 0 to 1 to enable
the file's content, and set LV_COLOR_FORMAT_DEFAULT to match
your display panel. Adjust the other options as needed, every option is documented
by a comment in the file.
For the full details see Configuring LVGL.
Connecting to Hardware
Several frameworks integrate LVGL deeply, and all the drivers are already in place. This is the case in Zephyr, ESP-IDF, NuttX, RT-Thread, NXP, Renesas FSP, etc.
LVGL also comes with many built-in display and input device drivers for on-chip LCD peripheries, Embedded Linux, external display controllers, and many more. These drivers do the heavy lifting for the drivers and also serve as references for custom drivers.
If the existing support in frameworks or the drivers is not enough, setting up everything from scratch is also simple. The process can be read in the following.
Initialization
- Include
lvgl/lvgl.h - Initialize your hardware (clock, peripherals, etc.)
- Call
lv_init()to initialize LVGL
Tick Interface
Set the tick for LVGL by calling lv_tick_inc(x) in a timer interrupt every
x milliseconds, or set a callback that returns the milliseconds elapsed
since startup with lv_tick_set_cb(my_cb). Many platforms have built-in
functions that can be used as they are. For example:
- SDL:
lv_tick_set_cb(SDL_GetTicks); - Arduino:
lv_tick_set_cb(my_tick_get_cb);, wheremy_tick_get_cbis:static uint32_t my_tick_get_cb(void) { return millis(); } - FreeRTOS:
lv_tick_set_cb(xTaskGetTickCount); - STM32:
lv_tick_set_cb(HAL_GetTick); - ESP32:
lv_tick_set_cb(my_tick_get_cb);, wheremy_tick_get_cbis a wrapper foresp_timer_get_time() / 1000;
Displays and Input Devices
Create a Display (lv_display), set the buffers, and the flush callback.
In practice, this means implementing a single function that can show the rendered
image on the screen. It is called a flush callback. To learn more about
buffering options, see implementation examples and learn more about all the
features in Display (lv_display).
Add Input devices if needed (touchpad, external buttons, keyboard, etc.) by
creating lv_indevs. To do so, a single read callback needs to be implemented
which returns the state of the given input device. Read more about the input device
types, their features, and check the examples at Input devices (lv_indev).
Timer Handler
All the main tasks of LVGL are implemented as software timers handled by LVGL. There are timers for:
- rendering
- input device reading
- animation updates
- timers occasionally used by widgets
- user-created timers
- etc.
See the Timer (lv_timer) section to learn more about timers.
To process the timers of LVGL, you need to call lv_timer_handler
periodically in one of the following ways:
- in the
while(1)loop of themain()function, or - in an OS task periodically. (See Operating Systems and Threads.)
In the simplest case, it can be done like this:
while(1) {
lv_timer_handler(); /*Might return immediately or execute some timers*/
lv_sleep_ms(5);
}If LV_USE_OS is set, lv_sleep_ms will be the sleep function
provided by the operating system, otherwise it will fall back to a blocking delay.
Of course, you can use any custom delay, wait, or sleep functions instead.
Sleep Management
To better control the delay/sleep time, lv_timer_handler
returns the remaining time until the next timer:
while(1) {
uint32_t time_till_next = lv_timer_handler();
/*If there is nothing to do now, check again a little bit later.*/
if(time_till_next == LV_NO_TIMER_READY) {
time_till_next = LV_DEF_REFR_PERIOD; /*33 ms by default in lv_conf.h*/
}
lv_sleep_ms(time_till_next); /*Sleep the thread*/
}lv_timer_handler will return LV_NO_TIMER_READY (UINT32_MAX)
if there are no running timers. This can happen if there is nothing to redraw, there are
no indevs or they are disabled with lv_indev_enable, there are no running
animations, and no user-created timers.
When LV_NO_TIMER_READY is returned, special handling is needed to make
LVGL run again later:
- don’t sleep forever, just for a shorter time to check again a little bit later, or
- wait for an event that you will trigger when LVGL needs to run again, or
- sleep the CPU (not just the thread)
Also check the Operating System Support section of the documentation to learn more about the considerations when using LVGL in an operating system.
Sleep Management
The MCU can go to sleep when no user input has been received for a certain period.
In this case, the main while(1) could look like this:
while(1) {
/* Normal operation (no sleep) if < 5 sec inactivity */
if(lv_display_get_inactive_time(lv_display_get_default()) < 5000) {
lv_timer_handler();
}
/* Sleep after 5 sec inactivity */
else {
my_device_sleep(); /* Sleep the device, execution stops here */
}
lv_sleep_ms(5);
}In addition to lv_display_get_inactive_time, you can check
lv_anim_count_running to see if all animations have finished.
Operating Systems and Threads
LVGL is not thread-safe.
That is, while LVGL is executing a function, you cannot call another
LVGL function from another thread. This includes calls
to lv_timer_handler.
Typically, this means for example, if you want to set a label's text using
lv_label_set_text in one thread while in another thread
lv_timer_handler is running, the same widget
can be accessed concurrently, causing issues. The same applies to creating
and deleting widgets in different threads.
Solution
The solution is simple: before calling LVGL functions, take a mutex and release the mutex after the functions.
LVGL has some helper functions to make it even simpler. If LV_USE_OS
is set to something other than LV_OS_NONE in lv_conf.h, you can use
lv_lock and lv_unlock. lv_timer_handler
calls these internally. Here is an example:
void main_ui_thread(void)
{
while(1) {
lv_timer_handler(); /* lv_lock/lv_unlock is called internally */
lv_sleep_ms(5);
}
}
void other_thread(void)
{
lv_lock();
lv_obj_t * label = lv_label_create(lv_screen_active());
lv_unlock();
int cnt = 0;
while(1) {
lv_lock();
lv_label_set_text_fmt(label, "%d", cnt++);
/*Call more functions if needed*/
lv_unlock();
lv_sleep_ms(2000);
}
}Exceptions
There are some exceptions when no locking/protecting is needed.
event callbacks, timer callbacks,
animation callbacks, and callbacks passed to LVGL functions
in general are called sequentially from lv_timer_handler, therefore
no special consideration is required as they are already protected in
lv_timer_handler.
Also, lv_tick_inc and lv_display_flush_ready are implemented
in a special way; therefore, they can be called from any thread without issues.
Compiling LVGL
In general, it's trivial to build LVGL: just compile it as you would compile the other source files of your project.
LVGL has built-in support for
Operating System Support
LVGL is not thread-safe.
That means it is the programmer's responsibility to see that no LVGL function is
called while another LVGL call is in progress in another thread. This includes calls
to lv_timer_handler.
Assuming the above is the case, it is safe to call LVGL functions in
- event callbacks, and in
- timer callbacks
because the thread that drives both of these is the thread that calls
lv_timer_handler.
Reason:
LVGL manages many complex data structures, and those structures are "system
resources" that must be protected from being "seen" by other threads in an
inconsistent state. A high percentage LVGL functions (functions that start with
lv_) either read from or change those data structures. Those that change them
place the data in an inconsistent state during call execution (because such changes are
multi-step sequences), but return them to a consistent state before those functions
return. For this reason, execution of each LVGL function must be allowed to complete
before any other LVGL function is started.
Exceptions to the Above:
These two LVGL functions may be called from any thread:
lv_tick_inc(if writing to auint32_tis atomic on your platform; see Tick Interface for more information) andlv_display_flush_ready(Flush Callback for more information)
Use of MUTEXes requires:
- acquiring the MUTEX (locking it) before each LVGL call (or group of calls), and
- releasing the MUTEX (unlocking it) afterwards.
If your OS is integrated with LVGL (the macro LV_USE_OS has a value
other than LV_OS_NONE in lv_conf.h) you can use lv_lock and
lv_unlock to perform #1 and #2.
When this is the case, lv_timer_handler calls lv_lock
and lv_unlock internally, so you do not have to bracket your
calls to lv_timer_handler with them.
If your OS is NOT integrated with LVGL, then these calls either return immediately with no effect, or are optimized away by the linker.
This pseudocode illustrates the concept of using a MUTEX:
void lvgl_thread(void)
{
while(1) {
uint32_t time_till_next;
time_till_next = lv_timer_handler(); /* lv_lock/lv_unlock is called internally */
if(time_till_next == LV_NO_TIMER_READY) time_till_next = LV_DEF_REFR_PERIOD; /*try again soon because the other thread can make the timer ready*/
thread_sleep(time_till_next); /* sleep for a while */
}
}
void other_thread(void)
{
/* You must always hold (lock) the MUTEX while calling LVGL functions. */
lv_lock();
lv_obj_t *img = lv_image_create(lv_screen_active());
lv_unlock();
while(1) {
lv_lock();
/* Change to next image. */
lv_image_set_src(img, next_image);
lv_unlock();
thread_sleep(2000);
}
}Multiple Instances
It is possible to run multiple, independent instances of LVGL in the same firmware.
To enable its multi-instance feature, set LV_GLOBAL_CUSTOM in lv_conf.h
and provide a custom function to lv_global_default using __thread or
pthread_key_t. This allows running multiple LVGL instances by storing LVGL's
global variables in TLS (Thread-Local Storage).
For example:
lv_global_t * lv_global_default(void)
{
static __thread lv_global_t lv_global;
return &lv_global;
}Last updated on