简介:针对嵌入式Linux平台下LVGL图形库的移植需求,这份docx文档提供了一套完整的操作指引,适合已具备ARM开发板基础、希望为设备快速添加图形界面的嵌入式工程师。文档从获取Gitee上的源码包讲起,逐步演示如何将两个定制目录放入源码目录、配置交叉编译工具链、修改根目录下的构建脚本并去除无关库,同时调整主函数入口以对接生成的界面逻辑。构建阶段详细说明了进入构建目录执行编译命令的完整流程,并针对显示驱动兼容性、内存管理、性能调优等常见落地问题提出排查思路,如内存紧张时可调整缓存或关闭非必需特效。整个资源包为单个docx文档,大小449KB,内容紧凑、要点突出,便于离线学习。资源发布至今已有3372人学习下载,是系统掌握LVGL在嵌入式Linux下适配流程并规避常见坑位的实用参考。
1. 嵌入式Linux上的LVGL移植:这份 cmake_lvgl8.1 资源解决的是什么
LVGL(LittlevGL)是老牌开源嵌入式图形库,想在一块 ARM Linux 开发板上跑出仪表盘或触摸菜单,通常要经历工具链配置、屏幕驱动对接、依赖裁剪和入口代码改造四个步骤。这份 cmake_lvgl8.1 打包文件把 LVGL 8.1 的框架源码、编译脚本和由 GUI Guider 生成的界面代码预先组织好,配合 custom 和 generated 两个目录,能少走不少从零搭工程的弯路。对刚接触嵌入式 GUI 的开发者、做边缘显示终端的运维集成场景,以及需要快速出界面原型的产品验证来说,它都算是一份可以照着复现的模板。下面就从实际拆解和上板踩坑的角度把它讲透。
2. 源码包结构:custom 目录与 generated 目录先补齐,界面才能上屏
拿到打包文件并解压后,第一眼往往是一片 CMakeLists.txt 和 lvgl 相关源码目录。很多人习惯性先打开根目录的 CMake 文件修改交叉编译器,结果编译报错满天飞,原因就是缺了 custom 和 generated 这两个目录。先看结构,再动手改配置,才是正确顺序。
2.1 custom 目录是什么,为什么少了它编译直接失败
从项目正文的描述看,custom 目录里装的是不属于 LVGL 官方仓库的自定义代码,常见内容是板级显示驱动、输入设备驱动(如触摸屏、鼠标)、自定义控件或者硬件初始化函数。generated 目录则是由 GUI Guider 这类可视化工具自动生成的界面代码,文件名类似gui_guider.h、events_init.h,把你在画布上拖出来的页面、坐标、样式和事件回调都固化成了 C 源码。
这里有一个常见的误区:#include "src/generated/gui_guider.h"并不是标准 LVGL 头文件,而是图形生成器产出的。如果只下载了官方 LVGL 仓库而没有把 GUI Guider 生成的generated文件夹放进去,编译到 main 函数时就会看到fatal error: src/generated/gui_guider.h: No such file or directory。所以解压打包文件之后,首先要做的不是修改编译器,而是对照资源描述,确认custom和generated是否已经出现在 src 目录下。
我一般会先把整个目录树划出三层:
| 目录或文件 | 职责 | 是否必须 |
|---|---|---|
| lvgl/ | LVGL 8.1 官方核心源码 | 必须 |
| src/custom/ | 板级自定义驱动与配置 | 必须,缺了功能不完整 |
| src/generated/ | GUI Guider 生成的界面代码 | 必须,缺了无法编译 |
| CMakeLists.txt | 构建脚本,控制编译链与源文件列表 | 必须 |
如果仓库里没有 src/custom,也可以自己新建,把 LCD 初始化、背光 GPIO 控制、触摸屏 I2C 读取这些底层函数放进去。常见做法是仿照 custom 目录内已有的lv_custom_draw.h和lv_custom_draw.c,在里面注册显示设备驱动和输入设备驱动。对于新手,这一步建议保持原样,只确认目录存在即可。
2.2 generated 目录里三个头文件的分工
打开src/generated目录后,通常会看到gui_guider.h、events_init.h、setup_scr_xxx.h等文件。gui_guider.h定义了全局 UI 容器结构体lv_ui,里面有指向各个页面的屏幕对象指针,比如screen_main、screen_setting。events_init.h声明了事件初始化函数,它负责把按钮点击、滑块滑动等动作绑定到回调函数。setup_scr_xxx.h则负责在运行时构建页面元素,比如设置标签文本、图片路径和动画起点。
这三个文件之间的关系是:UI 容器负责持有对象,setup 函数负责创建和配置对象,事件函数负责把交互逻辑挂到对象上。在 main 函数里依次调用:
lv_ui guider_ui; setup_ui(&guider_ui); events_init(&guider_ui);这三行代码的执行顺序不能调换。setup_ui必须执行完,页面控件才被创建出来,events_init才能绑定事件;如果事件初始化提前,就会出现“回调对象为空”的段错误,或者控件完全没有反应。这一点在移植后第一次运行触摸功能时尤其明显。
3. 交叉编译链设置:工具链路径与 CMakeLists.txt 的几处修改点
嵌入式 Linux 移植的核心矛盾是“开发机上编译、开发板上运行”,因此必须用到交叉编译工具链。打包文件里的 CMakeLists.txt 虽然预置了工具链变量,但每个人的交叉编译器路径不同,照搬一定会出问题。这一章把修改点拆成路径修改、子目录同步和依赖裁剪三块逐步解决。
3.1 修改交叉编译工具链路径:根目录和子目录都要看
先找到项目根目录下的 CMakeLists.txt,通常存在类似下面的段落:
set(CMAKE_C_COMPILER /opt/toolchain/arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER /opt/toolchain/arm-linux-gnueabihf-g++) set(CMAKE_SYSROOT /opt/toolchain/arm-linux-gnueabihf)这里的路径必须指向你自己的交叉编译器实际安装位置。常见的工具链名称有arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc,后缀hf表示硬浮点,如果你的板子 CPU 支持硬件浮点但用了非hf版本,运行时通常会出现Illegal instruction错误。可以用下面命令确认编译器路径:
which arm-linux-gnueabihf-gcc如果系统里没有输出,说明工具链还没安装,或者没有加入 PATH 环境变量。我一般会把工具链的 bin 目录加到~/.bashrc:
export PATH=/opt/toolchain/bin:$PATH修改完成后再执行source ~/.bashrc。要注意的是,原文档里提到“每个 CMakeLists.txt 有设置到工具链的都要修改”,这一点在项目里是真实存在的。LVGL 工程为了让各个模块能单独编译,常在 lvgl 子目录、custom 子目录甚至 generated 目录下放置独立的 CMakeLists.txt。如果某个子目录里单独指定了编译器,即使根目录已经改好,那一个文件仍然会拖慢整体编译速度,甚至导致“找不到 stdio.h”这类奇怪报错。可以先全局搜索:
grep -n "CMAKE_C_COMPILER" -r ./CMakeLists.txt确保所有出现的路径都已替换。
3.2 删除冗余库:PC 模拟器专用库怎么识别
项目正文特别提到“这两个的在 pc 模拟时才用到的”,指的是 LVGL 的 PC 模拟器依赖,常见包括 SDL2、pthread、libpng、libjpeg 等,用于在开发机上弹出窗口模拟显示。实际嵌入式板子上没有这些库,链接时会直接报undefined reference to SDL_Init或者cannot find -lSDL2。
解决方式是在根 CMakeLists.txt 中查找以下标记:
find_package(SDL2 REQUIRED) target_link_libraries(lvgl SDL2 SDL2main)找到后注释掉或者删除。如果不想改动太多,也可以保留 find_package 调用但在嵌入式编译时不安装 SDL2,让 CMake 配置阶段失败后回退条件分支。更推荐的做法,是直接在 CMakeLists.txt 里加上平台判断:
if(NOT DEFINED ENV{LVGL_SIMULATOR}) message(STATUS "嵌入式板卡模式: 不链接 SDL2") else() find_package(SDL2 REQUIRED) endif()这样同一份源码既能保证可追溯性,又能在需要模拟调试时通过环境变量重新开启。
文件中其他常见的不必要依赖还有lv_drivers/display/monitor.c、lv_drivers/indev/mouse.c,它们实际依赖 SDL2 的窗口坐标体系。如果打包文件里包含lv_drivers,建议在 CMake 源文件列表中直接剔除 monitor 和 mouse 编译源,保留 fbdev 和 evdev,这样才能匹配真实显示屏和触摸设备。
4. 主程序与屏幕参数:main 函数三行初始化加分辨率匹配
编译环境改完只是“条件具备”,真正决定画面能不能显示的核心在 main 函数和屏幕参数上。这一章给出可抄直接用的代码块,并解释每一行的作用,以及改分辨率时最不容易注意到的两个位置。
4.1 main 函数改造:从裸循环到 GUI Guider 界面启动
原工程里如果 main 函数还没有接入 LVGL,第一步就是把 LVGL 的核心头文件和生成器头文件全部包含进来。下面是一段可直接参考的完整入口代码:
#include "lvgl/lvgl.h" #include "src/generated/gui_guider.h" #include "src/generated/events_init.h" lv_ui guider_ui; int main(void) { lv_init(); // 初始化显示驱动,flush_cb 是你的 LCD 刷新回调函数 static lv_disp_draw_buf_t draw_buf; static lv_color_t buf1[800 * 100]; static lv_color_t buf2[800 * 100]; lv_disp_draw_buf_init(&draw_buf, buf1, buf2, 800 * 100); // 这里应注册你的显示驱动,具体函数指针由 custom 目录实现 // 初始化输入设备驱动,对应触摸屏或鼠标 // lv_indev_drv_t 注册 touchpad 或 mouse setup_ui(&guider_ui); events_init(&guider_ui); while (1) { lv_task_handler(); // 周期处理 LVGL 任务 usleep(5000); } return 0; }这段代码的核心是lv_init()之后的显示缓冲和 LVGL 任务循环。lv_task_handler是 LVGL 的心跳函数,它负责处理屏幕刷新、动画推进、输入事件分发等全部周期任务。没有这个循环,界面会停留在初始化完成的那一帧,不刷新、不响应触摸。usleep(5000)是常见取法,表示每 5 毫秒调用一次任务,习惯上把任务周期保持在 1ms 到 10ms 之间即可,具体数值可以按你的 CPU 负载调整。
lv_ui guider_ui是整个界面的根容器,setup_ui(&guider_ui)会初始化guider_ui内部的screen_main、screen_setting等屏幕指针,并为它们创建页面上的全部控件。紧接着的events_init(&guider_ui)对guider_ui中已经创建完成的控件做事件绑定,例如触屏点击后跳转到另一页。需要特别留意的是,这一步使用的是guider_ui的地址,事件回调内部通常会取出guider_ui.screen_main->btn_start等对象,如果两者不匹配,事件回调会拿到一个空指针。
// events_init.c 内部典型的回调绑定逻辑 static void btn_start_event_handler(lv_event_t *e) { lv_ui *ui = lv_event_get_user_data(e); if (ui->screen_setting != NULL) { lv_scr_load(ui->screen_setting); } }事件回调里大量使用了lv_event_get_user_data来获取lv_ui指针,所以 main 函数里向events_init传入的必须是同一个guider_ui地址。一旦地址传错,页面看起来正常,但点击任何控件都没有响应。这也是后面避坑章节要再次强调的重灾区。
4.2 屏幕分辨率修改:从 LV_HOR_RES 到 flush 行宽
嵌入式 Linux 下的显示屏通常通过 Linux 帧缓冲(/dev/fb0)或 DRM 接口驱动。不同开发板屏幕尺寸不同,比如 800x480、1024x600 或 1920x1080。修改屏幕像素时,光在lv_conf.h里改宏定义是不够的,还要注意缓存数组大小与刷新回调的映射。
lv_conf.h中对应的分辨率宏一般长这样:
#define LV_HOR_RES_MAX 800 #define LV_VER_RES_MAX 480#define LV_COLOR_DEPTH 16这两个宏告诉 LVGL 屏幕宽度、高度和颜色深度。LV_COLOR_DEPTH决定一个像素占几字节,16 位色深下每个像素占 2 字节,32 位色深占 4 字节。显存行宽等于LV_HOR_RES_MAX乘以每像素字节数。如果开发板 LCD 实际是 24 位色,而 LVGL 配置为 16 位,画面会出现偏色或条纹。
刷新回调函数flush_cb是另一个踩坑点。它的实现通常如下:
void my_disp_flush(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p) { uint32_t w = lv_area_get_width(area); uint32_t h = lv_area_get_height(area); // 实际写入 framebuffer 的偏移计算 lcd_fb_write(area->x1, area->y1, w, h, color_p); lv_disp_flush_ready(drv); }很多人把分辨率改成了 1920x1080,但flush_cb内部仍然是按 800x480 计算偏移地址,结果屏幕上出现错位的色块。修改分辨率时,一定要同步确认驱动层的坐标范围,不能只看lv_conf.h。我一般会检查显示屏规格书的数据手册,确认行场同步参数与 Linux 内核 DTS 里的disp_mode一致,之后再回 LVGL 层确认宏定义。
4.3 构建命令执行顺序:为什么必须 rm -rf build
项目正文给出了完整的构建流程。把它拆成几步看:
cd ../ rm -rf build mkdir build cd build/ cmake .. make -j12这里rm -rf build的优先级容易被低估。CMake 会把编译器和路径配置缓存进build/CMakeCache.txt,如果你先前用另一个工具链或分辨率配置构建过,残留的缓存会覆盖新配置,导致修改不生效。比如改过CMAKE_C_COMPILER后不删除 build 目录,cmake 可能会提示你在 CMakeCache.txt 里仍然找到旧编译器,并让你手动确认。这也是嵌入式 Linux 移植过程中最常见的“改完配置不管用”原因。
cmake ..会读取上一级目录的 CMakeLists.txt,并生成 Makefile。make -j12中的数字 12 表示并行编译线程数,如果你的服务器 CPU 是 8 核,建议改成-j8;如果-j数字远大于核心数,反而会因为内存不足导致编译进程被 killed。用nproc命令可以直接看到可用核心数:
nproc编译完成后,可执行文件位于build/bin目录下,这个路径就是接下来要上传到开发板的文件。上传方式可以用 FTP、SSH 或挂载 TFTP,但没有统一说法,一般更推荐 SSH 的scp直接拷贝。
5. 移植避坑清单:白屏、链接错误和触控失灵的五种典型案例
LVGL 移植过程中真正耗时间的,不是读源码而是排错。下面是多年板端项目中遇到频率最高的五种问题,每条按现象、原因、解决顺序说明。
5.1 现象:编译报错undefined reference to 'lv_ui'或找不到gui_guider.h
原因:generated 和 custom 目录没有正确加入 CMake 源文件列表。LVGL 在编译时只处理 CMakeLists.txt 中列出的源文件,如果你只把目录放到了磁盘上但没有让构建系统知道,头文件仍然无法被引用。解决:打开根 CMakeLists.txt,查找到类似target_sources(lvgl PRIVATE的位置,确认src/generated/gui_guider.c、src/generated/events_init.c等文件都在列表里。如果没有,用相对路径追加:
target_sources(lvgl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/gui_guider.c ${CMAKE_CURRENT_SOURCE_DIR}/src/generated/events_init.c )改完重新执行构建命令,且必须加上rm -rf build。
5.2 现象:make 链接阶段报cannot find -lSDL2或undefined reference to SDL_Init
原因:CMakeLists.txt 里仍然链接了 PC 模拟器版本的图形库。嵌入式板子上没有 SDL2 运行库,链接器自然找不到。解决:按照 3.2 节的方法注释或删除 SDL2 相关依赖,然后重新构建。同时可以去lv_conf.h检查LV_USE_GPU相关选项,如果板子没有 GPU 模块,建议把LV_USE_GPU关闭,从源头减少对上游图形库的依赖。
5.3 现象:程序能跑但屏幕持续白屏或只有左上角有一小块色块
原因:分辨率宏定义与显示驱动写入范围不一致。常见情况是 Linux 内核 DTS 配置的 LCD 分辨率是 800x480,但lv_conf.h仍然保持默认 320x240,LVGL 认为屏幕很小,只在左上角画了一片;另一种原因是 flush_cb 没有处理脏矩形区域,直接写全屏缓存导致撕裂。解决:先把 DTS 或内核 cmdline 的video=参数与屏幕规格书对比,确认实际分辨率。然后同步修改LV_HOR_RES_MAX、LV_VER_RES_MAX,再让flush_cb严格按area参数传入的坐标区域做局部刷新。简便验证方法是把my_disp_flush里直接打印area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area),观察是否和屏幕矩形吻合。
5.4 现象:触摸屏完全没反应,但鼠标可以操作
原因:input 设备初始化时,LVGL 被注册成了鼠标类型,或者触摸屏用的是 /dev/input/eventX 但驱动层没有正确识别。GUI Guider 自动生成的事件回调绑定的是点按事件,如果驱动认为指针类型不对,点击事件无法触发。解决:确认开发板的触摸设备节点,通常可在串口执行:
cat /proc/bus/input/devices看触摸屏对应的事件编号。随后在 main 函数中注册输入设备时要使用LV_INDEV_TYPE_POINTER,并把read_cb函数中读取到的坐标按屏幕分辨率做归一化,不要直接当作绝对坐标写入。还有一点容易被忽略:触摸屏的坐标方向和屏幕显示方向可能不同,需要根据实际情况做 x/y 交换或翻转。
// read_cb 示例:把触摸屏坐标映射到屏幕坐标 bool touch_read(lv_indev_drv_t *drv, lv_indev_data_t *data) { int rx, ry; read_touch_point(&rx, &ry); // 读取原始坐标 >static uint32_t frame_count = 0; static uint64_t last_ms = 0; while (1) { lv_task_handler(); frame_count++; uint64_t now = lv_tick_get(); if (now - last_ms >= 1000) { printf("fps = %" PRIu32 "\n", frame_count); frame_count = 0; last_ms = now; } usleep(5000); }内存占用则看 LVGL 的堆内存管理。打开LV_MEM_CUSTOM后,LVGL 会改用系统 malloc,当 UI 对象较多时,动态内存碎片可能成为隐患。我一般会在setup_ui之后立即打印一次空闲内存:
size_t free_mem = lv_mem_get_free_size(); size_t total_mem = lv_mem_get_size(); printf("LVGL mem free: %d / %d bytes\n", free_mem, total_mem);如果 free 剩余低于总内存的 20%,就需要精简页面控件或图片资源。LVGL 8.1 中,对象控件数量也不是无限的,打开任务监视器检查是否有控件泄漏:
lv_task_debug();检查过程中特别要注意生成的图片资源是否以外部引用的方式加载。嵌入式 Linux 环境下,可能存在 LCD 显存与 LVGL 显存混用的场景,常见做法是把buf1和buf2分配到连续物理内存,使用 CMA 或 hugepage 区域,这样刷新效率更高,也不会因为虚拟内存反复换页导致画面闪烁。资源上传到板子之后,如果发现显示方向不对,优先检查的是LV_USE_GPU和lv_disp_drv->rotated,不要先改代码。
从那以后,我每次移植 LVGL 都会强制走一遍这套流程:查看目录结构,全局搜索工具链路径,删除 PC 模拟库,改完配置先清理 build 目录,烧录后第一件事不是看界面,而是打印CAT /proc/fb和fbset判断帧缓冲位深。每项检查都写成一份几行的简表,下次遇到新板卡只需要几分钟就能定位问题。希望帮到你。
本文还有配套的精品资源,点击获取