Skip to content

概述

LVGL 是一款开源的嵌入式图形库(GUI 库,GUI = 图形用户界面),专门给资源有限的单片机做「手机 UI」:按钮、进度条、输入框、列表等现成控件(widget,界面上的按钮/进度条/输入框等元素)拿来就能用,不用自己一个个像素去画。本教程使用官方 lvgl_example 里的 hello_lvgl 入门示例:在 SSD1306 OLED 屏幕(128×64)中央显示 「Hello lvgl」 文本,跑通 LVGL 在 Ai-WB2 上的最小系统。

用大白话讲:LVGL 就像给开发板装了个「安卓系统」——你不需要关心每个图标怎么画、每个界面怎么布局,只要说「我要一个按钮,放在中间,文字写 OK」,它就能帮你画出来。本教程的「按钮」叫控件(widget),画面每秒刷新多少次叫帧率(帧率 = 每秒刷新画面的次数)。LVGL 会占用一部分内存开销(内存 = 芯片里临时存数据的空间)来存放画面缓冲,这是它「帮你干活」的代价。

本教程基于安信可官方 SDKAi-Thinker-Open/Ai-Thinker-WB2,版本 release_bl_iot_sdk_1.6.40)的官方示例 applications/iot-solution/lvgl_example 编写(本页使用其中的 hello_lvgl 入门示例),代码可在本地 SDK 中直接找到。lvgl_example/widgets 目录下还有按钮、滑动条、下拉列表等十多个控件示例(如 lvgl_buttonlvgl_sliderlvgl_switch),本页跑通后可直接切换进阶。

🎯本页目标跑通 LVGL 最小系统:在 SSD1306 屏幕上显示居中的「Hello lvgl」文本,理解控件、帧率与内存开销。
🧰前置条件① Ai-WB2 开发板、0.96 寸 SSD1306 OLED 模块、杜邦线 ② 已按 [SDK 安装](../sdk/sdk_intro) 完成开发环境搭建;建议先完成 [SSD1306 OLED 显示屏](./ssd1306)(接线相同)。
🔗相关章节屏幕驱动基础见 [SSD1306 OLED 显示屏](./ssd1306);上篇 [TM1721 驱动芯片](./tm1721)。

硬件接线

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_buttonlvgl_slider),用同样的编译烧录流程即可体验按钮、滑动条等交互控件;想改显示方向,修改 lv_conf.hLV_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 靠它计算时间、驱动动画)。

参数

  • timerhosal_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)

启动已初始化的定时器,开始周期触发回调。

参数

  • timerhosal_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:水平偏移(像素),可选值:任意整数,本教程 1
  • y_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 完整代码
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 共地;确认模块地址是 0x3Clv_conf.h 中 OLED 配置)

⚠️ 屏幕有显示但方向不对(倒着/镜像)
原因lv_conf.h 中显示方向宏与屏幕实际方向不匹配
解决:把 lv_conf.hLV_DISPLAY_ORIENTATION_LANDSCAPE 换成 LV_DISPLAY_ORIENTATION_LANDSCAPE_INVERTED,重新编译烧录

⚠️ 画面卡顿、刷新慢
原因:I2C(400KHz)传一帧 128×64 画面本身较慢,或 LVGL 内存缓冲不足
解决:本示例帧率受 I2C 速度限制属正常;如需流畅动画可换 SPI 屏幕(lv_port_disp 支持 ST7789/ST7796S);不要随意调小 lv_conf.hLV_MEM_SIZE(内存开销不够会导致控件创建失败)

⚠️ 烧录时提示无法打开串口 / 一直卡住等待
原因:串口设备号不对、权限不足,或未进入下载模式、数据线只能充电
解决:确认设备号 ls /dev/ttyUSB*,权限不足执行 sudo usermod -aG dialout $USER;烧录时按提示长按 EN 键,换一根能传数据的 Type-C 数据线后重试

运行自检

SSD1306 屏幕中央显示 Hello lvgl 文本,即 LVGL 最小系统验证通过;再进入 lvgl_example/widgets/ 编译运行其他控件示例,可确认交互控件正常。

遇到问题?

如有其他问题,请到统一的提问与讨论区:Ai-Thinker Discussions

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