之前在一个小屏设备上调试界面,一开始纠结要不要上 Qt,后来评估了内存和 Flash 占用以后,换成了 LVGL。实际跑起来之后,不管是页面切换、列表滑动还是动画过渡,整体都很顺滑。所以这篇就围绕“嵌入式 Linux + 小屏 + LVGL”这套组合,整理一份偏实战的笔记,内容包括环境准备、Framebuffer 显示对接、触摸屏接入、基础界面开发、性能优化和常见坑点。
适用读者:刚接触嵌入式 Linux 的开发者、想把 LVGL 跑在真实屏幕上的朋友,以及在 LVGL 8.x / 9.x 版本迁移中遇到问题的同学。文章里涉及的命令和代码以常见环境为例,你拿到手以后要根据自己的屏幕尺寸、触摸芯片和工具链做小范围调整。
1. 为什么嵌入式 Linux 小屏界面首选 LVGL
1.1 LVGL 是什么
LVGL 全称 LittlevGL,是一个开源的嵌入式图形库,官方定位是“为 MCU 和嵌入式 Linux 等资源受限设备提供现代、流畅、低资源占用的 GUI”。它支持丰富的控件:按钮、标签、进度条、开关、图表、键盘、动画、布局系统等。与直接在 Framebuffer 上自己画点、画线相比,LVGL 相当于帮你把 UI 框架、事件系统、输入设备管理、资源管理都封装好了。
很多开发者会问:嵌入式 Linux 上能跑 Qt,为什么还要用 LVGL?
关键区别在于资源占用。Qt 的功能很强大,但对小屏设备来说,库体积、内存占用和启动时间都偏高。LVGL 的代码量更小,裁剪配置灵活,可以做到几百 KB 级别的内存运行,特别适合 3.5 寸、4.3 寸、7 寸这类分辨率不高、性能有限的小屏幕设备。
1.2 嵌入式 Linux 小屏界面开发的常见痛点
小屏设备通常有几个特点:分辨率不高但需要精致效果,CPU 性能有限但希望有动画,Flash 空间有限但 UI 资源要尽量小。如果在裸机或者 RTOS 环境下,LVGL 也可以通过软件渲染直接跑在 LCD 驱动之上,但资源管理、文件系统、网络、多任务这些能力往往不如 Linux 完善。
在嵌入式 Linux 上使用 LVGL,最大的好处是可以复用 Linux 的生态:
- 使用 Framebuffer 或 DRM/KMS 显示接口;
- 通过 input 子系统读取触摸屏、按键、鼠标等设备;
- 使用 pthread 管理 UI 线程和业务线程;
- 使用文件系统加载图片、字体、配置文件;
- 支持网络、蓝牙、数据库等复杂业务逻辑。
LVGL 负责的只是“图形界面”这一层,底层硬件能力全部交给 Linux 内核和驱动,这也是它能在嵌入式 Linux 小屏设备上稳定运行的原因。
1.3 LVGL 与 Qt、AWTK 等方案怎么选
很多人在项目选型时纠结,这里给一个简单的判断思路:
- 如果你需要复杂的桌面级交互,或者需要 WebEngine、QML 生态,选 Qt。
- 如果屏幕小、硬件资源有限,UI 以仪表、控制面板、简单列表为主,选 LVGL。
- 如果团队熟悉 C 语言、希望深度定制控件,选 LVGL 更容易掌控。
- 如果已经有大量 AWTK 资源积累,AWTK 也是不错的选择,但 LVGL 社区更活跃、资料更多。
LVGL 这些年发展很快,从 8.x 到 9.x 一直在调整 API 和架构。做项目前优先确定版本,因为 8.x 和 9.x 的部分接口不兼容,后面会单独说明。
2. 环境准备与版本说明
2.1 硬件环境
本文的示例以常见的小屏 Linux 开发板为例,你需要确认以下信息:
- 屏幕接口:RGB 并口、SPI、LVDS、MIPI DSI 都行;
- 屏幕驱动:在内核里已经能正常显示,比如
/dev/fb0存在; - 触摸芯片:GT911、FT5x06、GT9147 等常见型号,内核已经注册为 input 设备;
- 开发板 CPU 架构:ARM、RISC-V 或 x86 都可以,LVGL 对架构没有强依赖。
用 LVGL 跑嵌入式 Linux,底层显示不一定要自己写驱动,重点是会调用 Linux 提供的显示接口和输入接口。屏幕本身能起来,LVGL 就成功了一大半。
2.2 软件环境与版本
LVGL 版本发展很快,目前常见的有 8.3.x 和 9.x。如果你是老项目或者习惯了lv_disp_drv_t那套 API,可以继续用 8.3;如果是新项目,建议优先考虑 9.x,因为官方后续迭代重心在 9.x。本文示例代码以 LVGL 9.x 为主,涉及到 8.x 差异的地方会单独说明。
从 github 拉取方式:
git clone --branch v9.2.0 --depth 1 https://github.com/lvgl/lvgl.git版本号要根据你的实际需要选择,不要盲目追新。9.x 不同小版本之间也有细微差异,比如某些 API 的参数顺序或宏名称变了。建议锁定一个小版本,并记录在项目 README 中。
2.3 构建工具与 IDE 搭配
在嵌入式 Linux 上开发 LVGL,推荐用 CMake + GCC 交叉编译工具链。IDE 方面,VS Code 配合 C/C++ 插件就很好用。
如果你是先在 PC 上调试界面,也可以使用官方模拟器项目,例如 lv_sim_eclipse_sdl 或 lv_port_pc_eclipse,在 Linux 桌面上利用 SDL2 模拟 LVGL 运行效果。先模拟器调 UI,再移植到开发板,效率会高很多。
常见软件清单:
- Ubuntu 20.04/22.04 或者开发板配套 Linux 环境;
- CMake 3.16+;
- 交叉编译工具链,比如 arm-linux-gnueabihf-gcc;
- 开发板 rootfs 中要有 framebuffer 和 input 设备驱动;
- 如果使用模拟器,需要安装 SDL2 开发库;
- VS Code 可选安装 CMake、C/C++ Extension Pack 插件。
3. LVGL 在嵌入式 Linux 上的运行机制
3.1 显示链路:Framebuffer
Linux 下最直接的显示输出接口是 Framebuffer,设备节点通常是/dev/fb0。应用程序可以通过open()打开设备,用ioctl()获取屏幕参数,再用mmap()把显存映射到用户空间。之后往这块内存里写入像素数据,屏幕上就会显示对应颜色。
LVGL 本身不直接知道怎么操作/dev/fb0,它需要你提供一个刷新回调。LVGL 内部根据“脏矩形”计算出需要重绘的区域后,把这一块像素矩阵交给回调,回调再负责把像素拷贝到 Framebuffer 的对应位置。
所以开发步骤是:
- 初始化 LVGL;
- 创建 display 对象;
- 注册 flush 回调;
- 在回调中实现显存拷贝;
- 创建输入设备并注册 read 回调;
- 在主循环中周期调用
lv_timer_handler()。
3.2 输入链路:input 子系统
触摸屏、鼠标、键盘等输入设备在 Linux 中通常表现为/dev/input/eventX。LVGL 的输入设备驱动(indev)需要你提供一个 read 回调,每次回调读取一次输入设备数据,并把坐标和按下状态告诉 LVGL。
实际项目中,输入坐标往往不能直接用于屏幕显示。比如触摸芯片上报的坐标范围是 0~4095,而屏幕分辨率是 480x272,需要做一次线性映射。映射公式也不复杂:
screen_x = touch_x * screen_width / touch_range_x;3.3 LVGL 的心跳与任务调度
LVGL 内部有大量定时任务,比如动画、刷新、控件状态更新。它自己会维护一个 tick 时钟,你的应用需要给它提供毫秒级时间戳。
在 Linux 上,可以通过clock_gettime(CLOCK_MONOTONIC)获取单调时钟,然后注册为 LVGL 的 tick 源。主循环中则反复调用lv_timer_handler()处理任务。
如果 UI 不顺滑,一个常见原因就是 tick 不准,或者lv_timer_handler()被其他耗时任务阻塞太长时间。后文会继续讨论。
4. 完整实战:在嵌入式 Linux 小屏上跑 LVGL
4.1 创建项目结构
先建一个清晰的目录结构,方便扩展:
lvgl_demo/ ├── CMakeLists.txt ├── lvgl/ # LVGL 源码 ├── main.c # 主程序入口 └── src/ ├── driver/ │ ├── fb.h │ ├── fb.c # Framebuffer 驱动 │ ├── ts.h │ └── ts.c # 触摸屏驱动 └── ui/ ├── ui_main.h └── ui_main.c # 界面代码建议把驱动和 UI 分开,不要把所有代码塞进一个 main.c。屏幕和触摸属于硬件抽象层,UI 属于业务层,两层分离后,换屏幕或换触摸芯片时不需要改动 UI 代码。
4.2 配置 lv_conf.h
LVGL 源码根目录下有一个lv_conf_template.h,你需要复制为lv_conf.h,并放在编译 include 路径中。关键配置如下:
#define LV_COLOR_DEPTH 16 #define LV_MEM_CUSTOM 1 #define LV_MEM_SIZE (256 * 1024) #define LV_FONT_MONTSERRAT_24 1 #define LV_USE_LOG 1LV_COLOR_DEPTH:屏幕色深,常见小屏是 RGB565,对应 16。LV_MEM_CUSTOM:自定义内存分配,Linux 下建议用标准库 malloc/free。LV_MEM_SIZE:LVGL 内部内存池大小,如果使用LV_MEM_CUSTOM 1,这个参数可以忽略;但如果是裸机移植,需要重点关注。LV_FONT_MONTSERRAT_24:启用内置英文字体,方便调试。LV_USE_LOG:打开日志,开发阶段很有用。
如果你的屏幕是 RGB888,则把LV_COLOR_DEPTH改为 24 或 32,同时注意 Framebuffer 的像素格式要匹配。
4.3 对接 Framebuffer 显示
先写一个最简单的 Framebuffer 初始化函数。这里以/dev/fb0为例。
// 文件路径:src/driver/fb.c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <linux/fb.h> #include "fb.h" static int fb_fd = -1; static struct fb_var_screeninfo vinfo; static struct fb_fix_screeninfo finfo; static void *fb_mem = NULL; static size_t fb_mem_len = 0; int fb_init(void) { fb_fd = open("/dev/fb0", O_RDWR); if (fb_fd < 0) { perror("open /dev/fb0 failed"); return -1; } if (ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo) < 0) { perror("FBIOGET_VSCREENINFO failed"); close(fb_fd); return -1; } if (ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo) < 0) { perror("FBIOGET_FSCREENINFO failed"); close(fb_fd); return -1; } fb_mem_len = finfo.smem_len; fb_mem = mmap(NULL, fb_mem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0); if (fb_mem == MAP_FAILED) { perror("mmap framebuffer failed"); close(fb_fd); return -1; } printf("fb: %dx%d, %d bpp, line_len=%d\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel, finfo.line_length); return 0; } void fb_exit(void) { if (fb_mem) { munmap(fb_mem, fb_mem_len); fb_mem = NULL; } if (fb_fd >= 0) { close(fb_fd); fb_fd = -1; } }mmap之后,fb_mem就可以当作一块连续内存使用。finfo.line_length是屏幕一行的字节数,有的系统为了对齐,会大于xres * bpp / 8。
接下来实现 LVGL 的 flush 回调。这里假设屏幕是 RGB565,且行长度与 LVGL 的 buffer 一致;如果line_length和理论行长度不一致,说明有 padding,需要按行拷贝。
// 文件路径:src/driver/fb.c #include "lvgl.h" void display_flush_cb(lv_display_t *display, const lv_area_t *area, lv_color_t *px_map) { int32_t x1 = area->x1; int32_t y1 = area->y1; int32_t x2 = area->x2; int32_t y2 = area->y2; int32_t row_bytes = (x2 - x1 + 1) * sizeof(lv_color_t); int32_t stride = finfo.line_length; char *fb_base = (char *)fb_mem; for (int32_t y = y1; y <= y2; y++) { char *fb_line = fb_base + y * stride + x1 * sizeof(lv_color_t); const char *lv_line = (const char *)px_map + (y - y1) * row_bytes; memcpy(fb_line, lv_line, row_bytes); } lv_display_flush_ready(display); }lv_display_flush_ready是 LVGL 9.x 的接口,表示“这一帧的数据已经处理完”。在 8.x 中,这个函数名是lv_disp_flush_ready。如果你的刷新很快,还可以在memcpy前后加内存屏障或dma_map操作,这里先不做额外处理。
然后在 main 中创建 display:
// main.c 中的部分代码 #define HOR_RES 480 #define VER_RES 272 #define DISP_BUF_SIZE (128 * 1024) static lv_color_t buf_1[DISP_BUF_SIZE]; static lv_display_t *disp; void lvgl_display_init(void) { disp = lv_display_create(HOR_RES, VER_RES); lv_display_set_flush_cb(disp, display_flush_cb); lv_display_set_buffers(disp, buf_1, NULL, DISP_BUF_SIZE * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL); }lv_display_set_buffers的第三个参数是第二个 buffer,传NULL表示单缓冲;如果传入第二个 buffer,LVGL 会使用双缓冲来减少撕裂。第四个参数在 9.x 中单位是字节数,在 8.x 中单位是像素数,这也是很多人版本升级后遇到过的问题。
4.4 接入触摸屏事件
先通过cat /proc/bus/input/devices查看触摸设备对应的 event 节点,通常名字里有 touch 或者具体的触摸芯片型号。
cat /proc/bus/input/devices确认设备节点后,用open()打开,并把read_cb注册给 LVGL。下面是一个基于struct input_event的读取示例。
// 文件路径:src/driver/ts.c #include <stdio.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <linux/input.h> #include "ts.h" #include "lvgl.h" #define TOUCH_DEVICE "/dev/input/event1" #define TOUCH_RANGE_X 4096 #define TOUCH_RANGE_Y 4096 static int touch_fd = -1; int ts_init(void) { touch_fd = open(TOUCH_DEVICE, O_RDONLY | O_NONBLOCK); if (touch_fd < 0) { perror("open touch device failed"); return -1; } return 0; } static void ts_read_cb(lv_indev_t *indev_drv, lv_indev_data_t *data) { struct input_event ev; static int last_x = 0; static int last_y = 0; static bool pressed = false; while (read(touch_fd, &ev, sizeof(ev)) > 0) { switch (ev.type) { case EV_ABS: if (ev.code == ABS_X || ev.code == ABS_MT_POSITION_X) { last_x = ev.value; } else if (ev.code == ABS_Y || ev.code == ABS_MT_POSITION_Y) { last_y = ev.value; } break; case EV_KEY: if (ev.code == BTN_TOUCH || ev.code == BTN_TOOL_FINGER) { pressed = ev.value; } break; default: break; } } >// 文件路径:src/ui/ui_main.c #include "lvgl.h" static lv_obj_t *status_label; static void switch_event_cb(lv_event_t *e) { lv_obj_t *sw = lv_event_get_target(e); bool is_checked = lv_obj_has_state(sw, LV_STATE_CHECKED); if (is_checked) { lv_label_set_text(status_label, "ON"); } else { lv_label_set_text(status_label, "OFF"); } } void ui_main_create(lv_obj_t *scr) { lv_obj_t *title = lv_label_create(scr); lv_label_set_text(title, "LVGL on Linux"); lv_obj_set_style_text_font(title, &lv_font_montserrat_24, 0); lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20); status_label = lv_label_create(scr); lv_label_set_text(status_label, "OFF"); lv_obj_align(status_label, LV_ALIGN_CENTER, 0, -30); lv_obj_t *sw = lv_switch_create(scr); lv_obj_add_event_cb(sw, switch_event_cb, LV_EVENT_VALUE_CHANGED, NULL); lv_obj_align(sw, LV_ALIGN_CENTER, 0, 30); lv_obj_t *btn = lv_button_create(scr); lv_obj_t *btn_label = lv_label_create(btn); lv_label_set_text(btn_label, "Click Me"); lv_obj_align(btn_label, LV_ALIGN_CENTER, 0, 0); lv_obj_align(btn, LV_ALIGN_BOTTOM_MID, 0, -20); }这里用LV_EVENT_VALUE_CHANGED监听开关变化,在回调中通过lv_obj_has_state判断当前是否处于选中状态。很多“switch 按下不变化”的问题,就是因为事件回调里没有重新获取状态,或者事件类型写成了LV_EVENT_CLICKED。
4.6 主循环与编译运行
main.c 的逻辑比较简单:
#include <stdio.h> #include <unistd.h> #include <pthread.h> #include "lvgl.h" #include "src/driver/fb.h" #include "src/driver/ts.h" #include "src/ui/ui_main.h" static uint32_t tick_get_cb(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint32_t)(ts.tv_sec * 1000 + ts.tv_nsec / 1000000); } int main(void) { if (fb_init() < 0) { return -1; } lv_init(); lv_tick_set_cb(tick_get_cb); lvgl_display_init(); lvgl_touch_init(); ui_main_create(lv_screen_active()); while (1) { lv_timer_handler(); usleep(5000); } fb_exit(); return 0; }CMake 配置参考如下:
cmake_minimum_required(VERSION 3.16) project(lvgl_demo C) set(CMAKE_C_STANDARD 99) add_subdirectory(lvgl) add_executable(lvgl_demo main.c src/driver/fb.c src/driver/ts.c src/ui/ui_main.c ) target_include_directories(lvgl_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_link_libraries(lvgl_demo PRIVATE lvgl pthread m dl)交叉编译时,使用 CMake 工具链文件:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm-linux-gnueabihf.cmake cmake --build build -j4编译出的可执行文件拷贝到开发板后,确认当前用户有/dev/fb0和/dev/input/eventX的读写权限,再运行:
./build/lvgl_demo如果屏幕正常出现界面,触摸可以点击按钮和开关,说明整个链路已经跑通。
5. 让界面更“丝滑”的关键优化
LVGL 本身是一个软件渲染引擎,性能瓶颈通常不在 LVGL 逻辑,而在显存拷贝、buffer 命中率和资源尺寸。下面几个优化点对流畅度影响最大。
5.1 使用双缓冲
单缓冲模式下,LVGL 正在写 buffer 的同时如果有硬件刷新屏幕,就可能出现撕裂或闪烁。使用双缓冲后,LVGL 可以在一个 buffer 里绘制下一帧,同时把另一个 buffer 的内容刷到屏幕,能明显减少画面闪烁。
在 9.x 中:
static lv_color_t buf_1[DISP_BUF_SIZE]; static lv_color_t buf_2[DISP_BUF_SIZE]; lv_display_set_buffers(disp, buf_1, buf_2, DISP_BUF_SIZE * sizeof(lv_color_t), LV_DISPLAY_RENDER_MODE_PARTIAL);注意DISP_BUF_SIZE不要过大,否则内存占用太高。通常可以设置为屏幕总像素数的 1/10 到 1/5,再根据实际运行情况调整。如果内存充足,也可以考虑整屏双缓冲。
5.2 颜色深度匹配
LVGL 的颜色深度必须和 Framebuffer 的颜色格式一致。如果 LVGL 使用 16 位 RGB565,Framebuffer 也是 RGB565,那么memcpy可以直接按行拷贝。如果两者不一致,LVGL 会在内部做颜色转换,CPU 开销会明显增加。
如果你的屏幕支持 RGB888,但 LVGL 用 16 位也能显示,建议还是按屏幕原生格式配置,避免不必要的转换。
5.3 图片和字体资源优化
小屏设备的内存有限,图片资源尽量不要直接放 PNG 或 JPEG 大图。LVGL 官方提供了在线图片转换工具,可以把图片转成 C 数组。转码时选择 RGB565 格式,能比 ARGB8888 少一半内存。
字体也一样。中文字库体积大,开发时如果只是临时显示几个字符,不需要一次性加载整个字库。可以使用字体工具按需生成字符集,只把用到的文字编进字库,能减少 ROM 和 RAM 占用。
例如在代码中:
lv_obj_t *label = lv_label_create(scr); lv_obj_set_style_text_font(label, &my_font_24, 0); lv_label_set_text(label, "你好,LVGL");my_font_24是通过工具生成的字库变量。字体制作的字符范围要包含你准备显示的文字,否则运行时会显示空白或乱码。
5.4 用性能监视器定位卡顿
LVGL 内置性能监视器,打开后可以在屏幕上显示渲染帧率、CPU 占用情况。在lv_conf.h中开启:
#define LV_USE_PERF_MONITOR 1开启后,屏幕角落会显示 FPS 和 CPU 占用。如果动画掉帧明显,先看 FPS,再定位是刷新回调慢还是 LVGL 绘制慢。实际项目中,常见卡顿原因有:
- flush 回调里做了阻塞式 I/O 或等待;
- 图片太大,软解压耗时;
- 脏矩形区域过大;
- 其他线程频繁抢占 UI 线程;
- 没有及时调用
lv_display_flush_ready。
5.5 使用 LVGL 时钟而不是自定义延时
很多开发者习惯在业务代码里用sleep或usleep做延时,这类阻塞会让lv_timer_handler()无法及时执行,UI 自然卡顿。建议把周期任务放到 LVGL Timer 中:
static void my_timer_cb(lv_timer_t *timer) { // 每 100ms 执行一次的任务 } lv_timer_create(my_timer_cb, 100, NULL);LVGL Timer 会在每次lv_timer_handler()执行时被检查,不会阻塞界面线程。
6. 常见问题与排查思路
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 屏幕黑屏或花屏 | Framebuffer 像素格式与 LVGL 颜色深度不匹配 | 检查LV_COLOR_DEPTH与fb_var_screeninfo.bits_per_pixel是否一致 |
| 中文显示为方块 | 字体未包含对应字符 | 生成包含目标字符的中文字库 |
| switch 按下没有状态变化 | 事件类型不对或未重新获取状态 | 使用LV_EVENT_VALUE_CHANGED,回调中调用lv_obj_has_state |
| 触摸点击位置不对 | 坐标范围未映射或方向反转 | 查看触摸设备原始范围,做线性映射和翻转 |
| 界面闪烁 | 单缓冲刷新与屏幕扫描冲突 | 改双缓冲,或优化 flush 拷贝方式 |
| 动画掉帧 | 图片解码慢、UI 线程被阻塞、刷新区域太大 | 预转图片为 C 数组,使用 LVGL Timer 替代 sleep |
| 某些平台出现卡死 | 缓存、DMA、任务栈或显示回调未正确完成 | 先关闭硬件加速,检查 flush 后是否调用 ready,再看任务栈大小 |
6.1 中文显示乱码或不显示
LVGL 默认字体只包含 ASCII 字符。要显示中文,必须启用或生成包含中文字符的字体。如果已经启用了中文字库,仍然显示空白,多半是 text 字库中没有对应的字。建议先从一个最小字符集开始,比如“你好,LVGL”,生成字库后测试,再逐步扩展。
6.2 switch 按下不变化
这个问题出现频率很高。先确认是否注册了事件回调:
lv_obj_add_event_cb(sw, switch_event_cb, LV_EVENT_VALUE_CHANGED, NULL);回调中必须读取最新状态:
bool checked = lv_obj_has_state(sw, LV_STATE_CHECKED);不要只依赖LV_EVENT_PRESSED或LV_EVENT_RELEASED。switch组件的业务逻辑主要体现在VALUE_CHANGED事件中。此外,如果 switch 被其他控件覆盖或放在了错误的父容器中,也可能导致点击事件不到 switch 本身,需要检查布局层级。
6.3 不同版本 API 差异
LVGL 9.x 和 8.x 的差异很多,下面是一些高频注意点:
lv_disp_drv_t在 9.x 中变为lv_display_t;lv_disp_flush_ready变为lv_display_flush_ready;lv_disp_buf_init在 9.x 中改为lv_display_set_buffers;lv_indev_drv_register在 9.x 中改为lv_indev_create+lv_indev_set_read_cb;- 9.x 的
lv_display_set_buffers第四个参数单位是字节,而 8.x 是像素数。
版本升级时,建议只参考同版本官方示例,不要混用。
7. 最佳实践与工程建议
7.1 代码组织与命名
建议把代码分为三层:
- 驱动层:负责 Framebuffer 和触摸设备,与 LVGL 只通过 flush_cb / read_cb 交互;
- 应用层:负责业务逻辑,比如传感器数据、网络状态、配置保存;
- 界面层:只创建控件、绑定事件,不直接操作硬件。
这样当屏幕从 4.3 寸换到 7 寸,或者触摸芯片换型号时,只需要改驱动层,界面代码基本不动。
7.2 内存与资源管理
LVGL 在 Linux 上通常使用LV_MEM_CUSTOM 1,走系统 malloc/free。虽然简单,但要注意创建大量控件后及时释放,页面切换时使用lv_obj_clean或者删除屏幕。长时间运行的设备要特别关注内存泄漏,可以定期查看/proc/meminfo或使用 Valgrind 检测。
图片资源尽量在编译期转成 C 数组,避免运行时解码。如果必须使用文件系统加载图片,也要注意文件读取速度对流畅度的影响。
7.3 权限与生产环境注意事项
访问/dev/fb0和/dev/input/eventX通常需要 root 权限。生产环境中不推荐直接使用 root 运行整个应用,更合理的做法是:
- 创建专门的业务用户;
- 使用 udev 规则,将视频和 input 设备组权限授权给业务用户;
- 或者只给可执行文件设置必要的 setcap 权限。
# 查看设备权限 ls -l /dev/fb0 /dev/input/event1如果设备权限不足,运行时会报Permission denied。开发阶段可以先临时用sudo运行,生产环境务必遵循最小权限原则,避免不必要的安全风险。另外,对屏幕和触摸设备的操作属于硬件层面,任何修改都建议先在测试环境验证,再部署到生产设备。
7.4 使用模拟器提升开发效率
如果你还在调试界面布局和交互逻辑,没有必要每次都在开发板上烧录。推荐先在 PC 上用 SDL2 模拟器跑同一套 LVGL 代码,调好 UI 后再通过交叉编译部署到开发板。这样可以显著缩短开发周期。
模拟器环境搭建后,界面代码可以作为独立模块复用,驱动层提供对应的模拟实现即可。
7.5 日志与调试开关
开发阶段打开 LVGL 日志和性能监视器。发布时再关闭,减少日志输出对性能的影响。
#define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_INFO #define LV_USE_PERF_MONITOR 1上线前记得把LV_USE_PERF_MONITOR关闭,否则会在屏幕上一直显示 FPS 和 CPU 占用。
8. 总结与下一步学习路线
这篇内容从 LVGL 的基本定位讲起,梳理了嵌入式 Linux 小屏设备上从 Framebuffer 显示到触摸输入、再到 UI 编写和性能优化的一整条链路。如果你能顺利跑通,说明你已经具备了把 LVGL 工程快速迁移到不同屏幕和开发板的能力。
接下来可以继续深入的方向包括:
- 学习 LVGL 的布局系统,比如 Flex 和 Grid,替代手写坐标;
- 掌握更多的动画 API,比如
lv_anim_t和lv_anim_timing; - 学习 LVGL 消息总线
lv_msg,让界面和业务模块解耦; - 研究 DRM/KMS 显示方案,比起 Framebuffer 更适合复杂图形场景;
- 了解 LVGL 的缓存刷新机制,在性能不足时进一步优化绘制路径。
如果你正打算在小屏 Linux 设备上做界面,建议先把本文第 2 节的模拟器环境搭起来,用一套代码先在电脑上跑通 UI,再去调真实的触摸屏和屏幕驱动。这样能少走不少弯路。如果文章对你有帮助,可以收藏备用,后续遇到具体问题也欢迎继续交流。