Skip to content

Overview

LVGL is an open-source embedded graphics library (a GUI library; GUI = graphical user interface) that gives resource-constrained microcontrollers a "phone-style UI": ready-made widgets (elements on the interface — buttons, progress bars, input boxes, lists) you can use directly, without drawing each pixel yourself. This tutorial uses the hello_lvgl getting-started example from the official lvgl_example: it displays "Hello lvgl" text in the center of an SSD1306 OLED screen (128×64), running through LVGL's minimum system on the Ai-WB2.

In plain words: LVGL is like installing an "Android system" on your dev board — you don't need to worry about how each icon is drawn or how each interface is laid out; just say "I want a button in the middle saying OK" and it draws it for you. In this tutorial the "button" is called a widget, and how many times the picture refreshes per second is the frame rate (frame rate = refreshes per second). LVGL also takes some memory overhead (memory = the space inside the chip that temporarily stores data) to hold the frame buffer — that's the price it charges for "working for you".

This tutorial is based on the official Ai-Thinker SDK (Ai-Thinker-Open/Ai-Thinker-WB2, version release_bl_iot_sdk_1.6.40) example applications/iot-solution/lvgl_example (this page uses its hello_lvgl getting-started example); the code can be found directly in the local SDK. The lvgl_example/widgets directory also holds a dozen-plus widget examples (buttons, sliders, dropdown lists, e.g. lvgl_button, lvgl_slider, lvgl_switch) — once this page works, switch over to level up directly.

🎯Page GoalRun through LVGL's minimum system: display centered "Hello lvgl" text on the SSD1306 screen, and understand widgets, frame rate and memory overhead.
🧰Prerequisites① An Ai-WB2 development board, a 0.96-inch SSD1306 OLED module, dupont wires ② Development environment set up per [SDK Installation](../sdk/sdk_intro); it's recommended to finish [SSD1306 OLED Display](./ssd1306) first (same wiring).
🔗RelatedScreen driver fundamentals: [SSD1306 OLED Display](./ssd1306); previous: [TM1721 Driver Chip](./tm1721).

Hardware Wiring

LVGL itself is a pure software library — no wiring needed; it’s just a “drawing engine”. This example’s picture is shown on an SSD1306 OLED screen, wired exactly the same as the SSD1306 tutorial:

Ai-WB2 Pin SSD1306 Module
IO12 SCL (clock line)
IO3 SDA (data line)
3V3 VCC
GND GND

💡 If you’ve already wired it per the ssd1306 page, just reuse it here — no wire needs moving. The official lv_port_disp display porting layer also supports other screens like ST7789 and ST7796S (drivers under components/stage/lvgl/lv_device/); when switching screens, remember to sync the screen model and resolution config in lv_conf.h.

Enter the Example Project

Open a terminal and enter the official hello_lvgl example project directory:

cd ~/Ai-Thinker-WB2/applications/iot-solution/lvgl_example/hello_lvgl

Note: cd is the “change directory” command — entering the hello_lvgl project directory; all subsequent make build and make flash flash commands must run in this directory first.

Project structure:

File Purpose
hello_lvgl/main.c Main program source, the main file this tutorial looks at
hello_lvgl/bouffalo.mk Build config (brings in the LVGL component), usually no changes needed
lv_conf.h LVGL config header: screen model (LV_DISPLAY_SSD1306), I2C pins (OLED_IIC_SCL/SDA), orientation, resolution — all changed here

📌 Under lvgl_example/ there’s also a widgets/ directory with a dozen-plus widget examples — buttons (lvgl_button), sliders (lvgl_slider), switches (lvgl_switch), dropdown lists (lvgl_downList) — each an independent project; once this page works, cd in, build and flash to keep learning.

Write the Code

Open hello_lvgl/hello_lvgl/main.c — the complete code for this step has been moved to the end of this page:

📜 Full Code — in the “Full Code” section below, collapsed by default — click to expand, identical to the official example (applications/iot-solution/lvgl_example/hello_lvgl/hello_lvgl/main.c).

Code highlights:

Code Purpose
lv_init() Initializes the LVGL graphics library (allocates memory, builds the object system); without it all widget APIs fail
lv_port_disp_init() Connects LVGL’s “canvas” to the SSD1306 screen (display porting layer); without it the picture has nowhere to go
hosal_timer_init(&lv_timer_dev) Creates a 1ms periodic timer; its callback calls lv_tick_inc(1) to feed LVGL’s “heartbeat” — without it time doesn’t advance and animations don’t run
hosal_timer_start(&lv_timer_dev) Starts the timer; without it LVGL’s heartbeat stays still
lv_label_create(lv_scr_act()) Creates a text widget on the current screen (lv_scr_act() = the current active screen); a widget = an element on the interface like text/buttons
lv_label_set_text(label1, "Hello lvgl") Sets the text content; without it the label stays blank
lv_obj_align(label1, LV_ALIGN_CENTER, 1, 1) Centers the text (1-pixel offset); without it the text appears in the screen’s top-left corner
lv_timer_handler() LVGL’s “engine”: only by calling it in a loop does the interface refresh and animations run; the frame rate (refreshes per second) is decided by it
vTaskDelay(10/portTICK_PERIOD_MS) Yields the CPU every 10ms before refreshing; without it LVGL hogs the CPU and the system freezes
Build the Project

Build in the project directory:

make -j8

Note: make is the “build” command, turning code into firmware (the program file) the board can run; -j8 builds with 8 parallel CPU cores, faster. The LVGL library is large — the first build takes a while, be patient.

On success a firmware build_out/hello_lvgl.bin is generated.

Flash the Firmware

Keep the board connected via USB, confirm the serial device (usually /dev/ttyUSB0 on Linux), and flash:

make flash p=/dev/ttyUSB0 b=921600

Note: make flash is the “flash” command, writing the compiled firmware into the board’s chip. After p= comes the serial device (change it to your computer’s actual one — check with ls /dev/ttyUSB*), b= is the flash baud rate (transfer speed).

⏳ During flashing, press and hold the EN button on the board when prompted to enter download mode (some boards enter automatically); wait for the progress bar to complete — that means the flash succeeded.

Run and Verify

After flashing, the board automatically restarts and runs; watch the OLED screen:

The center of the screen should show Hello lvgl (if you flashed the ssd1306 page’s firmware before, the screen switches from “date and time” to “Hello lvgl” — LVGL has taken over the screen).

💡 Level up: enter the other widget projects under lvgl_example/widgets/ (e.g. lvgl_button, lvgl_slider) and follow the same build/flash flow to experience interactive widgets like buttons and sliders. To change the display orientation, edit LV_DISPLAY_ORIENTATION_LANDSCAPE in lv_conf.h (change to LV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTED for mirrored display).

Seeing Hello lvgl in the center of the screen means success; if the screen doesn’t light, shows garbage, or the build fails, it hasn’t succeeded yet — check the FAQ at the end.


API Summary for This Tutorial

lv_init()

Initializes the LVGL graphics library: builds the object system and allocates internal memory (LVGL needs a block of memory for objects and the frame buffer — the source of the memory overhead). Must be called before using any LVGL widget API.

Parameters: none

Return: none

lv_port_disp_init()

Initializes the display porting layer, connecting LVGL's "canvas" to the actual screen (SDK porting-layer interface, source in components/stage/lvgl/lv_device/lv_port_disp.c). This example connects an SSD1306 (128×64), allocating the internal display buffer and binding the refresh callback to the screen driver.

Parameters: none

Return: none

hosal_timer_init(timer)

