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) exampleapplications/iot-solution/lvgl_example(this page uses itshello_lvglgetting-started example); the code can be found directly in the local SDK. Thelvgl_example/widgetsdirectory 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.
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_dispdisplay porting layer also supports other screens like ST7789 and ST7796S (drivers undercomponents/stage/lvgl/lv_device/); when switching screens, remember to sync the screen model and resolution config inlv_conf.h.
Open a terminal and enter the official hello_lvgl example project directory:
cd ~/Ai-Thinker-WB2/applications/iot-solution/lvgl_example/hello_lvgl
Note:
cdis the “change directory” command — entering the hello_lvgl project directory; all subsequentmakebuild andmake flashflash 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 awidgets/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,cdin, build and flash to keep learning.
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 in the project directory:
make -j8
Note:
makeis the “build” command, turning code into firmware (the program file) the board can run;-j8builds 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.
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 flashis the “flash” command, writing the compiled firmware into the board’s chip. Afterp=comes the serial device (change it to your computer’s actual one — check withls /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.
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, editLV_DISPLAY_ORIENTATION_LANDSCAPEinlv_conf.h(change toLV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTEDfor 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_tstruct pointer. Key fields:config.cb(timer callback, this tutorialtimer_cb),config.period(period in us, this tutorial1000= 1ms),config.reload_mode(periodic reload,TIMER_RELOAD_PERIODICmeans loop),port(timer number, this tutorial0)
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_tstruct pointer (the same one passed tohosal_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 useslv_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 oflv_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 alignalign: alignment mode, values:LV_ALIGN_CENTER(center),LV_ALIGN_TOP_LEFT(top-left), etc., this tutorial centersx_ofs: horizontal offset in pixels, values: any integer, this tutorial1y_ofs: vertical offset in pixels, values: any integer, this tutorial1
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 tutorial10/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
#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.

