前言

最近一直在折腾 LICO 的硬件前端. 经历了很久的痛定思痛, 还是决定设计自己的纯硬件前端. 相对于手机 APP, 纯硬件前端可以不受 Android 系统严格的后台调度策略影响, 并且可以直接控制各种硬件, 简化控制流程. 独立的硬件并非没有缺点, 其电源管理完全比不上现代智能手机, 但我的需求是随时在线, 这种场景下使用更多的能源换来的服务是利大于弊的.

移动便携设备性能往往有限, 无法做到手机的性能, 具体地说是性能、功耗、功能不可能三角. 充分考虑了以上因素, 我决定使用 LVGL 显示 / 触摸方案. LVGL 是基于 C 的, 性能开销非常低, UI 美观丝滑, 体验非常好, 搭配树莓派 Zero 2W 这种小板非常适合便携设备. LVGL 官方也维护了一个 Linux 库, 可以很方便地在各种 Linux 设备上使用. 请参考此链接: lv_port_linux.

本文就是在这个项目的基础上, 记录在树莓派 Zero 2W 上安装编译 LVGL 的笔记, 顺便做一些针对性优化. (主要还是方便我回来查看, 这么多坑真的记不住)

我自己总结出来的流程, 大概是这样: 首先克隆仓库并安装相关依赖, 检查无误后, 修改基础设置以匹配你的显示屏. 然后使用默认配置文件编译 demo 示例, 确定基础运行条件. 验证可行性后, 可以修改默认的配置文件, 设计并编写你自己的 LVGL UI, 引入相关依赖, 使用默认字库和图标等, 取消编译默认 demo, 检测你自己的程序是否可用. 如果可用, 可以考虑定制字库和图标, 引入编译, 修改配置文件, 配置前后端通讯逻辑等. 最后是相关优化: 关闭光标以适配触摸屏, 关闭测试用的 HUD 信息小窗, 帧率和缓冲帧配置优化, 防烧屏等. 经过一系列交互与稳定性测试后就可以尝试部署了.

这一套整体流程下来其实思路并不复杂, 但其中有非常多的细节, 错一步就会影响别的配置, 很麻烦, 于是便有了这一篇笔记, 也供大家参考. 如果大家有更好的流程或方案, 请不吝赐教.

LVGL 的安装与编译

基本准备工作

首先, 我的硬件信息是这样的:

  • 树莓派 Zero 2W, Raspbian GNU/Linux 12 (Bookworm), 我使用的是官方 32 位的 Lite OS, 这个不是桌面版, 所以后续我们需要自己安装驱动

  • 一块 HDMI 触摸显示屏, 分辨率 800x480, 触摸信号通过 USB 传输, 走 EVDEV 协议回传触摸事件.

准备安装相关依赖:

# 1. 更新系统包列表和已安装的软件
sudo apt update && sudo apt upgrade -y
# 2. 安装编译所需的核心依赖
# - git:  用于下载 LVGL 项目
# - build-essential:  包含 gcc/g++ 编译器和 make
# - cmake:  我们 C 项目的构建系统
# - libevdev-dev:  evdev (触摸屏) 驱动所需的库
sudo apt install git build-essential cmake libevdev-dev -y

然后, 连接上显示屏和触摸用的 USB (复用电源), 重启:

sudo reboot

正常情况下, 你会看到树莓派开始输出启动日志, 跑得很快, 然后直到显示默认的登陆 tty 页面.

然后我们验证一下触摸屏的硬件有没有被识别:

# 这是一个成功的 lsusb 输出示例
lico@raspberrypi: ~ $ lsusb
# 这是我的触摸屏 (电容触摸屏 CTP)
Bus 001 Device 010:  ID 1a46: e5e3 QinHeng Electronics USB2IIC_CTP_CONTROL
# 这是我的 Hub
Bus 001 Device 008:  ID 1a38: 0101 Terminus Technology Inc. Hub
# 这是树莓派自己
Bus 001 Device 001:  ID 1d6b: 0252 Linux Foundation 2.0 root hub

如果不识别, 有可能是供电能力不足, 也有可能是数据线不对, 此问题请自行解决……

配置硬件权限

硬件被识别后, 我们的 C 程序 (lvglsim) 需要权限去读写这些硬件设备.

  1. 添加用户组:

    • video 组: 允许访问 Framebuffer (/dev/fb0).

    • input 组: 允许访问输入设备 (/dev/input/event*).

sudo usermod -a -G video $USER
sudo usermod -a -G input $USER
  1. 使权限生效:

    必须完全注销并重新登录(或直接 sudo reboot 重启)以使新的组权限生效.

  2. 验证权限:

    重新登录后, 运行 id 命令, 确保 video 和 input 出现在 groups= 列表中.

    lico@raspberrypi: ~ $ id
    # 输出应类似于: 
    # uid=1000(raspi) ... groups=... 44(video),  ... 108(input) ...
    

下载并编译 LVGL C 项目

没有安装好 git 的同学请安装好 Git.

然后:

# 1. 回到 home 目录
cd ~

# 2. 克隆项目
git clone https: //github.com/lvgl/lv_port_linux.git

# 3. 进入项目目录
cd lv_port_linux

# 4. 初始化并拉取 lvgl 核心库 (子模块会包含 LVGL, 这个要等一段时间)
#    此时自动安装的最新的 LVGL 我这里是 9.4 版本
git submodule update --init --recursive

# 5. 配置 CMake (关键步骤)
#    -B build:      指定编译目录为 'build'
#    -DCONFIG=default:  告诉 CMake 使用根目录里的默认配置, 默认就是 fbdev
cmake -B build -DCONFIG=default

# 6. 执行编译 (使用所有 CPU 核心)
cd build
#    如果你的开发板性能弱, 可以按需设置使用的 cpu 数量, 同时建议配置 SWAP
make -j$(nproc)

运行官方 Demo 与触摸交互

  1. 找出触摸屏设备号:

    lsusb 告诉我们触摸屏存在, evtest 告诉我们它是哪个文件.

    # 逐个测试 event 设备,  用手滑动会输出一大堆信息
    sudo evtest /dev/input/event0
    # (按 Ctrl+C 退出,  再试 event1,  event2...)
    sudo evtest /dev/input/event2
    

    当用手触摸屏幕时, 如果终端疯狂刷出 EV_ABS / ABS_X / ABS_Y 数据, 就说明我们找到了对应设备.

  2. 运行程序:

    使用环境变量 LV_LINUX_EVDEV_POINTER_DEVICE 来指定你的触摸屏.

    # 假设你的触摸屏是 /dev/input/event0
    LV_LINUX_EVDEV_POINTER_DEVICE=/dev/input/event0 ./bin/lvglsim
    

    此时, 我们应该能看到官方的 Demo, 并且可以用手触摸操作.

编写自己的 UI 代码

lv_port_linux 这个项目根目录有一个 src 文件夹, 我们可以在这个文件夹内编写自己的代码, 并修改已有的 main.c 使用我们自己定义的函数:

  • 在顶部添加 #include "src/my_ui.h"

  • 找到 main 函数中调用 Demo 的地方:

    /*Create a Demo*/
    lv_demo_widgets();
    lv_demo_widgets_start_slideshow();
    
  • 替换为你的函数:

    /*Create our own UI*/
    create_my_ui();
    

使用自己的字库

LVGL 默认字库对英文支持较好, 但对中文支持较差, 我们需要制作自己的字库. 这里我使用 思源简体 作为字库源文件, 然后我们使用官方的 Font Converter 字库转换工具制作我们要转换的字库.

我使用的是 Regular 字体, 然后 Bpp 选择为 1, 输出格式为 C File. C 文件使用速度最快, 集成也更好, 你也可以选择直接编译称为外部二进制 bin 字库, 这样就不用后续编译字库了, 但是外部加载会稍微慢一点.

对于 Fallback 选项, 这个是字体继承, 当中文内容中有要显示一些英文字符的情况时, 选择字体继承可以有效避免显示乱码. 这个根据你的英文字体名称来, 举例说明:

假如你的中文字体打算命名为 lico_16_zh, 这里 16 是字号, 那么你可以把你的对应字号的英文字体命名为 lico_16_en, 然后在制作中文字库时, 在 Fallback 那里填写 lico_16_en, 这样就配置好字体继承了, 你会分别得到一个 lico_16_en.c 和 lico_16_zh.c 文件. 按照这套流程, 分别制作出所有你需要的字号, 比如 16、18、20、24 等, 然后你可以把这些都放到你的 src 文件夹里, 新建一个 font 文件夹, 并写一个 font_manager.c 文件引入你的字体, 或者你也可以直接在 main.c 里管理, 但是不推荐.

此时, 我们如果我们不修改 CMakeLists.txt, 还是需要重复之前的操作——完全编译整个项目, 同时也会连带着编译 LVGL 自带的 Demo, 这很不优雅, 所以我们要深度优化这个项目的配置文件, 优雅地开发我们的 UI.

项目优化

因为我们在 src 文件夹引入了新的文件等, 我们需要修改根目录的 CMakeLists.txt 和其他配置文件, 保证能够正常编译, 同时做出相关优化.

创建子 CMakeLists.txt

简单概述一下思路: 我们会在原有工程里新增了一个独立的 CMake 子目录, 用来打包你的自定义 UI 和字体源码. 目录结构的关键部分如下(只列出和这次调整相关的片段):

顶层 CMakeLists.txt 会先 add_subdirectory(lvgl), 再 add_subdirectory(src/my_app), 最后在 add_executable(lvglsim …) 中只保留 main.c 和底层驱动源码, 然后把 my_app 这个静态库一起链接. 这样以后只要把新的页面或字体 .c 文件放进 ui 或 fonts, 重新执行一次 cmake -B build …, 库里就会自动包含这些文件, 而不需要再改顶层的 CMakeLists.txt.

这里有一个文件树示例作为参考:

src
├── fonts
│   ├── myfont_16.c
│   ├── myfont_18.c
│   ├── myfont_24.c
│   ├── myfont_30.c
│   ├── myfont_32.c
│   ├── myfont_en_16.c
│   ├── myfont_en_18.c
│   ├── myfont_en_24.c
│   ├── myfont_en_30.c
│   └── myfont_en_32.c
├── lib
│   ├── cjson/
│   ├── display_backends/
│   ├── indev_backends/
│   ├── ipc/
│   ├── backends.h
│   ├── driver_backends.c
│   ├── driver_backends.h
│   ├── gpio_button.c
│   ├── gpio_button.h
│   ├── mouse_cursor_icon.c
│   ├── simulator_settings.h
│   ├── simulator_util.c
│   └── simulator_util.h
├── my_app
│   └── CMakeLists.txt
├── ui
│   ├── font_manager.c
│   ├── font_manager.h
│   ├── my_ui.c
│   ├── my_ui.h
│   ├── page_about.c
│   ├── page_home.c
│   ├── page_settings.c
│   ├── pages.c
│   └── pages.h
└── main.c

然后我们编辑我们自己的 CMakeLists.txt:

# Collect custom application sources (UI pages,  fonts,  etc.)
# The CONFIGURE_DEPENDS flag lets CMake pick up new files on the next configure run.
file(GLOB MY_APP_SOURCES
    CONFIGURE_DEPENDS
    "${CMAKE_SOURCE_DIR}/src/ui/*.c"
    "${CMAKE_SOURCE_DIR}/src/fonts/*.c"
)

if(NOT MY_APP_SOURCES)
    message(WARNING "No sources found for my_app library. Check the glob patterns.")
endif()

add_library(my_app STATIC ${MY_APP_SOURCES})

# Expose include directories so other targets can include the UI headers without extra setup.
target_include_directories(my_app PUBLIC
    "${CMAKE_SOURCE_DIR}"
    "${CMAKE_SOURCE_DIR}/src/ui"
    "${CMAKE_SOURCE_DIR}/src/fonts"
)

target_link_libraries(my_app PUBLIC lvgl)

然后我们修改根目录的 CMakeLists.txt:

add_subdirectory(lvgl)
# 添加以下内容来设置子目录: 
add_subdirectory(src/my_app)
# 修改可执行文件和自定义库的目录成为这个亚子: 
add_executable(lvglsim
    src/main.c
    ${LV_LINUX_SRC}
    ${LV_LINUX_BACKEND_SRC}
)
target_link_libraries(lvglsim my_app lvgl_linux lvgl)

帧率优化

到这时候, 针对 CMakeLists.txt 的优化还没完, LVGL 默认帧率时 33 fps 满帧, 这个帧率放在 5202 年大抵是不大行的, 最起码也要 60. 我们将会使用自定义宏.

这里就要讲解一下机制, lv_port_linux 这个项目使用配置文件预生成对于 LVGL 的配置文件, 还记得我们最开始编译 demo 的时候的命令吗?我们当时用了 cmake -B build -DCONFIG=default 这个就是使用了根目录下的 lv_conf.defaults 文件, 理论上 config 文件夹里还有更多针对不同驱动的文件, 你可也以去修改哪些, 只是在预编译的时候记得使用对应的配置文件即可.

我们将简单修改 lv_conf.defaults 里的部分配置, 取消编译 demo 和一堆其他默认的库文件, 同时修改默认帧率到 60 fps, 然后再在 CMakeLists.txt 中设置自定义宏以应用这些配置.

在 lv_conf.defaults 中修改这些为 0 以取消编译 demo:

# Examples
LV_BUILD_EXAMPLES 0

# Demos
LV_BUILD_DEMOS 0
LV_USE_DEMO_WIDGETS         0
LV_USE_DEMO_KEYPAD_AND_ENCODER 0
LV_USE_DEMO_BENCHMARK       0
LV_USE_DEMO_RENDER          0
LV_USE_DEMO_STRESS          0
LV_USE_DEMO_MUSIC           0

另附性能调优配置 lv_conf.h:

// 刷新周期(ms)
#define LV_DEF_REFR_PERIOD 16  // ~60 FPS

// 双缓冲
#define LV_LINUX_FBDEV_BUFFER_COUNT 2
#define LV_LINUX_FBDEV_BUFFER_SIZE 60

// 渲染模式
#define LV_LINUX_FBDEV_RENDER_MODE LV_DISPLAY_RENDER_MODE_PARTIAL

// 多线程渲染(树莓派4推荐)
#define LV_USE_OS LV_OS_PTHREAD
#define LV_DRAW_SW_DRAW_UNIT_CNT 2

然后再修改根目录的 CMakeLists.txt:

# 首先找到这一行
message(STATUS "${LV_BUILD_CONF_PATH} generated successfully")
# 然后在这后面插入我们的自定义宏

# --- (开始) 插入自定义宏到 lv_conf.h 的指定位置 ---

# 1. 定义我们要插入的文本

set(CUSTOM_MACROS "/* --- 由 CMake 自动插入的自定义宏 --- */#define LV_DEF_TIMER_PERIOD 16#define LV_DISP_DEF_REFR_PERIOD 16/* --- 自定义宏结束 --- */")

# 2. 读取刚生成的 lv_conf.h 文件的全部内容

file(READ ${LV_BUILD_CONF_PATH} CONF_CONTENT)

# 3. 找到 #define LV_CONF_H 这一行, 并在它后面插入我们的文本

# 我们用 \n 来确保它换行

string(REPLACE "#define LV_CONF_H" "#define LV_CONF_H\n${CUSTOM_MACROS}" MODIFIED_CONF_CONTENT "${CONF_CONTENT}")

# 4. 把修改后的全部内容写回 lv_conf.h, 覆盖原文件

file(WRITE ${LV_BUILD_CONF_PATH} "${MODIFIED_CONF_CONTENT}")

# --- (结束) 插入自定义宏 ---

啊, 真的好麻烦, 但是我们就要成功了……

现在尝试一下完整编译我们的新项目:

cmake -B build -DCONFIG=default
cd build
make -j$(nproc)

如果没有报错的话, 运行一下试试:

# 假设你的触摸屏是 /dev/input/event0
LV_LINUX_EVDEV_POINTER_DEVICE=/dev/input/event0 ./bin/lvglsim

理论上你可以看到右下角小窗以 60 fps 运行了, 这样的话, 除了修改字库需要重新完整编译, 其余的都是增量编译, 不用再重新完整地编译了.

嗯, UI 很丝滑, 触摸屏很灵敏, 不枉我折腾这么久……

防烧屏修复

framebuffer 的机制很有趣, 它默认是被系统自带的 tty 占用的, 如果此时再加上一个 LVGL, 会导致显示叠加, 时间长了还会烧屏. 虽然这个可以通过关闭显示器来解决, 但能优化还是要尽量优化, 顺便把光标给关了:

首先转移 tty 服务:

# 停止tty1的登录服务
sudo systemctl stop [email protected]
sudo systemctl disable [email protected]

# 启用tty2
sudo systemctl enable [email protected]
sudo systemctl start [email protected]

这样的话启动日志仍然输出到 HDMI, 同时系统可调试.

然后永久关闭终端光标:

# 临时关闭
sudo sh -c 'TERM=linux setterm -cursor off > /dev/tty0'

# 永久关闭(添加到启动脚本)
sudo nano /etc/rc.local

在 exit 0 前添加:

# 关闭终端光标
TERM=linux setterm -cursor off > /dev/tty0

然后重启

sudo reboot

至此, 我们的 LVGL 新项目就基本配置优化完成了, 后续可以优化的还有: 关闭右下角 HUD 信息小窗、自定义图标、非线性动画、systemctl 部署、UDS / IPC 进程间通信等等……请大家自行探索吧, 73!