Getting Started with LVGL (Without Hardware) 01

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:

  1. Official LVGL Documentation
    https://docs.lvgl.io/
  2. LVGL GitHub Repository
    https://github.com/lvgl/lvgl
  3. LVGL Examples
    https://docs.lvgl.io/latest/en/html/examples.html
  4. 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 screen
  • lv_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 NULL parent 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

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *