一、移植LVGL的前期准备
第一步:ESP32 S3 LVGL版本选择
对于 ESP32-S3,推荐使用 LVGL v8.3.11,因为这个版本在性能、兼容性和 bug 修复方面较为稳定,具体原因如下:
推荐使用 LVGL v8.3.11 的原因
-
官方稳定版本:
- LVGL v8.3.11 是 LVGL v8.3 系列的最终修复版,修复了之前版本的一些 bug,提高了稳定性。
- ESP32 官方文档 和 Espressif 提供的示例工程 也是基于 LVGL v8.3.x,说明这个版本在 ESP32 上经过了较多的优化和测试。
-
适配 ESP32-S3 的 DMA & PSRAM:
- ESP32-S3 具备 PSRAM,LVGL v8.3 在 DMA 传输和 PSRAM 兼容性 方面已经进行了优化,能够更好地适配 S3 硬件加速。
- 之前的 LVGL 7.x 或 8.0 版本 在 ESP32 的 DMA 传输 和 PSRAM 支持 上可能会有 bug 或不够完善。
-
支持 SquareLine Studio:
- SquareLine Studio 是目前最流行的 LVGL UI 设计工具,且最高支持 LVGL v8.3.x。
- 如果你想通过 可视化界面拖拽设计 UI,使用 LVGL v8.3.11 兼容性最佳。
-
支持最新的 LVGL 组件和优化:
- LVGL v8.3 提供了更多的 UI 控件、动画效果,以及更好的输入设备适配(例如触摸屏驱动)。
- 适用于 ESP32-S3 LCD、触摸屏、SPI DMA 传输 等场景。
第二步:LVGL 源码下载
1. 官方 GitHub 下载(推荐)
方式 1:GitHub 页面手动下载(我先着重使用手动下载方式,更适合小白)
- 打开 LVGL GitHub 仓库
https://github.com/lvgl/lvgl
- 点击选择“v8.3”版本
- 点击 “Code” 按钮
- 选择 “Download ZIP” 下载
下载后,解压缩即可使用。
方式 2:直接克隆 GitHub 仓库
在你的 ESP32 开发环境(VS Code、ESP-IDF 终端等)中运行:
git clone --branch v8.3.11 --depth 1 https://github.com/lvgl/lvgl.git
如果你需要最新的 v8.3.11 版本,确保使用 --branch v8.3.11
参数。
2. 使用 ESP-IDF 下载(适用于 ESP32)
如果你使用的是 ESP-IDF 开发环境,可以直接运行:
idf.py add-dependency "lvgl/lvgl^8.3.11"
这将自动下载 LVGL v8.3.11 并添加到 ESP-IDF 组件中。
3. 使用 PlatformIO 下载(适用于 VS Code)
如果你在 VS Code + PlatformIO 开发环境中:
- 在
platformio.ini
文件中添加:
lib_deps = lvgl/lvgl @ ^8.3.11
- 保存后,PlatformIO 会自动下载并集成 LVGL 组件。
4. 在 SquareLine Studio 里下载
如果你使用 SquareLine Studio 设计 UI,它会自动提供 LVGL v8.3.x 版本,你可以直接导出到 ESP32 项目中。
第三步:ESP32 IDF工程文件和LVGL源码准备
我们找到IDF库的例子工程,在IDF安装路径的Espressif\frameworks\esp-idf-v5.3.1\examples\get-started路径下,将里面的hello world 工程文件复制出来。
重命名hello world 工程文件夹为“ESP32S3N16R8_with_LVGLv8.3.11_20250211”,并解压好刚才下载好的LVGL_v8.3.11源码,如图:
打开我们准备好的ESP32S3N16R8_with_LVGLv8.3.11_20250211工程文件,在里面新建一个“components”文件夹,用于存放ESP32组件文件(lvgl源码或者屏幕的驱动代码等等)。
第四步:显示(和触摸)驱动准备
在 ESP32-S3 上移植 LVGL 之前,需要先准备好 显示驱动(LCD) 和 触摸驱动(如果有触摸)。
以下是主要的要点:
(1)确保屏幕的驱动可用,并且可以实现在屏幕上 画点,矩形填充的功能
(2)确保屏幕的触摸驱动可用(如果有触摸功能),并且可以实现获取触摸的XY坐标的功能
(注意:您可以用 lv_port_esp32 里提供的 LVGL 显示驱动,如果里面有支持您选择的屏幕驱动IC)
为了使新手小白更深入的学会和理解LVGL如何在ESP32上移植,我先选择使用自己编写的触摸屏驱动用于lvgl移植!
我手上的屏幕是1.69寸,240x280分辨率,16位色,RGB565,驱动IC为ST7789V,通信接口SPI,触摸IC是 CST816T,通信接口IIC。
在“components”文件夹里面新建“BSP”文件夹,把提前准备好的屏幕驱动代码复制到BSP文件夹
为了将复制过来的屏幕驱动代码链接到ESP32的工程里面,我们还需在BSP文件夹下新建一个CMakeLists.txt文件,里面内容为:
set(src_dirs
LCD_ST7789V_SPI
TOUCH_CST816T_I2C
)
set(include_dirs
LCD_ST7789V_SPI
TOUCH_CST816T_I2C
)
set(requires
driver
esp_timer
esp_driver_ledc
)
idf_component_register(
SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs}
REQUIRES ${requires}
)
component_compile_options(
-ffast-math
-O3
-Wno-error=format=-Wno-format
-Werror=implicit-function-declaration
)
(注意:实际要根据你自己的驱动文件夹来模仿编写,从而把你的屏幕驱动文件链接到ESP32 IDF工程里面)
二、LVGL 源码移植
第一步:源码文件移植
将刚才下载准备好的lvgl-release-v8.3源码解压复制到我们新建的“components”文件夹,并重命名为lvgl (这一步必须重命名为lvgl !!,后面会提到原因)
做好以上步骤后,我们使用VS CODE 打开ESP32S3N16R8_with_LVGLv8.3.11_20250211工程,可以看到工程文件列表如下:
依次在VS CODE 上配置好ESP32对应芯片的配置:(该步骤默认小白已经掌握,我不再赘述哈!)
激动人心的时刻到来:编译!!!!
重要的事情说三遍:如果编译成功并没有错误的话,那恭喜你可以接着下面的步骤继续移植,如果还有编译报错,请先解决报错!!!
三、LVGL 显示驱动接口移植
核心任务
- 初始化 LCD 硬件(SPI 或并口驱动)
- 实现
flush_cb
(LVGL → LCD 数据传输) - 配置 LVGL 的显示缓冲区
- 注册显示驱动
对于LVGL显示驱动接口的移植,LVGL源码里面提供了移植示例,我们找到lvgl\examples\porting文件夹,里面有6个文件:
他们分别是 :
显示驱动移植示例代码(lv_port_disp_template)
文件系统移植示例代码(lv_port_fs_template)
输入设备驱动移植示例代码(lv_port_indev_template)
为方便调用管理,我们需要在ESP32 IDF 工程的main文件夹下新建“lv_port”文件夹,然后把这个porting文件夹里面的文件全部复制到新建的“lv_port”文件夹(没有用到的可以不用复制),并重命名,如图:
为了能把新建的lv_port文件夹里面的文件链接到ESP32 IDF工程里面,并且需要添加lv_por所依赖的组件:BSP lvgl 到PRIV_REQUIRES,我们需要修改main文件夹下的CMakeLists.txt,如下:
set(src_dirs
./
./lv_port
)
set(include_dirs
./
./lv_port
)
set(priv_requires
driver
esp_timer
esp_driver_ledc
BSP
lvgl
)
idf_component_register(
SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs}
PRIV_REQUIRES ${priv_requires}
EMBED_FILES ${embed_files}
)
component_compile_options(
-O3
-ffast-math
-Wno-error=format=-Wno-format
)
1.lv_port_disp.c
(显示驱动移植)
✅ 作用:
- 这个文件提供了一个 模板,帮助开发者移植 LCD 屏幕(SPI、RGB、8080并口)
- 主要完成 LVGL → 屏幕的数据传输
第一步:启用接口文件
打开lv_port文件夹,lv_port_disp.c文件,找到文件开头的
“#if 0”条件编译,将其改为“#if 1”,这样就启用了该接口文件,如图:
第二步:配置屏幕的分辨率
找到lv_port_disp.c文件里面的
MY_DISP_HOR_RES MY_DISP_VER_RES 宏定义,它们分别是设置屏幕水平分辨率和垂直分辨率(我手上的屏幕分辨率是240*280),请根据您的屏幕的实际分辨率填写即可:
第三步:配置LVGL 的显存缓冲区
在 lv_port_disp.c文件的
lv_port_disp_init()
里,"Create a buffer for drawing" 指的是 LVGL 的显存缓冲区,用于 存放 LCD 需要刷新的数据。选择合适的缓冲区大小 影响性能和内存使用,需要综合考虑 ESP32 的 RAM 资源、屏幕大小、刷新效率。
查看后我们可以清楚看到LVGL提供了缓冲区的三种选择:
要是您对这3种LVGL显示缓存区的具体区别有疑问,请移步查看:LVGL各种缓冲区分析比较(源码角度)_lvgl源码分析-优快云博客https://blog.youkuaiyun.com/weixin_42914339/article/details/129640435 为了方便移植讲解,我们先使用第一种显示缓存模式:单缓存模式!
注意:如果您想要使用其他2种显存缓存模式,记得要在disp_drv.draw_buf = &draw_buf_dsc_1; 处把缓存的指针改成您想使用的缓存指针。
第四步:显示屏IC驱动初始化
找到lv_port_disp.c文件里面的
disp_init(),在里面添加我们写好的LCD屏幕初始化的接口:
注意:要在 lv_port_disp.c文件头部添加你自己的LCD屏幕的驱动的头文件哦!以及包含
lv_port_disp.h头文件,如图:
第五步:LVGL 显示接口的flush_cb
刷屏回调函数配置(disp_flush)
查看disp_flush()函数里面的示例刷屏方式,我们发现这是画点方式来实现刷屏的,这个方式效率极低,但是确实是万能的!
为提高屏幕刷新的效率,我们将官方给的示例代码注释掉,将其改为自己编写的的矩形填充的LCD接口函数:
注意:该步骤极其关键,必须确保您自己写的LCD画点或者矩形填充接口函数是没有问题的!!! 如果您不确定您的矩形填充接口是否适配的话,请在这里调用自己屏幕的画点接口函数。
第六步:启用显示接口文件的头文件(lv_port_dsp.h
)
打开lv_port文件夹,lv_port_dsp.h文件,找到文件开头的
“#if 0”条件编译,将其改为“#if 1”,这样就启用了该接口文件的头文件了,如图:
至此,我们的显示屏的显示驱动接口就已经移植完成!!!
附录:我自己的ST7789V屏幕移植好的lv_port_dsp.c代码
/**
* @file lv_port_disp_templ.c
*
*/
/*Copy this file as "lv_port_disp.c" and set this value to "1" to enable content*/
#if 1
/*********************
* INCLUDES
*********************/
#include "lv_port_disp.h"
#include <stdbool.h>
#include "bsp_lcd_st7789v_spi.h"
/*********************
* DEFINES
*********************/
#ifndef MY_DISP_HOR_RES
#warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
#define MY_DISP_HOR_RES 240
#endif
#ifndef MY_DISP_VER_RES
#warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now.
#define MY_DISP_VER_RES 280
#endif
/**********************
* TYPEDEFS
**********************/
/**********************
* STATIC PROTOTYPES
**********************/
static void disp_init(void);
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
// const lv_area_t * fill_area, lv_color_t color);
/**********************
* STATIC VARIABLES
**********************/
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
void lv_port_disp_init(void)
{
/*-------------------------
* Initialize your display
* -----------------------*/
disp_init();
/*-----------------------------
* Create a buffer for drawing
*----------------------------*/
/**
* LVGL requires a buffer where it internally draws the widgets.
* Later this buffer will passed to your display driver's `flush_cb` to copy its content to your display.
* The buffer has to be greater than 1 display row
*
* There are 3 buffering configurations:
* 1. Create ONE buffer:
* LVGL will draw the display's content here and writes it to your display
*
* 2. Create TWO buffer:
* LVGL will draw the display's content to a buffer and writes it your display.
* You should use DMA to write the buffer's content to the display.
* It will enable LVGL to draw the next part of the screen to the other buffer while
* the data is being sent form the first buffer. It makes rendering and flushing parallel.
*
* 3. Double buffering
* Set 2 screens sized buffers and set disp_drv.full_refresh = 1.
* This way LVGL will always provide the whole rendered screen in `flush_cb`
* and you only need to change the frame buffer's address.
*/
/* Example for 1) */
static lv_disp_draw_buf_t draw_buf_dsc_1;
static lv_color_t buf_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
lv_disp_draw_buf_init(&draw_buf_dsc_1, buf_1, NULL, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
// /* Example for 2) */
// static lv_disp_draw_buf_t draw_buf_dsc_2;
// static lv_color_t buf_2_1[MY_DISP_HOR_RES * 10]; /*A buffer for 10 rows*/
// static lv_color_t buf_2_2[MY_DISP_HOR_RES * 10]; /*An other buffer for 10 rows*/
// lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, buf_2_2, MY_DISP_HOR_RES * 10); /*Initialize the display buffer*/
// /* Example for 3) also set disp_drv.full_refresh = 1 below*/
// static lv_disp_draw_buf_t draw_buf_dsc_3;
// static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*A screen sized buffer*/
// static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES]; /*Another screen sized buffer*/
// lv_disp_draw_buf_init(&draw_buf_dsc_3, buf_3_1, buf_3_2,
// MY_DISP_VER_RES * LV_VER_RES_MAX); /*Initialize the display buffer*/
/*-----------------------------------
* Register the display in LVGL
*----------------------------------*/
static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
/*Set up the functions to access to your display*/
/*Set the resolution of the display*/
disp_drv.hor_res = MY_DISP_HOR_RES;
disp_drv.ver_res = MY_DISP_VER_RES;
/*Used to copy the buffer's content to the display*/
disp_drv.flush_cb = disp_flush;
/*Set a display buffer*/
disp_drv.draw_buf = &draw_buf_dsc_1;
/*Required for Example 3)*/
//disp_drv.full_refresh = 1;
/* Fill a memory array with a color if you have GPU.
* Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
* But if you have a different GPU you can use with this callback.*/
//disp_drv.gpu_fill_cb = gpu_fill;
/*Finally register the driver*/
lv_disp_drv_register(&disp_drv);
}
/**********************
* STATIC FUNCTIONS
**********************/
/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
/*You code here*/
st7789v_Init();
}
volatile bool disp_flush_enabled = true;
/* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL
*/
void disp_enable_update(void)
{
disp_flush_enabled = true;
}
/* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL
*/
void disp_disable_update(void)
{
disp_flush_enabled = false;
}
/*Flush the content of the internal buffer the specific area on the display
*You can use DMA or any hardware acceleration to do this operation in the background but
*'lv_disp_flush_ready()' has to be called when finished.*/
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
if(disp_flush_enabled) {
// /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
// int32_t x;
// int32_t y;
// for(y = area->y1; y <= area->y2; y++) {
// for(x = area->x1; x <= area->x2; x++) {
// /*Put a pixel to the display. For example:*/
// /*put_px(x, y, *color_p)*/
// color_p++;
// }
// }
st7789v_fillColor((uint16_t)area->x1,(uint16_t)area->y1,(uint16_t)area->x2,(uint16_t)area->y2,(uint16_t*)color_p,USE_HORIZONTAL);
}
/*IMPORTANT!!!
*Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
/*OPTIONAL: GPU INTERFACE*/
/*If your MCU has hardware accelerator (GPU) then you can use it to fill a memory with a color*/
//static void gpu_fill(lv_disp_drv_t * disp_drv, lv_color_t * dest_buf, lv_coord_t dest_width,
// const lv_area_t * fill_area, lv_color_t color)
//{
// /*It's an example code which should be done by your GPU*/
// int32_t x, y;
// dest_buf += dest_width * fill_area->y1; /*Go to the first line*/
//
// for(y = fill_area->y1; y <= fill_area->y2; y++) {
// for(x = fill_area->x1; x <= fill_area->x2; x++) {
// dest_buf[x] = color;
// }
// dest_buf+=dest_width; /*Go to the next line*/
// }
//}
#else /*Enable this file at the top*/
/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif
四、LVGL 输入接口移植(例如:触摸驱动)
在 LVGL中,lv_port_indev.c
主要负责某某输入设备的适配和初始化。ESP32 的应用场景下,输入设备通常包括:
- 触摸屏(电容触摸/电阻触摸)
- 物理按键
- 编码器(旋钮)
- 鼠标(调试用)
lv_port_indev.c
作用:
1️⃣ 初始化输入设备(如触摸屏、按键)
2️⃣ 注册到 LVGL(让 LVGL 识别输入设备)
3️⃣ 定期读取输入状态(触摸、按键按下等)
4️⃣ 数据转换(将原始数据转换成 LVGL 需要的格式)
1.lv_port_indev.c输入驱动移植(触摸作为输入为例)
第一步:启用输入接口文件
打开lv_port文件夹,lv_port_indev.c文件,找到文件开头的
“#if 0”条件编译,将其改为“#if 1”,这样就启用了该接口文件,如图:
第二步:输入设备(触摸)驱动初始化
注意:要在 lv_port_indev.c文件头部添加你自己的屏幕触摸的驱动的头文件哦!以及包含
lv_port_indev.h头文件,(我的屏幕触摸IC是cst816t)如图:
找到lv_port_indev.c文件里面的
touchpad_init(),在里面添加我们写好的显示屏触摸初始化的接口:
第三步:输入获取接口(获取xy)函数配置
找到lv_port_indev.c文件里面的
touchpad_read(); 我们很容易可以发现lvgl在读取触摸屏的触摸点的xy之前会判断触摸屏是否被按下。
所以 在移植获取触摸xy坐标接口函数前,我们还需要完成检查触摸屏是否被按下的功能函数:touchpad_is_pressed();的实现:
注意:这个触摸屏是否被按下的检测函数需要估计您的触摸屏来自己实现哦!
紧接着,我们就该移植touchpad_get_xy();获取触摸屏xy坐标的函数接口了:
第四步:启用输入接口文件的头文件(lv_port_indev.h)
第五步:清理输入接口文件中未用到的输入设备相关的代码(lv_port_indev.c)
重要事情说3遍!!!这一步非常重要!!!比如我们现在使用触摸为输入,那一定要清理掉 按键、编码器、鼠标等没有用到的输入设备代码!!!尤其是在lv_port_indev_init();输入接口初始化函数里面!如果不清理掉,那么lvgl的输入设备句柄(static lv_indev_drv_t indev_drv;)就会被后面的按键、编码器和鼠标的驱动初始化所覆盖,导致我们的触摸屏失效。清理过后只留下触摸屏相关输入代码的lv_port_indev.c 文件:
/*Copy this file as "lv_port_indev.c" and set this value to "1" to enable content*/
#if 1
#include "lv_port_indev.h"
#include "lvgl.h"
#include "bsp_cst816t.h"
static void touchpad_init(void);
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static bool touchpad_is_pressed(void);
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y);
lv_indev_t * indev_touchpad;
void lv_port_indev_init(void)
{
static lv_indev_drv_t indev_drv;
/*------------------
* Touchpad
* -----------------*/
/*Initialize your touchpad if you have*/
touchpad_init();
/*Register a touchpad input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
indev_touchpad = lv_indev_drv_register(&indev_drv);
}
/*------------------
* Touchpad
* -----------------*/
/*Initialize your touchpad*/
static void touchpad_init(void)
{
/*Your code comes here*/
cst816t_init();
}
/*Will be called by the library to read the touchpad*/
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
/*Save the pressed coordinates and the state*/
if(touchpad_is_pressed()) {
touchpad_get_xy(&last_x, &last_y);
data->state = LV_INDEV_STATE_PR;
}
else {
data->state = LV_INDEV_STATE_REL;
}
/*Set the last pressed coordinates*/
data->point.x = last_x;
data->point.y = last_y;
}
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
if(cst816t_scan()) return true;
return false;
}
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t * x, lv_coord_t * y)
{
/*Your code comes here*/
cst816t_getAdjustdXY(CST816T_ADDR,(uint16_t *)x,(uint16_t *)y);
// (*x) = 0;
// (*y) = 0;
}
#else /*Enable this file at the top*/
/*This dummy typedef exists purely to silence -Wpedantic.*/
typedef int keep_pedantic_happy;
#endif
五、LVGL 时基单元配置(LV_TICK)
LVGL 使用 时基(LV_TICK) 作为它的核心时间管理机制,用于驱动动画、任务(lv_task)、输入处理等功能。在 ESP32 上移植 LVGL 时,必须正确配置 LV_TICK,否则 UI 可能会卡顿或无法正常工作。
1. LV_TICK 作用
LV_TICK 负责:
- 驱动 LVGL 内部任务(lv_task_handler())
- 管理动画(如按钮点击的过渡效果)
- 处理输入设备(如触摸屏、按键)
- 定时更新 UI
2. 配置LVGL时基 LV_TICK 方法
ESP32 主要有3种方式配置 LV_TICK:
- ESPTimer(推荐)
- GPTimer(硬件定时器)
- FreeRTOS 软定时器
✅ 方法 1:使用 ESP32 ESPTimer 软件定时器(推荐 ✅)
ESP-IDF 提供的 高精度定时器,不受 FreeRTOS 任务调度影响,误差极低,非常适合 LVGL 时基。
📌 代码示例
#include "esp_timer.h"
#include "lvgl.h"
static esp_timer_handle_t lvgl_tick_timer = NULL;
// 定时回调函数,每 1ms 触发
static void lv_tick_task(void *arg) {
lv_tick_inc(1);
}
// 初始化 LVGL Tick 定时器
void lvgl_tick_timer_init(void) {
const esp_timer_create_args_t timer_args = {
.callback = &lv_tick_task,
.arg = NULL,
.dispatch_method = ESP_TIMER_TASK,
.name = "lv_tick_timer"
};
esp_timer_create(&timer_args, &lvgl_tick_timer);
esp_timer_start_periodic(lvgl_tick_timer, 1000); // 1ms 触发
}
📌 在 app_main()
里调用
lvgl_tick_timer_init(); // 初始化 LVGL 时基
📌 特点 ✅ 系统级高精度定时器(误差极小)
✅ 不会被 FreeRTOS 任务调度影响(更稳定)
✅ 代码简单,官方推荐
✅ 方法 2:使用 ESP32 GPTimer 硬件定时器
ESP32 通用定时器(GPTimer),更底层,适用于高精度需求,但代码更复杂。
📌 代码示例
#include "driver/gptimer.h"
#include "lvgl.h"
static gptimer_handle_t gptimer = NULL;
// 定时回调函数
static bool IRAM_ATTR gptimer_callback(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_ctx) {
lv_tick_inc(1); // 每 1ms 递增 LVGL Tick
return false; // 不需要触发高优先级任务
}
// 初始化 GPTimer
void lvgl_gptimer_init(void) {
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000 // 1MHz(1us 分辨率)
};
gptimer_new_timer(&timer_config, &gptimer);
gptimer_event_callbacks_t cbs = {
.on_alarm = gptimer_callback
};
gptimer_register_event_callbacks(gptimer, &cbs, NULL);
gptimer_alarm_config_t alarm_config = {
.alarm_count = 1000, // 1ms 触发一次
.reload_count = 0,
.flags.auto_reload_on_alarm = true
};
gptimer_set_alarm_action(gptimer, &alarm_config);
gptimer_enable(gptimer);
gptimer_start(gptimer);
}
📌 在 app_main()
里调用
lvgl_tick_timer_init(); // 初始化 LVGL 时基
📌 特点 ✅ 高精度(1us 级别)
✅ 适用于超高精度需求
🟡 代码复杂度较高,不适用于一般 LVGL 应用
✅ 方法3:使用 FreeRTOS 软定时器
使用 FreeRTOS 软定时器(Software Timer)实现 LVGL Tick,容易受任务调度影响,导致误差较大 (不推荐)。
📌 代码示例
#include "freertos/FreeRTOS.h"
#include "freertos/timers.h"
#include "lvgl.h"
static TimerHandle_t lv_tick_timer;
static void lv_tick_task(TimerHandle_t xTimer) {
lv_tick_inc(1); // 每 1ms 递增 LVGL 计时
}
void lvgl_tick_timer_init(void) {
lv_tick_timer = xTimerCreate("lv_tick", pdMS_TO_TICKS(1), pdTRUE, NULL, lv_tick_task);
if (lv_tick_timer != NULL) {
xTimerStart(lv_tick_timer, 0);
}
}
📌 在 app_main()
里调用
lvgl_tick_timer_init(); // 初始化 LVGL 时基
📌 特点 🟡 受 FreeRTOS 任务调度影响,可能有误差
🟡 适用于简单应用
❌ 不推荐用于复杂 UI
📊 三种 LV_TICK 实现对比
方法 | 适用场景 | 误差 | 代码复杂度 | 资源占用 | 推荐指数 |
---|---|---|---|---|---|
esp_timer (推荐) | 大多数 UI 应用 | 极低(系统时钟级) | 简单 | 低 | ✅✅✅ |
GPTimer | 超高精度需求(计时) | 极低(1us 级) | 复杂 | 低 | ✅✅ |
FreeRTOS 软定时器 | 简单 UI | 可能较大 | 简单 | 低 | ❌ |
✅ 推荐 esp_timer
,稳定性高,代码简单,适用于 大多数 ESP32 + LVGL 项目 🚀
3. 确保 lv_task_handler()
在主循环中执行
无论你选择哪种 LV_TICK 方法,都必须在 app_main()
里调用lv_task_handler()
:
while (1) {
lv_task_handler(); // LVGL 任务管理
vTaskDelay(pdMS_TO_TICKS(10)); // 延迟 10ms
}
🚀 lv_task_handler()
必须 定期调用,否则 UI 无法刷新!
至此,LVGL时基就已经配置完成了!!!
六、LVGL 的核心配置文件(lv_conf.h)
lv_conf.h
是 LVGL 的核心配置文件,用于 启用 / 禁用 LVGL 功能、优化性能、减少内存占用。
1.lv_conf.h
的关键作用
- 控制 LVGL 组件启用 / 禁用(如按钮、滑块、进度条等)。
- 设置颜色格式(RGB565、ARGB8888 等),影响 屏幕颜色显示。
- 定义缓冲区大小,优化 内存管理。
- 启用 / 关闭动画、字体、日志,优化 性能与调试。
- 选择时基单元(Tick Source),确保 LVGL 正确计时。
2.如何配置lv_conf.h
第一步:启用lv_conf.h
打开lvgl源码文件夹,里面有一个lv_conf_template.h文件,这个就是官方给我们的lv_conf.h示例文件:
我们直接将lv_conf_template.h文件拷贝到lvgl\src文件夹里面,并重命名为lv_conf.h:
打开刚才复制到src文件夹里面的lv_conf.h文件,找到文件开头的
“#if 0”条件编译,将其改为“#if 1”,这样就启用了该头文件了,如图:
第二步:设置颜色格式(LV_COLOR_DEPTH)
我的屏幕是16位色的RGB565屏幕,所以我的设置为#define LV_COLOR_DEPTH 16,加上我的屏幕的通讯接口为SPI,每次只能发8位的颜色数据,所以如果后面在 ESP32 上使用 LVGL 时,颜色显示异常(如蓝色变成绿色),就需要#define LV_COLOR_16_SWAP 1 ,对于我的屏幕是要交换颜色的高低字节的。(当通过字节导向接口(如 SPI)发送 16 位颜色时,您可能需要此功能。由于 16 位数字以小端格式存储(低位字节在低地址),因此接口将首先发送低位字节。但是,显示器通常需要先发送高位字节。字节顺序不匹配会导致颜色严重失真。)
参考于LVGL源码里面的lvgl\docs\overview\color.md说明:
如果对 lv_conf.h还有其他的疑问,可以查阅下面的博客:
LVGL配置文件lv_conf.h详解-优快云博客https://blog.youkuaiyun.com/qq_28576837/article/details/136333681
第三步:检查IDF的menuconfig的LVGL配置(非常重要哦!!!)
找到LVGL configuration,取消勾选“Uncheck this to use custom lv_conf.h”项,确保 lv_conf.h生效!!!(ESP32 menuconfig 默认是勾选该项,即使用IDF menuconfig里面的lvgl配置,就不会使用我们移植的lv_conf.h)
注意:记得要保存并重新编译呀!
七、LVGL 测试
在测试LVGL前,我们需要再main.c里面添加相关的接口头文件:
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
#include "lv_demos.h"
方式一:创建一个简单的按钮测试LVGL。(简单推荐)
📌 代码示例
void app_main(void)
{
lvgl_tick_timer_init(); // 初始化 LVGL 时基
lv_init(); /* 初始化LVGL图形库 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
//lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
/*在屏幕中间创建一个120*50大小的按钮*/
lv_obj_t* switch_obj = lv_switch_create(lv_scr_act());
lv_obj_set_size(switch_obj, 120, 50);
lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
while (1)
{
lv_task_handler(); // LVGL 任务管理
vTaskDelay(pdMS_TO_TICKS(10)); // 延迟 10ms
}
}
📌 屏幕显示效果
方式二:使用LVGL官方给出的测试demo(比如music demo)
第一步:链接LVGL官方给的demo示例代码到IDF工程里面,例如:music demo 
第二步:打开我们移植好的lv_conf.h文件,找到LV_USE_DEMO_MUSIC,打开该宏定义。
注意:在开启LV_USE_DEMO_MUSIC的同时,找到字体开启处,开启下面3种字体:
第三步:在main函数里面调用 lv_demo_music(); 示例:
void app_main(void)
{
lvgl_tick_timer_init1(); // 初始化 LVGL 时基
lv_init(); /* 初始化LVGL图形库 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
//lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
// /*在屏幕中间创建一个120*50大小的按钮*/
// lv_obj_t* switch_obj = lv_switch_create(lv_scr_act());
// lv_obj_set_size(switch_obj, 120, 50);
// lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
lv_demo_music();
while (1)
{
lv_task_handler(); // LVGL 任务管理
vTaskDelay(pdMS_TO_TICKS(10)); // 延迟 10ms
}
}
激动人心的时刻再次到来:编译下载!!!!
📌 屏幕显示效果(music demo)
我自己的 ST7789V屏幕+CST816T触摸 移植好的main.c代码
/*
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h>
#include <inttypes.h>
#include "sdkconfig.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "freertos/timers.h"
#include "esp_timer.h"
#include "driver/gptimer.h"
#include "lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
#include "lv_demos.h"
/* 方法 1:使用 ESP32 ESPTimer 软件定时器(推荐 )*/
static esp_timer_handle_t lvgl_tick_esptimer = NULL;
// 定时回调函数,每 1ms 触发
static void lv_tick_esptask(void *arg) {
lv_tick_inc(1);
}
// 初始化 LVGL Tick 定时器
void lvgl_tick_timer_init1(void) {
const esp_timer_create_args_t timer_args = {
.callback = &lv_tick_esptask,
.arg = NULL,
.dispatch_method = ESP_TIMER_TASK,
.name = "lv_tick_timer"
};
esp_timer_create(&timer_args, &lvgl_tick_esptimer);
esp_timer_start_periodic(lvgl_tick_esptimer, 1000); // 1ms 触发
}
/* 方法 2:使用 ESP32 GPTimer 硬件定时器*/
static gptimer_handle_t gptimer = NULL;
static bool IRAM_ATTR lv_tick_cb(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *user_data) {
lv_tick_inc(1); // 1ms 递增
return true; // 返回 true 继续触发
}
void lvgl_tick_timer_init2(void)
{
gptimer_config_t timer_config = {
.clk_src = GPTIMER_CLK_SRC_DEFAULT,
.direction = GPTIMER_COUNT_UP,
.resolution_hz = 1000000 // 1MHz, 即 1us 计数一次
};
gptimer_new_timer(&timer_config, &gptimer);
gptimer_event_callbacks_t cbs = {
.on_alarm = lv_tick_cb
};
gptimer_register_event_callbacks(gptimer, &cbs, NULL);
gptimer_alarm_config_t alarm_config = {
.alarm_count = 1000, // 1000us = 1ms
.reload_count = 0,
.flags.auto_reload_on_alarm = true
};
gptimer_set_alarm_action(gptimer, &alarm_config);
gptimer_enable(gptimer);
gptimer_start(gptimer);
}
/* 方法3:使用 FreeRTOS 软定时器*/
static TimerHandle_t lv_tick_freertostimer;
static void lv_tick_freertostask(TimerHandle_t xTimer) {
lv_tick_inc(1); // 每 1ms 递增 LVGL 计时
}
void lvgl_tick_timer_init3(void)
{
lv_tick_freertostimer = xTimerCreate("lv_tick", pdMS_TO_TICKS(1), pdTRUE, NULL, lv_tick_freertostask);
if (lv_tick_freertostimer != NULL) {
xTimerStart(lv_tick_freertostimer, 0);
}
}
void app_main(void)
{
lvgl_tick_timer_init1(); // 初始化 LVGL 时基
lv_init(); /* 初始化LVGL图形库 */
lv_port_disp_init(); /* lvgl显示接口初始化,放在lv_init()的后面 */
lv_port_indev_init(); /* lvgl输入接口初始化,放在lv_init()的后面 */
// /*在屏幕中间创建一个120*50大小的按钮*/
// lv_obj_t* switch_obj = lv_switch_create(lv_scr_act());
// lv_obj_set_size(switch_obj, 120, 50);
// lv_obj_align(switch_obj, LV_ALIGN_CENTER, 0, 0);
lv_demo_music();
while (1)
{
lv_task_handler(); // LVGL 任务管理
vTaskDelay(pdMS_TO_TICKS(10)); // 延迟 10ms
}
}
附录:移植好LVGLv8.3.11的完整工程文件
ESP32S3N16R8_with_LVGLv8.3.11_ST7789V+CST816T_20250213.zip工程文件:
如果需要移植到STM32平台的,可以先参考我编写的在STM32 keil 5 环境下的LVGL移植教程,大致的移植思路都是一样的: