If you are an embedded developer who has touched displays at least once, you have probably heard about LVGL. And if you have not, chances are you will very soon. LVGL quietly sits behind a lot of modern looking embedded UIs, smartwatches, HMI panels, appliances, medical devices, and even hobby projects.
In this article, I will walk you through:
- What LVGL actually is
- How you can learn it without owning any hardware
- Where to find the right documentation and references
- A Module 1 style introduction with simple example codes
What is LVGL?
LVGL (Light and Versatile Graphics Library) is an open-source graphics library written in C. It is designed specifically for resource‑constrained systems microcontrollers with limited RAM, flash, and CPU power.
At a high level, LVGL gives you:
- A widget system (buttons, labels, bars, sliders, LEDs, charts, etc.)
- A flexible layout engine (flexbox and grid)
- Themes, styles, and animations
- Input device handling (touch, buttons, encoders)
- Display abstraction (LVGL doesn’t care which LCD you use)
The important thing to understand is this:
LVGL does not draw pixels directly to your screen.
It sits in the middle and talks to a display driver that you provide.
That separation is exactly why learning LVGL without hardware is totally possible.
How to Learn LVGL Without Any Hardware
A common misconception is:
“I need an STM32 / ESP32 / LCD panel before I can start LVGL.”
You don’t.
The Simulator Approach
LVGL officially supports PC simulators:
- SDL-based simulator (Windows / Linux / macOS)
- Visual Studio / VS Code based setups
In a simulator:
- Your PC screen acts as the display
- Mouse acts as touch input
- You write the same LVGL code you wouldd write on a microcontroller
This means:
- Faster iteration
- Easy debugging
- Zero hardware cost
Once you are confident, you can port the same logic to STM32, ESP32, NXP, etc.
What You Actually Need
To learn LVGL without hardware:
- LVGL source code
- A PC simulator project (SDL)
- Basic C knowledge
No display drivers, no SPI, no FSMC, no headaches at least initially 🙂
References and Documentation
Before jumping into code, bookmark these:
- Official LVGL Documentation
https://docs.lvgl.io/ - LVGL GitHub Repository
https://github.com/lvgl/lvgl - LVGL Examples
https://docs.lvgl.io/latest/en/html/examples.html - LVGL Widgets Catalog
https://docs.lvgl.io/latest/en/html/widgets/index.html
If you read just one thing carefully, read the Object model and Style system sections. Everything else builds on top of that.
MODULE 1 -> Basic Examples
Module 1 is about getting comfortable with:
- Screens
- Objects
- Parent – child relationships
- Basic widgets
No layouts, no events, no animations yet.
Example 1: Hello World
The embedded equivalent of printing Hello World.
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void hello_world(void)
{
lv_obj_t* screen = lv_screen_active();
lv_obj_t* label = lv_label_create(screen);
lv_label_set_text(label, "Hello World! ");
}
What is Happening Here?
lv_screen_active()returns the currently active screenlv_label_create(screen)creates a label object- The label’s parent is the screen
If you don’t set position explicitly, LVGL places it at (0, 0) by default.
Example 2: Loading a New Screen
Screens in LVGL are just objects but they live at the top level.
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void loading_screens(void)
{
lv_obj_t* screen = lv_screen_active();
lv_obj_t* obj = lv_obj_create(screen);
lv_obj_set_width(obj, 100);
lv_obj_set_height(obj, 100);
lv_obj_set_x(obj, 100);
lv_obj_set_y(obj, 100);
lv_obj_t* screen2 = lv_obj_create(NULL);
lv_obj_t* label = lv_label_create(screen2);
lv_label_set_text(label, "Hi!");
lv_obj_set_y(label, 50);
lv_screen_load(screen2);
}
Key Takeaways
- Creating an object with
NULLparent means it becomes a screen - Child coordinates are always relative to their parent
lv_screen_load()switches the active screen
This parent <–> child coordinate system is core to LVGL.
Example 3: Basic Widget (Parent <–> Child Relationship)
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void basic_widget(void)
{
lv_obj_t* screen = lv_screen_active();
lv_obj_t* obj = lv_obj_create(screen);
lv_obj_set_width(obj, 100);
lv_obj_set_height(obj, 100);
lv_obj_set_x(obj, 100);
lv_obj_set_y(obj, 100);
lv_obj_t* label = lv_label_create(obj);
lv_label_set_text(label, "Hi!");
lv_obj_set_y(label, 50);
}
Here the label is inside the object, not directly on the screen.
If you move the parent object, the label moves with it just like a real UI container.
Example 4: Drawing a Basic Line
LVGL is not limited to widgets you can draw primitives too.
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void basic_line(void)
{
static lv_point_precise_t points[] = { {100,0}, {0,100}, {200,100}, {100,0} };
static lv_style_t line_style;
lv_style_init(&line_style);
lv_style_set_line_width(&line_style, 12);
lv_style_set_line_color(&line_style, lv_palette_main(LV_PALETTE_INDIGO));
lv_style_set_line_rounded(&line_style, true);
lv_obj_t* line = lv_line_create(lv_screen_active());
lv_line_set_points(line, points, 4);
lv_obj_add_style(line, &line_style, 0);
lv_obj_center(line);
}
This example introduces styles, which are heavily used in LVGL.
Once you understand styles, customizing UI becomes much easier.
Example 5: Basic LED Widget
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void basic_led(void)
{
lv_obj_t* led = lv_led_create(lv_screen_active());
lv_led_off(led);
lv_obj_t* led2 = lv_led_create(lv_screen_active());
lv_led_set_color(led2, lv_palette_main(LV_PALETTE_PURPLE));
lv_obj_set_y(led2, 40);
lv_obj_t* led3 = lv_led_create(lv_screen_active());
lv_led_set_color(led3, lv_palette_main(LV_PALETTE_PURPLE));
lv_led_set_brightness(led3, 200);
lv_obj_set_y(led3, 80);
}
This shows how the same widget can look completely different using color and brightness alone.
Example 6: Bar Widget
#include "BasicWidget.h"
#include "lvgl/lvgl.h"
void basic_bar(void)
{
lv_obj_t* bar = lv_bar_create(lv_screen_active());
lv_bar_set_value(bar, 50, LV_ANIM_ON);
}
Bars are commonly used for:
- Progress indicators
- Battery levels
- Sensor values
Animations come almost for free.
DO not forgot this
Edit this before running the code…
#include <Windows.h>
#include <LvglWindowsIconResource.h>
#include "lvgl/lvgl.h"
#include "lvgl/examples/lv_examples.h"
#include "lvgl/demos/lv_demos.h"
#include "BasicWidget.h"
#include "BasicEvent.h"
#include "BasicLayout.h"
int main()
{
lv_init();
/*
* Optional workaround for users who wants UTF-8 console output.
* If you don't want that behavior can comment them out.
*
* Suggested by jinsc123654.
*/
#if LV_TXT_ENC == LV_TXT_ENC_UTF8
SetConsoleCP(CP_UTF8);
SetConsoleOutputCP(CP_UTF8);
#endif
int32_t zoom_level = 100;
bool allow_dpi_override = false;
bool simulator_mode = true;
lv_display_t* display = lv_windows_create_display(
L"LVGL Windows Simulator Display 1",
800,
480,
zoom_level,
allow_dpi_override,
simulator_mode);
if (!display)
{
return -1;
}
HWND window_handle = lv_windows_get_display_window_handle(display);
if (!window_handle)
{
return -1;
}
HICON icon_handle = LoadIconW(
GetModuleHandleW(NULL),
MAKEINTRESOURCE(IDI_LVGL_WINDOWS));
if (icon_handle)
{
SendMessageW(
window_handle,
WM_SETICON,
TRUE,
(LPARAM)icon_handle);
SendMessageW(
window_handle,
WM_SETICON,
FALSE,
(LPARAM)icon_handle);
}
lv_indev_t* pointer_indev = lv_windows_acquire_pointer_indev(display);
if (!pointer_indev)
{
return -1;
}
lv_indev_t* keypad_indev = lv_windows_acquire_keypad_indev(display);
if (!keypad_indev)
{
return -1;
}
lv_indev_t* encoder_indev = lv_windows_acquire_encoder_indev(display);
if (!encoder_indev)
{
return -1;
}
//lv_demo_widgets();
//lv_demo_benchmark();
//basic_widget();
//hello_world();
//loading_screens();
//basic_label();
//basic_led();
//basic_line();
//basic_bar();
while (1)
{
uint32_t time_till_next = lv_timer_handler();
lv_delay_ms(time_till_next);
}
return 0;
}
//basic_widget();
//hello_world();
//loading_screens();
//basic_label();
//basic_led();
//basic_line();
//basic_bar()Use these to run the mentioned examples…
Final Learnings with this much experimentation
LVGL looks simple at first and that is a good thing.
The real skills comes when you combine:
- Styles
- Layouts (flex & grid)
- Events
- Animations
Learning it on a simulator lets you focus on UI logic, not hardware bring-up.
Once you are comfortable, moving to STM32, ESP32, or Linux framebuffer becomes a mechanical task.
In the next module, the natural progression would be:
- Styles in depth
- Flex layout
- Grid layout
- Event handling
LVGL rewards structured thinking very similar to how good embedded firmware is written.
THANK YOU