第12章 LVGL
约 2367 字大约 8 分钟
2026-03-23
1 LVGL 概述
LVGL(Light and Versatile Graphics Library:轻量和通用图形库),是一款免费且开源的图形库,提供创建嵌入式 GUI 所需的一切,包括易于使用的图形元素、美丽的视觉效果和低内存占用。
官网: https://lvgl.io/
文档: https://lvgl.100ask.net/master/index.html
1.1 LVGL的特点
(1)丰富且强大的模块化图形组件:按钮(buttons)、图表(charts)、列表(lists)、滑动条(sliders)、图片(images)等。
(2)高级的图形引擎:动画、抗锯齿、透明度、平滑滚动、图层混合等效果。
(3)支持多种输入设备:触摸屏、键盘、编码器、按键等。
(4)支持多显示设备。
(5)不依赖特定的硬件平台,可以在任何显示屏上运行。
(6)配置可裁剪(最低资源占用:64 kB Flash,16 kB RAM)。
(7)基于UTF-8的多语种支持,例如中文、日文、韩文、阿拉伯文等。
(8)可以通过类CSS的方式来设计、布局图形界面(例如:Flexbox、Grid)。
(9)支持操作系统、外置内存、以及硬件加速(LVGL已内建支持STM32 DMA2D、NXP PXP和VGLite)。
(10)即便仅有单缓冲区(frame buffer)的情况下,也可保证渲染如丝般顺滑。
(11)全部由C编写完成,并支持C++调用。
(12)支持Micropython编程,参见:LVGL API in Micropython。
(13)支持模拟器仿真,可以无硬件依托进行开发。
(14)丰富详实的例程。
(15)在 MIT 许可下免费和开源。
1.2 LVGL对硬件的要求
基本上,每个能够驱动显示器的现代控制器都适合运行 LVGL。最低要求是:
| 名字 | 最低 | 推荐 |
|---|---|---|
| 架构 | 16、32 或 64 位微控制器或处理器(arm或x86) | |
| 时钟 | >16MHz | >48MHz |
| Flash(ROM) | >64KB | >180KB |
| SRAM | >16KB | >48KB |
| 显示缓冲区 | >1 * 1 * 水平分辨率 | > 10 * 10 * 水平分辨率 |
| 帧缓冲区 | MCU或外部显示控制器中的一个帧缓冲区 | |
| 编译器 | C99 或更新的编译器(keil5 务必开启c99支持) | |
| 需要的知识 | C或c++ |
2 LCD 驱动
2.1 官方示例位置
2.2 代码实现
int_lcd.h
#ifndef __INT_LCD_H__
#define __INT_LCD_H__
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_timer.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#define LCD_PIXEL_CLOCK_HZ (20 * 1000 * 1000)
#define LCD_BK_LIGHT_ON_LEVEL 1
#define LCD_BK_LIGHT_OFF_LEVEL !LCD_BK_LIGHT_ON_LEVEL
#define PIN_NUM_SCLK 47
#define PIN_NUM_MOSI 48
#define PIN_NUM_MISO 18
#define PIN_NUM_LCD_DC 45
#define PIN_NUM_LCD_RST 16
#define PIN_NUM_LCD_CS 21
#define PIN_NUM_BK_LIGHT 40
#define LCD_HOST SPI2_HOST
/**
* @brief 初始化LCD
*
*/
void int_lcd_init(void);
#endif /* __INT_LCD_H__ */int_lcd.c
#include "int_lcd.h"
static const char *TAG = "LCD:";
esp_lcd_panel_io_handle_t io_handle;
esp_lcd_panel_handle_t panel_handle;
/**
* @brief 初始化LCD
*
*/
void int_lcd_init(void)
{
// 初始化背光引脚
ESP_LOGI(TAG, "Turn off LCD backlight");
gpio_config_t bk_gpio_config = {
.mode = GPIO_MODE_OUTPUT,
.pin_bit_mask = 1ULL << PIN_NUM_BK_LIGHT,
};
ESP_ERROR_CHECK(gpio_config(&bk_gpio_config));
// 初始化SPI总线
ESP_LOGI(TAG, "Initialize SPI bus");
spi_bus_config_t buscfg = {
.sclk_io_num = PIN_NUM_SCLK,
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(spi_bus_initialize(LCD_HOST, &buscfg, SPI_DMA_CH_AUTO));
// LCD配置
ESP_LOGI(TAG, "Install panel IO");
esp_lcd_panel_io_spi_config_t io_config = {
.dc_gpio_num = PIN_NUM_LCD_DC,
.cs_gpio_num = PIN_NUM_LCD_CS,
.pclk_hz = LCD_PIXEL_CLOCK_HZ,
.lcd_cmd_bits = 8,
.lcd_param_bits = 8,
.spi_mode = 0,
.trans_queue_depth = 10,
};
// 将LCD加载到SPI总线上
ESP_ERROR_CHECK(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)LCD_HOST, &io_config, &io_handle));
// LCD配置
esp_lcd_panel_dev_config_t panel_config = {
.reset_gpio_num = PIN_NUM_LCD_RST,
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_BGR,
.bits_per_pixel = 16,
};
// 设置LCD显示屏类型
esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle);
// 重置LCD
ESP_ERROR_CHECK(esp_lcd_panel_reset(panel_handle));
// 初始化LCD
ESP_ERROR_CHECK(esp_lcd_panel_init(panel_handle));
// 是否翻转LCD颜色
ESP_ERROR_CHECK(esp_lcd_panel_invert_color(panel_handle, false));
// X轴Y轴是否需要镜像
ESP_ERROR_CHECK(esp_lcd_panel_mirror(panel_handle, false, false));
// 启动LCD模块
ESP_ERROR_CHECK(esp_lcd_panel_disp_on_off(panel_handle, true));
// 开启背光
gpio_set_level(PIN_NUM_BK_LIGHT, LCD_BK_LIGHT_ON_LEVEL);
}2.3 测试
main.c
// 初始化LCD
int_lcd_init();3 LVGL移植
从资料中找到lvgl移植的示例代码:
解压后重新为其选择esp-idf框架,target,端口以及刷机方法等:

