第2章 LVGL图形库入门:从零开始搭建你的第一个屏幕
大家好,欢迎来到第二章。上一章我们搭好了硬件环境,这一章,我们来聊聊LVGL——这个在嵌入式圈子里越来越火的图形库。
说实话,我第一次接触LVGL是在一个智能家居项目里。当时客户要求做一块3.5寸的触摸屏,要显示温湿度、灯光状态,还要有滑动切换页面的效果。我一开始想用传统的画点画线方式,结果算了一下工作量……嗯,果断放弃。后来同事推荐了LVGL,一试就回不去了。
2.1 LVGL是什么?为什么选它?
LVGL的全称是Light and Versatile Graphics Library,翻译过来就是“轻量且通用的图形库”。它专门为嵌入式系统设计,资源占用小,但功能却非常丰富。
我总结了一下,LVGL有这几个核心特点:
- 开源免费:MIT协议,商用也没问题
- 控件丰富:按钮、滑块、图表、键盘……你能想到的它基本都有
- 硬件适配简单:只需要提供显示驱动和触摸驱动,剩下的LVGL帮你搞定
- 支持样式和动画:可以做出很漂亮的界面,不像传统嵌入式UI那么死板
- 内存占用可控:从几十KB到几百KB都能跑,ESP32完全够用
核心观点:LVGL不是简单的画图库,它是一个完整的GUI框架。你写的是业务逻辑,而不是像素操作。
2.2 LVGL在ESP32上的移植
移植LVGL到ESP32,说白了就是让LVGL知道“怎么往屏幕上画东西”和“怎么读取触摸数据”。我刚开始移植的时候也踩过坑,这里把步骤拆开讲清楚。
2.2.1 获取LVGL源码
我建议直接用ESP-IDF的组件管理器来拉取LVGL,这样版本管理最省心。在项目根目录下执行:
idf.py add-dependency "lvgl/lvgl^8.3.0"
如果你习惯手动管理,也可以从GitHub下载源码,放到components目录下。我个人更推荐第一种方式,因为后续升级方便。
2.2.2 配置LVGL
LVGL有一个核心配置文件叫lv_conf.h。你需要把它从LVGL源码的模板复制出来,放到项目根目录,然后修改几个关键参数:
/* 颜色深度,常见的是16位RGB565 */
#define LV_COLOR_DEPTH 16
/* 显示缓冲区大小,我一般设为屏幕宽度的10倍 */
#define LV_HOR_RES_MAX 320
#define LV_VER_RES_MAX 240
/* 开启触摸支持 */
#define LV_INDEV_USE_TOUCH 1
/* 内存分配,使用ESP-IDF的heap管理 */
#define LV_MEM_CUSTOM 1
我的经验:颜色深度选16位就够了,32位虽然颜色更丰富,但内存占用翻倍,对ESP32来说性价比不高。
2.2.3 编写显示驱动
LVGL需要你提供两个函数:一个初始化显示,一个刷新显示缓冲区。这里以常见的ST7789驱动芯片为例:
/* 显示初始化 */
void lvgl_display_init(void)
{
/* 初始化SPI、GPIO、背光等 */
spi_bus_config_t buscfg = {
.miso_io_num = -1,
.mosi_io_num = 23,
.sclk_io_num = 18,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = LV_HOR_RES_MAX * 40 * 2
};
spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
/* 初始化LCD控制器 */
lcd_st7789_init();
}
/* 刷新缓冲区,LVGL会调用这个函数把数据推送到屏幕 */
void lvgl_flush_cb(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
{
/* 使用DMA传输,效率更高 */
lcd_st7789_write_ram(area->x1, area->y1, area->x2, area->y2, (uint8_t *)color_p);
lv_disp_flush_ready(drv);
}
注意:刷新函数最后一定要调用lv_disp_flush_ready(),否则LVGL会认为传输还没完成,一直等待。我曾经漏掉这一行,结果屏幕死活不显示,排查了半天。
2.2.4 编写触摸驱动
触摸驱动相对简单,你只需要在LVGL的轮询中返回当前触摸点的坐标:
void lvgl_touch_read_cb(lv_indev_drv_t *drv, lv_indev_data_t *data)
{
uint16_t x, y;
uint8_t pressed;
/* 读取触摸芯片,比如FT6236 */
ft6236_read(&x, &y, &pressed);
if (pressed) {
data->point.x = x;
data->point.y = y;
data->state = LV_INDEV_STATE_PR;
} else {
data->state = LV_INDEV_STATE_REL;
}
}
2.3 创建第一个屏幕
移植完成后,我们来写第一个屏幕。别紧张,代码量很少。
#include "lvgl.h"
void app_main(void)
{
/* 1. 初始化LVGL */
lv_init();
/* 2. 初始化显示和触摸 */
lvgl_display_init();
lvgl_touch_init();
/* 3. 创建一个屏幕对象 */
lv_obj_t *scr = lv_obj_create(NULL);
lv_scr_load(scr);
/* 4. 在屏幕上放一个标签 */
lv_obj_t *label = lv_label_create(scr);
lv_label_set_text(label, "Hello, ESP32!");
lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
/* 5. 进入LVGL主循环 */
while (1) {
lv_timer_handler();
vTaskDelay(pdMS_TO_TICKS(5));
}
}
这段代码做了几件事:初始化LVGL、加载屏幕、显示文字、进入循环。你烧录进去,屏幕上应该会出现居中的“Hello, ESP32!”。
关键点:lv_timer_handler()必须定期调用,它是LVGL的心脏,负责处理动画、输入、重绘等所有事情。我一般放在一个5ms周期的任务里。
2.4 理解显示缓冲区
显示缓冲区是LVGL性能的关键。你想想看,LVGL要绘制各种控件、处理触摸反馈,如果每次画一个像素就刷新一次屏幕,那效率得多低?
所以LVGL的做法是:先在内存里画好一整块区域,然后一次性推送到屏幕。这块内存就是显示缓冲区。
2.4.1 缓冲区的工作模式
LVGL支持三种缓冲区模式,我整理了一个表格:
| 模式 | 缓冲区大小 | 特点 | 适用场景 |
|---|---|---|---|
| 单缓冲区 | 1个全屏大小 | 简单,但刷新时会有撕裂感 | 静态界面,不常用 |
| 双缓冲区 | 2个全屏大小 | 无撕裂,但内存占用大 | 动画流畅,推荐使用 |
| 部分缓冲区 | 屏幕宽度的10~20行 | 省内存,但刷新次数多 | 内存紧张时使用 |
我个人最常用的是部分缓冲区模式。以320x240的屏幕为例,如果设置缓冲区为320x20像素,每个像素2字节,那么缓冲区大小就是320 * 20 * 2 = 12.8KB。ESP32的RAM完全扛得住。
2.4.2 缓冲区配置示例
/* 定义缓冲区 */
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 20]; /* 320 * 20 = 6400像素 */
/* 初始化缓冲区 */
lv_disp_draw_buf_init(&draw_buf, buf, NULL, LV_HOR_RES_MAX * 20);
/* 关联到显示驱动 */
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.draw_buf = &draw_buf;
disp_drv.flush_cb = lvgl_flush_cb;
disp_drv.hor_res = 320;
disp_drv.ver_res = 240;
lv_disp_drv_register(&disp_drv);
避坑指南:我曾经把缓冲区设得太小,比如只有屏幕宽度的5行,结果界面刷新特别慢,滑动时明显卡顿。后来改成20行,效果好了很多。建议至少10行起步。
2.5 本章小结
这一章我们走完了LVGL的入门流程:了解它是什么、移植到ESP32、创建第一个屏幕、理解缓冲区原理。说实话,移植这一步最容易出问题,但只要显示和触摸驱动写对了,后面就顺了。
下一章,我们会深入LVGL的控件系统,教你怎么用按钮、滑块和标签搭出一个真正的智能家居控制面板。到时候你会看到,LVGL的控件就像乐高积木一样,拼一拼就是一个完整的界面。
嗯,今天就到这里。如果你在移植过程中遇到问题,可以回头看看显示刷新函数有没有调用lv_disp_flush_ready()——我敢打赌,80%的问题都出在这里。