Initializes a hardware timer (hosal timer API; hosal = the SDK's unified hardware abstraction layer). This tutorial uses it to call lv_tick_inc(1) in the timer callback, feeding LVGL a 1ms-per-tick "heartbeat" (LVGL uses it to count time and drive animations).

Parameters:

  • timer: hosal_timer_dev_t struct pointer. Key fields: config.cb (timer callback, this tutorial timer_cb), config.period (period in us, this tutorial 1000 = 1ms), config.reload_mode (periodic reload, TIMER_RELOAD_PERIODIC means loop), port (timer number, this tutorial 0)

Return: 0 on success; negative error code on failure

hosal_timer_start(timer)

Starts the initialized timer, triggering the callback periodically.

Parameters:

  • timer: hosal_timer_dev_t struct pointer (the same one passed to hosal_timer_init)

Return: 0 on success; negative error code on failure

lv_label_create(parent)

Creates a text widget (Label, a text element on the interface) and attaches it to the given parent object.

Parameters:

  • parent: parent object pointer, values: any widget object, this tutorial uses lv_scr_act() (the current active screen, placing the text directly on the screen)

Return: the new text widget object pointer (lv_obj_t*); NULL on failure

lv_label_set_text(obj, text)

Sets the content the text widget displays.

Parameters:

  • obj: text widget object pointer (the return value of lv_label_create)
  • text: the string to display, this tutorial "Hello lvgl"

Return: none

lv_obj_align(obj, align, x_ofs, y_ofs)

Aligns a widget to its parent in the given way (LVGL general widget-align API, works on all widgets).

Parameters:

  • obj: the widget object pointer to align
  • align: alignment mode, values: LV_ALIGN_CENTER (center), LV_ALIGN_TOP_LEFT (top-left), etc., this tutorial centers
  • x_ofs: horizontal offset in pixels, values: any integer, this tutorial 1
  • y_ofs: vertical offset in pixels, values: any integer, this tutorial 1

Return: none

lv_timer_handler()

LVGL's "engine": processes all regions pending refresh, runs animations and timer tasks. It must be called repeatedly in the main loop, or the interface won't refresh (the frame rate — refreshes per second — is decided by how often this function is called).

Parameters: none

Return: milliseconds until it needs to be called again (uint32_t), usually ignored

vTaskDelay(ms)

Suspends the current task for the given milliseconds, yielding the CPU to other tasks (FreeRTOS system API).

Parameters:

  • ms: delay in milliseconds, values: any non-negative integer (this tutorial 10/portTICK_PERIOD_MS = 10ms — yield the CPU first, then refresh, to avoid hogging the system)

Return: none


Full Code

Below is the complete hello_lvgl/hello_lvgl/main.c source, identical to the official example (applications/iot-solution/lvgl_example/hello_lvgl/hello_lvgl/main.c):

📜 Click to expand the full hello_lvgl/hello_lvgl/main.c code
c

#include <stdio.h>
#include <string.h>
#include <FreeRTOS.h>
#include <task.h>
#include <blog.h>
#include "bl_sys.h"
#include "lvgl.h"
#include "lv_conf.h"
#include "lv_port_disp.h"
#include <hosal_timer.h>


static void timer_cb(void* arg)
{
    lv_tick_inc(1);
}

void main(void)
{

    static hosal_timer_dev_t lv_timer_dev = {
        .config = {
            .arg = NULL,
            .cb = timer_cb,
            .period = 1000,
            .reload_mode = TIMER_RELOAD_PERIODIC,
        },
        .port = 0,
    };
    lv_init();

    lv_port_disp_init();

    hosal_timer_init(&lv_timer_dev);
    hosal_timer_start(&lv_timer_dev);

    lv_obj_t* label1 = lv_label_create(lv_scr_act());

    lv_label_set_text(label1, "Hello lvgl");
    lv_obj_align(label1, LV_ALIGN_CENTER, 1, 1);

    while (1) {
        vTaskDelay(10/portTICK_PERIOD_MS);
        lv_timer_handler();
    }
}

FAQ & Troubleshooting

⚠️ Build fails with a pile of undefined reference to 'lv_xxx'
Cause: the LVGL component wasn't compiled into the project (component inclusion config issue)
Fix: confirm you run make -j8 at the project root ~/Ai-Thinker-WB2/applications/iot-solution/lvgl_example/hello_lvgl, and hello_lvgl/bouffalo.mk is untouched; the LVGL library is large — wait patiently for the build to finish

⚠️ The screen doesn't light (blank)
Cause: wiring error or wrong I2C address (LVGL shares the same screen and address 0x3C as the ssd1306 page)
Fix: check per the ssd1306 page: IO12=SCL, IO3=SDA, VCC to 3V3, GND common ground; confirm the module's address is 0x3C (the OLED config in lv_conf.h)

⚠️ Screen displays but in the wrong orientation (upside-down/mirrored)
Cause: the display orientation macro in lv_conf.h doesn't match the screen's actual orientation
Fix: swap LV_DISPLAY_ORIENTATION_LANDSCAPE in lv_conf.h for LV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTED, rebuild and reflash

⚠️ The picture stutters, refresh is slow
Cause: I2C (400KHz) transmitting a 128×64 frame is inherently slow, or LVGL's memory buffer is insufficient
Fix: this example's frame rate being limited by I2C speed is normal; for smooth animation switch to an SPI screen (lv_port_disp supports ST7789/ST7796S); don't shrink LV_MEM_SIZE in lv_conf.h (insufficient memory overhead makes widget creation fail)

⚠️ Flashing reports cannot open the serial port / keeps waiting
Cause: wrong serial device, insufficient permission, download mode not entered, or the cable only charges
Fix: confirm the device with ls /dev/ttyUSB*; if permission denied run sudo usermod -aG dialout $USER; press and hold EN during flashing as prompted; try a Type-C data-capable cable

Self-Check

The SSD1306 screen shows Hello lvgl text in the center — LVGL's minimum system is verified; then enter lvgl_example/widgets/ to build and run other widget examples to confirm interactive widgets work.

Released under the MIT License. Build Time 2026-09-11 14:52:23