概述
LVGL 是一款开源的嵌入式图形库(GUI 库,GUI = 图形用户界面),专门给资源有限的单片机做「手机 UI」:按钮、进度条、输入框、列表等现成控件(widget,界面上的按钮/进度条/输入框等元素)拿来就能用,不用自己一个个像素去画。本教程使用官方 lvgl_example 里的 hello_lvgl 入门示例:在 SSD1306 OLED 屏幕(128×64)中央显示 「Hello lvgl」 文本,跑通 LVGL 在 Ai-WB2 上的最小系统。
用大白话讲:LVGL 就像给开发板装了个「安卓系统」——你不需要关心每个图标怎么画、每个界面怎么布局,只要说「我要一个按钮,放在中间,文字写 OK」,它就能帮你画出来。本教程的「按钮」叫控件(widget),画面每秒刷新多少次叫帧率(帧率 = 每秒刷新画面的次数)。LVGL 会占用一部分内存开销(内存 = 芯片里临时存数据的空间)来存放画面缓冲,这是它「帮你干活」的代价。
本教程基于安信可官方 SDK(Ai-Thinker-Open/Ai-Thinker-WB2,版本
release_bl_iot_sdk_1.6.40)的官方示例applications/iot-solution/lvgl_example编写(本页使用其中的hello_lvgl入门示例),代码可在本地 SDK 中直接找到。lvgl_example/widgets目录下还有按钮、滑动条、下拉列表等十多个控件示例(如lvgl_button、lvgl_slider、lvgl_switch),本页跑通后可直接切换进阶。
LVGL 本身是纯软件库,不需要任何接线——它只是个「画画引擎」。本示例的画面显示在 SSD1306 OLED 屏幕上,接线与 SSD1306 教程 完全一致:
| Ai-WB2 引脚 | SSD1306 模块 |
|---|---|
| IO12 | SCL(时钟线) |
| IO3 | SDA(数据线) |
| 3V3 | VCC |
| GND | GND |
💡 如果你已经按 ssd1306 篇接好线,这里直接复用即可,不用动任何线。官方
lv_port_disp显示移植层也支持 ST7789、ST7796S 等其他屏幕(驱动见components/stage/lvgl/lv_device/),换屏时需要同步修改lv_conf.h里的屏幕型号与分辨率配置。
打开终端进入官方 hello_lvgl 示例工程目录:
cd ~/Ai-Thinker-WB2/applications/iot-solution/lvgl_example/hello_lvgl
说明:
cd是「进入目录」命令,这里进入 hello_lvgl 工程目录;后续的make编译、make flash烧录命令都必须先在这个目录里执行。
工程目录结构说明:
| 文件 | 作用 |
|---|---|
hello_lvgl/main.c |
主程序源码,本教程主要修改的文件 |
hello_lvgl/bouffalo.mk |
工程编译配置(引入 LVGL 组件),一般无需修改 |
lv_conf.h |
LVGL 配置头文件:屏幕型号(LV_DISPLAY_SSD1306)、I2C 引脚(OLED_IIC_SCL/SDA)、方向、分辨率都在这里改 |
📌
lvgl_example/下还有widgets/目录,里面有按钮(lvgl_button)、滑动条(lvgl_slider)、开关(lvgl_switch)、下拉列表(lvgl_downList)等十多个控件示例,每个都是独立工程,本页跑通后可cd进去编译烧录进阶学习。
打开 hello_lvgl/hello_lvgl/main.c,本步完整代码已移至文末,见:
📜 完整代码 — 位于本页「完整代码」章节,默认折叠,点击展开,与官方示例(
applications/iot-solution/lvgl_example/hello_lvgl/hello_lvgl/main.c)完全一致。
代码要点:
| 代码 | 作用 |
|---|---|
lv_init() |
初始化 LVGL 图形库(分配内存、建立对象系统),不调用后面所有控件 API 都会失败 |
lv_port_disp_init() |
把 LVGL 的「画布」接到 SSD1306 屏幕(显示移植层),不接画面就没有输出地方 |
hosal_timer_init(&lv_timer_dev) |
创建 1ms 周期定时器,回调里调 lv_tick_inc(1) 给 LVGL 提供「心跳」计时,没有它时间不走、动画不转 |
hosal_timer_start(&lv_timer_dev) |
启动定时器,不启动 LVGL 的心跳就一直停着 |
lv_label_create(lv_scr_act()) |
在当前屏幕(lv_scr_act() = 当前活动屏幕)上创建文本控件,控件 = 界面上的文字/按钮等元素 |
lv_label_set_text(label1, "Hello lvgl") |
设置文本内容,不设置标签就是空白的 |
lv_obj_align(label1, LV_ALIGN_CENTER, 1, 1) |
把文本居中(偏移 1 像素),不居中的话文字出现在屏幕左上角 |
lv_timer_handler() |
LVGL 的「引擎」:循环调用它才刷新界面、处理动画,帧率(每秒刷新次数)由它决定 |
vTaskDelay(10/portTICK_PERIOD_MS) |
每 10ms 让出 CPU 再刷新,没有它 LVGL 会独占 CPU 导致系统卡死 |
在工程目录执行编译:
make -j8
说明:
make是「编译工程」命令,把代码变成开发板能运行的固件(程序文件);-j8表示用 8 个核并行编译,速度更快。LVGL 库较大,首次编译需要多等一会儿。
编译成功后生成固件 build_out/hello_lvgl.bin。
开发板保持 USB 连接,确认串口设备号(Linux 下通常为 /dev/ttyUSB0),执行烧录:
make flash p=/dev/ttyUSB0 b=921600
说明:
make flash是「烧录」命令,把编译好的固件写进开发板芯片。p=后面是串口设备号(要改成你电脑上实际的串口,可用ls /dev/ttyUSB*查看),b=是烧录波特率(传输速度)。
⏳ 烧录过程中按提示长按开发板 EN 键进入下载模式(部分开发板自动进入),等待进度条完成即烧录成功。
烧录完成后开发板自动重启运行,观察 OLED 屏幕:
屏幕中央应显示 Hello lvgl 文本(如果你之前烧录过 ssd1306 篇的固件,屏幕会从「日期时间」变成「Hello lvgl」,说明 LVGL 已经接管了屏幕)。
💡 进阶玩法:进
lvgl_example/widgets/下的其他控件工程(如lvgl_button、lvgl_slider),用同样的编译烧录流程即可体验按钮、滑动条等交互控件;想改显示方向,修改lv_conf.h中LV_DISPLAY_ORIENTATION_LANDSCAPE(改为LV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTED则为镜像显示)。
看到屏幕中央显示 Hello lvgl 即为成功;如果屏幕不亮、显示乱码或编译失败,说明还没成功,对照文末「常见问题与踩坑提示」排查。
代码执行流程
例程从启动到运行的完整流程如下(图中的循环箭头表示反复执行):
本文 API 汇总
lv_init()
初始化 LVGL 图形库:建立对象系统、分配内部内存(LVGL 需要一块内存存放对象和画面缓冲,即内存开销的来源)。使用任何 LVGL 控件 API 前必须先调用。
参数:无
返回值:无
lv_port_disp_init()
初始化显示移植层,把 LVGL 的「画布」接到具体屏幕上(SDK 移植层接口,源码见 components/stage/lvgl/lv_device/lv_port_disp.c)。本示例接 SSD1306(128×64),初始化内部显示缓冲并把刷新回调绑定到屏幕驱动。
参数:无
返回值:无
hosal_timer_init(timer)
初始化一个硬件定时器(hosal 定时器 API,hosal = SDK 的统一硬件抽象层)。本教程用它在定时回调里调用 lv_tick_inc(1),给 LVGL 提供 1ms 一跳的「心跳」计时(LVGL 靠它计算时间、驱动动画)。
参数:
timer:hosal_timer_dev_t结构体指针。关键字段:config.cb(定时回调函数,本教程timer_cb)、config.period(周期,单位 us,本教程1000= 1ms)、config.reload_mode(是否周期重载,TIMER_RELOAD_PERIODIC表示周期循环)、port(定时器号,本教程0)
返回值:成功返回 0;失败返回负值错误码
hosal_timer_start(timer)
启动已初始化的定时器,开始周期触发回调。
参数:
timer:hosal_timer_dev_t结构体指针(与hosal_timer_init同一个)
返回值:成功返回 0;失败返回负值错误码
lv_label_create(parent)
创建一个文本控件(Label,界面上的文字元素),并挂到指定的父对象上。
参数:
parent:父对象指针,可选值:任意控件对象,本教程用lv_scr_act()(当前活动屏幕,把文本直接放屏幕上)
返回值:新创建的文本控件对象指针(lv_obj_t*);失败返回 NULL
lv_label_set_text(obj, text)
设置文本控件显示的内容。
参数:
obj:文本控件对象指针(lv_label_create的返回值)text:要显示的字符串,本教程"Hello lvgl"
返回值:无
lv_obj_align(obj, align, x_ofs, y_ofs)
把控件按指定方式对齐到父对象上(LVGL 通用控件对齐 API,所有控件都能用)。
参数:
obj:要对齐的控件对象指针align:对齐方式,可选值:LV_ALIGN_CENTER(居中)、LV_ALIGN_TOP_LEFT(左上)等,本教程居中x_ofs:水平偏移(像素),可选值:任意整数,本教程1y_ofs:垂直偏移(像素),可选值:任意整数,本教程1
返回值:无
lv_timer_handler()
LVGL 的「引擎」:处理所有待刷新区域、运行动画和定时任务。必须在主循环里反复调用,否则界面不会刷新(帧率 = 每秒刷新画面的次数,就由这个函数的调用频率决定)。
参数:无
返回值:距离下次需要调用它的毫秒数(uint32_t),一般无需处理
vTaskDelay(ms)
让当前任务挂起指定毫秒数,期间让出 CPU 给其他任务(FreeRTOS 系统 API)。
参数:
ms:延时毫秒数,可选值:任意非负整数(本教程10/portTICK_PERIOD_MS= 10ms,先让出 CPU 再刷新,避免独占系统)
返回值:无
完整代码
以下为 hello_lvgl/hello_lvgl/main.c 完整源码,与官方示例(applications/iot-solution/lvgl_example/hello_lvgl/hello_lvgl/main.c)完全一致:
📜 点击展开 hello_lvgl/hello_lvgl/main.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();
}
}常见问题与踩坑提示
⚠️ 编译失败,报一堆 undefined reference to 'lv_xxx'
原因:LVGL 组件没有被编译进工程(组件引入配置问题)
解决:确认在工程根目录 ~/Ai-Thinker-WB2/applications/iot-solution/lvgl_example/hello_lvgl 执行 make -j8,且 hello_lvgl/bouffalo.mk 未被改动;LVGL 库较大,请耐心等编译完成
⚠️ 屏幕不亮(白屏)
原因:接线错误或 I2C 地址不对(LVGL 与 ssd1306 篇共用同一块屏幕和地址 0x3C)
解决:按 ssd1306 篇排查:IO12=SCL、IO3=SDA、VCC 接 3V3、GND 共地;确认模块地址是 0x3C(lv_conf.h 中 OLED 配置)
⚠️ 屏幕有显示但方向不对(倒着/镜像)
原因:lv_conf.h 中显示方向宏与屏幕实际方向不匹配
解决:把 lv_conf.h 中 LV_DISPLAY_ORIENTATION_LANDSCAPE 换成 LV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTED,重新编译烧录
⚠️ 画面卡顿、刷新慢
原因:I2C(400KHz)传一帧 128×64 画面本身较慢,或 LVGL 内存缓冲不足
解决:本示例帧率受 I2C 速度限制属正常;如需流畅动画可换 SPI 屏幕(lv_port_disp 支持 ST7789/ST7796S);不要随意调小 lv_conf.h 中 LV_MEM_SIZE(内存开销不够会导致控件创建失败)
⚠️ 烧录时提示无法打开串口 / 一直卡住等待
原因:串口设备号不对、权限不足,或未进入下载模式、数据线只能充电
解决:确认设备号 ls /dev/ttyUSB*,权限不足执行 sudo usermod -aG dialout $USER;烧录时按提示长按 EN 键,换一根能传数据的 Type-C 数据线后重试
运行自检
SSD1306 屏幕中央显示 Hello lvgl 文本,即 LVGL 最小系统验证通过;再进入 lvgl_example/widgets/ 编译运行其他控件示例,可确认交互控件正常。
遇到问题?
如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

