1. 硬件与软件环境搭建
在开始LVGL移植之前,我们需要准备好硬件和软件环境。我选择的硬件是ESP32-S3 N16R8开发板,这款芯片内置8MB PSRAM,对于图形界面开发来说非常关键。显示屏用的是ILI9341,这是一款240x320分辨率的TFT LCD屏,通过SPI接口通信。实际使用中发现,市面上有些ILI9341屏的驱动芯片略有差异,但大部分都能兼容。
软件方面,我使用ESP-IDF v5.4.1框架,这个版本对ESP32-S3的支持比较稳定。LVGL库选择v8.3.11版本,这个版本在性能和稳定性上都有不错的表现。ILI9341驱动用的是espressif官方提供的esp_lcd_ili9341组件,版本2.0.1。
开发环境我推荐VSCode配合ESP-IDF插件,这样代码提示和调试都会方便很多。刚开始玩的时候,我试过用纯命令行操作,但后来发现IDE的图形化界面确实能节省不少时间。
这里有个小坑要注意:有些教程会推荐用idf.py add-dependency直接添加依赖,但我建议手动下载组件包。因为自动下载有时候会碰到网络问题,而且手动管理能更清楚地知道每个文件的作用。组件下载地址在ESP-IDF的组件仓库里都能找到,搜索lvgl和esp_lcd_ili9341就行。
2. LVGL库移植实战
2.1 组件目录结构配置
首先在项目根目录下创建components文件夹,把下载的lvgl和esp_lcd_ili9341组件解压到这里。我习惯给每个组件都创建独立的文件夹,这样结构清晰,后期维护也方便。
对于esp_lcd_ili9341组件,只需要保留三个核心文件:include目录下的头文件、src目录下的源文件,还有CMakeLists.txt。这里要注意的是CMakeLists.txt需要做些修改,把包管理和版本信息检查的部分注释掉,因为我们不是通过包管理器安装的。
lvgl组件需要保留五个关键文件:src目录、examples目录、lv_conf.h、lvgl.h和CMakeLists.txt。其中lv_conf.h是由lv_conf-template.h改名得来的,这个文件是LVGL的配置核心,后面会详细讲怎么配置。
在CMakeLists.txt里,需要根据我们的使用场景做些调整。我把用不到的功能都删掉了,比如模拟器支持和一些不必要的驱动,这样编译速度会快很多。env_support目录下的CMake文件也只保留了esp32的相关内容,其他平台的都删除了。
2.2 显示驱动实现
接下来要创建自定义的ILI9341驱动组件。我在components目录下新建了user_lcd_ili9341文件夹,里面包含CMakeLists.txt、user_lcd_ili9341.c和include/user_lcd_ili9341.h。
在CMakeLists.txt里这样配置:
idf_component_register(SRCS "user_lcd_ili9341.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_lcd esp_timer esp_lcd_ili9341)
头文件里定义了硬件引脚和屏幕参数:
#define LCD_HOST SPI2_HOST
#define MY_DISP_HOR_RES 320
#define MY_DISP_VER_RES 240
#define MY_LVGL_BUFF_LINES 40
#define PIN_NUM_MOSI 13
#define PIN_NUM_SCLK 14
#define PIN_NUM_CS 10
#define PIN_NUM_DC 11
#define PIN_NUM_RST 12
#define PIN_NUM_BL -1
驱动实现里最关键的是SPI总线的初始化。我设置了10MHz的SPI时钟,这个速度在ESP32-S3上很稳定,再高的话可能会因为线长和屏的质量出现闪屏。DMA通道设置为自动选择,这样系统会帮我们分配最优的通道。
初始化过程中有几个容易出错的地方:一是GPIO引脚号要核对清楚,二是SPI模式要设置正确(ILI9341一般是mode 0),三是RGB颜色顺序要根据实际显示效果调整。我遇到过颜色反过来的情况,就是因为rgb_ele_order设置错了。
3. LVGL显示接口配置
3.1 显示缓冲区设置
显示缓冲区的配置是LVGL性能的关键。我创建了单独的user_lvgl组件,在其CMakeLists中添加了对user_lcd_ili9341和lvgl的依赖。
这里我参考了lvgl/examples/porting下的移植模板,把lv_port_disp_template.c和.h的内容复制到我的组件中,然后把条件编译#if 0改为#if 1启用相关代码。
缓冲区配置有三种模式,我每种都试过:
单缓冲模式最简单,但刷新时会有明显闪烁。双缓冲模式需要配合DMA使用,渲染和传输可以并行,效果很好。全刷新双缓冲模式最吃内存,但效果也最流畅。
我最终选择了双缓冲模式,分配40行缓存。计算下内存占用:320x40x2(双缓冲)x2(16位色)= 51.2KB,这在ESP32-S3的8MB PSRAM里根本不算什么。
size_t line_px = MY_DISP_HOR_RES * MY_LVGL_BUFF_LINES;
static lv_color_t buf_2_1[MY_DISP_HOR_RES * MY_LVGL_BUFF_LINES];
static lv_color_t buf_2_2[MY_DISP_HOR_RES * MY_LVGL_BUFF_LINES];
lv_disp_draw_buf_init(&s_draw_buf, buf_2_1, buf_2_2, line_px);
3.2 刷新回调函数实现
刷新回调函数disp_flush是连接LVGL和硬件驱动的桥梁。这里直接调用我们之前写好的user_esp_lcd_panel_draw_bitmap函数,把LVGL渲染好的区域数据发送到屏幕。
有个细节要注意:LVGL传来的区域坐标是包含的,即x2和y2坐标也是需要刷新的像素。但有些屏驱动需要的是独占坐标,所以我在调用时加了1:
user_esp_lcd_panel_draw_bitmap(area->x1, area->y1, area->x2 + 1, area->y2 + 1, color_p);
完成后一定要调用lv_disp_flush_ready通知LVGL刷新完成,否则LVGL会一直等待,导致界面卡死。
4. 内存优化与性能调优
4.1 内存分配策略
LVGL的内存管理很灵活,在lv_conf.h中有个关键设置LV_MEM_CUSTOM。如果设为0,LVGL使用内部的内存分配器,大小由LV_MEM_SIZE决定。如果设为1,则可以自定义内存分配函数。
我建议设为1,然后使用ESP32的堆分配函数:
#define LV_MEM_CUSTOM 1
#define LV_MEM_SIZE (100 * 1024)
#define LV_MEM_ATTR
#define LV_MEM_ADR 0
#define LV_MEM_CUSTOM_INCLUDE "stdlib.h"
#define LV_MEM_CUSTOM_ALLOC malloc
#define LV_MEM_CUSTOM_FREE free
#define LV_MEM_CUSTOM_REALLOC realloc
对于大块的显示缓冲区,最好分配到PSRAM中:
s_buf1 = (lv_color_t *)heap_caps_malloc(line_px * sizeof(lv_color_t), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
4.2 渲染性能优化
ESP32-S3有两个核心,我们可以利用这个特性提升性能。我把LVGL的刷新任务放在CPU0上,界面渲染任务放在CPU1上,这样能有效避免因为其他任务阻塞导致的界面卡顿。
创建专门GUI任务:
BaseType_t ok = xTaskCreatePinnedToCore(gui_task, "gui", 4096, NULL, 3, NULL, 1);
在gui_task中循环调用lv_timer_handler,延时时间设置为10ms比较合适。太短会浪费CPU,太长则界面响应迟钝。
我还开启了LVGL的GPU加速功能,虽然ESP32-S3没有硬件GPU,但LVGL提供了一些软件优化的绘制算法,能显著提升绘制效率。
5. 实际应用与问题排查
5.1 界面布局适配
ILI9341是240x320的分辨率,在实际使用中需要注意坐标系的转换。我写了个set_pos_from_bottom_left函数,可以方便地从左下角开始布局元素:
static inline void set_pos_from_bottom_left(lv_obj_t *obj, lv_coord_t x, lv_coord_t yb) {
lv_obj_update_layout(obj);
lv_coord_t w = lv_obj_get_width(obj);
lv_coord_t h = lv_obj_get_height(obj);
lv_coord_t yy = MY_DISP_VER_RES - yb - h;
lv_obj_set_pos(obj, x, yy);
}
这个函数会自动计算元素高度,然后从底部向上布局,比默认的从上向下布局更符合直觉。
5.2 常见问题解决
在移植过程中我遇到过两个典型问题:
一是程序崩溃,通常是内存分配不足。计算下内存需求:320x240全屏双缓冲需要320x240x2x2/1024=300KB,如果开启全刷新模式更是需要600KB。ESP32-S3虽然有很多PSRAM,但也要注意不要超分配。
二是显示模糊,这个问题困扰了我很久。最后发现是SPI时钟相位设置问题,把spi_mode从0改为1就清晰了。不同厂家的ILI9341屏可能略有差异,需要实际调试。
还有一次遇到屏幕花屏,原因是DMA传输大小设置不对。在spi_bus_config_t中需要设置max_transfer_sz,我设置为MY_DISP_HOR_RES * MY_LVGL_BUFF_LINES * 2,正好是一次传输的数据量。
6. 进阶优化技巧
6.1 双缓冲与局部刷新
双缓冲机制是提升显示性能的关键。我实测下来,开启双缓冲后刷新帧率能从15fps提升到35fps以上。实现方法是在显示驱动中维护两个缓冲区,LVGL在一个缓冲区渲染时,另一个缓冲区的数据正在通过DMA发送到屏幕。
局部刷新也能显著提升性能。在disp_flush回调中,我只刷新实际发生变化的区域,而不是整个屏幕。这对电池供电的设备尤其重要,能大大降低功耗。
6.2 动态内存管理
对于复杂界面,静态内存分配可能不够灵活。我实现了动态内存管理方案,根据界面复杂度动态调整缓冲区大小:
void adjust_buffer_size(size_t need_size) {
if (need_size > current_buffer_size) {
s_buf1 = realloc(s_buf1, need_size);
s_buf2 = realloc(s_buf2, need_size);
lv_disp_draw_buf_init(&s_draw_buf, s_buf1, s_buf2, need_size / sizeof(lv_color_t));
}
}
这样在简单界面时节省内存,在复杂界面时又能保证流畅性。
6.3 电源管理
ILI9341屏幕的背光功耗很大,我添加了背光控制功能。虽然硬件上可能没有直接连接背光控制引脚,但可以通过SPI命令调节亮度:
void set_brightness(uint8_t percent) {
uint8_t cmd = 0x51;
uint8_t data = percent * 255 / 100;
esp_lcd_panel_io_tx_param(s_io, cmd, &data, 1);
}
在设备休眠时,我还会完全关闭屏幕显示,只维持最低功耗的待机状态。这样整个系统的待机功耗能从50mA降到5mA以下。
移植完成后,整个系统运行很稳定,界面流畅度超出预期。ESP32-S3的性能确实强劲,驱动320x240的屏幕绰绰有余。最重要的是掌握了LVGL移植的方法论,以后遇到其他屏幕也能快速适配。
1019



被折叠的 条评论
为什么被折叠?