完成后尝试构建并刷入项目,观看lvgl的演示demo。
这个项目演示了lvgl移植的所需要的完整步骤:
(1)LCD显示驱动:我们这里使用spi接口的st7789;
(2)esp_lvgl_port依赖,初始化lvgl后台刷新任务;
(3)LVGL绘制UI,这里我们采用了LVGL自带的演示项目
4 LVGL基本使用
4.1. Objects(对象)
在LVGL中,用户界面的基本组成部分是对象,也称为 Widgets。例如,一个按钮、标签、图像、列表、图表或者文本区域都可称为一个对象。
所有的对象都使用 lv_obj_t 指针作为句柄进行引用。之后可以使用该指针来设置或获取对象的属性。
4.1.1 属性
1)Basic attributes(基本属性)
所有的对象类型都有如下通用的基本属性:位置,大小,父级,样式,事件处理程序等等。
可以使用 lv_obj_set_... 和 lv_obj_get_... 函数设置或者获取这些属性。
2)Specific attributes(特殊属性)
对象类型也有特殊的属性。例如,滑块有:最小值和最大值,当前值。
针对这些特殊属性,每个对象类型可能有独特的API函数。例如,对于滑块。
4.1.2 对象的父子结构
一个父对象可以被视为其子对象的容器。每个对象都必须会有且仅有一个父对象(屏幕除外),但一个父对象可以有任意数量的子对象。父对象的类型没有限制,但是有些对象一般是父对象(例如按钮)或者是子对象(例如标签)。
如果父对象的位置改变,子对象也会随之移动。因此,所有子对象的位置都是相对于父对象而言的。
移动父对象后。你会发现在子对象会随着父对象进行移动。
如果一个子对象部分或完全超出父对象,则超出部分将不可见。
4.1.3 对象的动态创建和删除
在LVGL中,可以在运行时动态创建或删除对象。这也就是说,直到当对象被创建之后才会消耗内存资源。
因此,可以在点击按钮准备打开新界面(屏幕)时再创建新界面(屏幕),并在加载新界面(屏幕)时删除旧界面(屏幕)。
每个控件都有自己的 create 函数,函数原型如下:
lv_obj_t * lv__create(lv_obj_t * parent, <如果有其他参数>);通常,创建函数只有一个 parent 参数,指示在哪个对象上创建该控件。返回值是指向创建出来的控件的指针,类型为 lv_obj_t *。
有一个通用的 delete 函数适用于所有对象类型。它删除对象及其所有子对象。
void lv_obj_delete(lv_obj_t * obj);lv_obj_del() 会立即删除对象。如果出于任何原因无法立即删除对象,可以使用 lv_obj_delete_async(obj) ,它会在下一次调用lv_timer_handler()时执行删除操作。这在子对象的 LV_EVENT_DELETE 处理程序中删除父对象时很有用(现在不能马上删除父对象,下一次运行lv_timer_handler时再删除)。
可以使用 lv_obj_clean(obj) 删除对象的所有子对象(但不包括对象本身)。
可以使用 lv_obj_delete_delayed(obj, 1000) 在经过一定时间后再删除对象,以毫秒为单位。
4.2 Screens(屏幕)
屏幕是一种特殊的对象,它们没有父对象。我们可以在创建对象前,先获取活动的屏幕,然后在其之上建立对象。
lv_obj_t*screen=lv_screen_active();5. 初始化LVGL
5.1. 安装包
组件地址:https://components.espressif.com/components/espressif/esp_lvgl_port
idf.py add-dependency "espressif/esp_lvgl_port^2.6.3"5.2. 在 LCD 中暴露io_handle 和 panel_handle
int_led.h
extern esp_lcd_panel_io_handle_t io_handle;
extern esp_lcd_panel_handle_t panel_handle;5.2 代码
app_display.h
#ifndef __APP_DISPLAY_H__
#define __APP_DISPLAY_H__
#include "esp_lvgl_port.h"
#include "int_lcd.h"
#define LCD_H_RES (240)
#define LCD_V_RES (320)
/**
* @brief 初始化显示
*
*/
void app_display_init(void);
#endif /* __APP_DISPLAY_H__ */app_display.c
#include "app_display.h"
static const char *TAG = "LVGL";
/**
* @brief 初始化显示
*
*/
void app_display_init(void)
{
/* 初始化 LVGL */
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 10, /* LVGL task priority */
.task_stack = 4096, /* LVGL task stack size */
.task_affinity = -1, /* LVGL task pinned to core (-1 is no affinity) */
.task_max_sleep_ms = 500, /* Maximum sleep in LVGL task */
.timer_period_ms = 5 /* LVGL timer tick period in ms */
};
lvgl_port_init(&lvgl_cfg);
/* Add LCD screen */
ESP_LOGD(TAG, "Add LCD screen");
const lvgl_port_display_cfg_t disp_cfg = {
.io_handle = io_handle,
.panel_handle = panel_handle,
.buffer_size = LCD_H_RES * 10,
.double_buffer = true,
.hres = LCD_H_RES,
.vres = LCD_V_RES,
.monochrome = false,
.color_format = LV_COLOR_FORMAT_RGB565,
.rotation = {
.swap_xy = false,
.mirror_x = false,
.mirror_y = false,
},
.flags = {
.buff_dma = true,
.swap_bytes = true,
}};
lvgl_port_add_disp(&disp_cfg);
// 创建屏幕
lv_obj_t *scr = lv_scr_act();
lvgl_port_lock(0);
/* Your LVGL objects code here .... */
/* Label */
lv_obj_t *text_label1 = lv_label_create(scr);
lv_label_set_long_mode(text_label1, LV_LABEL_LONG_WRAP); /*Break the long lines*/
lv_label_set_text(text_label1, "Xiaozhi_AI Xiaozhi_AI Xiaozhi_AI Xiaozhi_AI Xiaozhi_AI Xiaozhi_AI");
lv_obj_set_width(text_label1, LCD_H_RES); /*Set smaller width to make the lines wrap*/
lv_obj_set_style_text_align(text_label1, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(text_label1, LV_ALIGN_CENTER, 0, -40);
lvgl_port_unlock();
}6. 显示中文
6.1. 安装包
组件地址:https://components.espressif.com/components/78/xiaozhi-fonts
idf.py add-dependency "78/xiaozhi-fonts^1.5.5"6.2. LVGL的显示配置





6.3. 代码
app_display.h
// 添加头文件包含
#include "font_emoji.h"
#include "font_puhui.h"app_display.c
// 初始化加载中文字体
LV_FONT_DECLARE(font_puhui_14_1);
// 在显示文字的下边添加:
lv_obj_set_style_text_font(text_label1, &font_puhui_14_1, 0);7 显示 emoji
app_display.c
lv_obj_t *emoji_label1 = lv_label_create(screen);
lv_label_set_long_mode(emoji_label1, LV_LABEL_LONG_WRAP); /*Break the long lines*/
lv_label_set_text(emoji_label1, "😍");
lv_obj_set_style_text_font(emoji_label1, font_emoji_64_init(), 0);
lv_obj_set_width(emoji_label1, LCD_H_RES); /*Set smaller width to make the lines wrap*/
lv_obj_set_style_text_align(emoji_label1, LV_TEXT_ALIGN_CENTER, 0);
lv_obj_align(emoji_label1, LV_ALIGN_CENTER, 0, -90);8. 封装
app_display.h
void app_display_show_text(char *text);
void app_display_show_emoji(char *emotion);app_display.c
typedef struct Emotion
{
const char *icon;
const char *text;
} Emotion;
static const Emotion emotions[] = {
{"😶", "neutral"},
{"🙂", "happy"},
{"😆", "laughing"},
{"😂", "funny"},
{"😔", "sad"},
{"😠", "angry"},
{"😭", "crying"},
{"😍", "loving"},
{"😳", "embarrassed"},
{"😯", "surprised"},
{"😱", "shocked"},
{"🤔", "thinking"},
{"😉", "winking"},
{"😎", "cool"},
{"😌", "relaxed"},
{"🤤", "delicious"},
{"😘", "kissy"},
{"😏", "confident"},
{"😴", "sleepy"},
{"😜", "silly"},
{"🙄", "confused"},
};
void app_display_show_text(char *text)
{
lvgl_port_lock(0);
lv_label_set_text(text_label1, text);
lvgl_port_unlock();
}
void app_display_show_emoji(char *emotion)
{
// 计算表情包的个数
int emoji_count = sizeof(emotions) / sizeof(Emotion);
for (uint16_t i = 0; i < emoji_count; i++)
{
// 取出数组的某个值
Emotion emotion_stru = emotions[i];
if (strcmp(emotion, emotion_stru.text) == 0)
{
lvgl_port_lock(0);
lv_label_set_text(emoji_label1, emotion_stru.icon);
lvgl_port_unlock();
return;
}
}
}main.c
else if (strcmp(type_str, "tts") == 0)
{
MY_LOGI("收到tts消息");
else if (strcmp(state_str, "sentence_start") == 0)
{
// 得到json中的text
cJSON *text = cJSON_GetObjectItem(root, "text");
char *text_str = cJSON_GetStringValue(text);
inf_display_show_text(text_str);
}
else if (strcmp(type_str, "llm") == 0)
{
MY_LOGI("收到llm消息");
// 取出emotion字段
cJSON *emotion = cJSON_GetObjectItem(root, "emotion");
char *emotion_str = cJSON_GetStringValue(emotion);
inf_display_show_emoji(emotion_str);
}