第15章:集成与多平台部署

一次编写,处处运行——从 PC 模拟器到量产硬件,都拿证据说话

⚖️

本章导师:包青天

核心方法论:铁面无私,一切以仓库里的真实文件为准

「集成这条路,最怕的不是复杂,而是『想当然』。哪个平台该用哪个文件、哪个宏对应哪个开关,都得回到仓库里逐条对证。本官断案讲证据,你集成也要讲证据——把每条路径、每个文件都核实过,部署时才能铁面无私、胸有成竹。」

15.1 CMake:add_subdirectory 与 lvgl 目标

CMake 是 LVGL 官方首选的集成方式,入口就是仓库根的 CMakeLists.txt——文件开头的 cmake_minimum_required(VERSION 3.12.4) 写明了最低版本要求。集成方式简单到出乎意料:在你的工程里 add_subdirectory(lvgl),LVGL 就会暴露出一个名为 lvgl 的 CMake 目标,你的可执行文件只要 target_link_libraries 到它即可。

# 你的应用工程 CMakeLists.txt
cmake_minimum_required(VERSION 3.12.4)
project(my_ui C)

add_subdirectory(lvgl)                      # LVGL 源码目录
target_link_libraries(my_ui PRIVATE lvgl)    # 链接 lvgl 目标

这个 CMakeLists.txt 还有个"见招拆招"的分流逻辑:一进来就判断宿主环境——检测到 ESP_PLATFORM 就自动 include env_support/cmake/esp.cmake(ESP-IDF 专用);检测到 MICROPY_DIR 就 include env_support/cmake/micropython.cmake(MicroPython 绑定构建);其余普通工程统一走 env_support/cmake/main.cmake。也就是说,同一份 CMakeLists 会根据宿主自动切换集成策略,不需要你手动选。

# CMakeLists.txt(节选)
if(ESP_PLATFORM)
  include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/esp.cmake)
elseif(MICROPY_DIR)
  include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/micropython.cmake)
else()
  include(${CMAKE_CURRENT_LIST_DIR}/env_support/cmake/main.cmake)
endif()

如果你想把 LVGL 安装到系统、或作为第三方包分发,仓库同样备好了路:env_support/cmake/lvglConfig.cmake.in 会生成 CMake 包配置(配合导出的 lvglTargets.cmake),让下游工程可以 find_package(lvgl)lvgl.pc.in 则是 pkg-config 用的描述文件。至于老式构建(Make、lvgl.mk、SConscript),README 明确说了仍受支持——LVGL 在"怎么编译进去"这件事上从不挑食。

文件作用
CMakeLists.txt构建入口,暴露 lvgl 目标
env_support/cmake/main.cmake普通工程集成(配置路径、Kconfig 选项)
env_support/cmake/esp.cmakeESP-IDF 环境集成
env_support/cmake/micropython.cmakeMicroPython 绑定集成
env_support/cmake/lvglConfig.cmake.in生成包配置,支持 find_package(lvgl)
env_support/cmake/lvgl.pc.inpkg-config 描述文件

15.2 env_support:一张集成地图

env_support/ 是整个仓库的"平台适配层",按宿主分类排布:cmake/(CMake 辅助脚本)、esp/(ESP 相关)、kconfig/(Kconfig 片段,如 Kconfig.buildKconfig.compiler)、qnx/(QNX)、rt-thread/(RT-Thread)、pikascript/(PikaScript)、cmsis-pack/(ARM CMSIS-Pack)、debian/(Debian 打包)。README 说 Make、CMake、简单通配符编译都支持——这里就是各类构建与平台的具体落点。

# env_support/ 目录地图
env_support/
├── cmake/          CMake 辅助脚本(main/esp/micropython/kconfig/...)
├── esp/            ESP 相关(rlottie 等)
├── kconfig/        Kconfig 片段(Kconfig.build / Kconfig.compiler)
├── qnx/            QNX 平台
├── rt-thread/      RT-Thread 平台
├── pikascript/     PikaScript
├── cmsis-pack/     ARM CMSIS-Pack
└── debian/         Debian 打包

其中 cmake/ 子目录最值得逐文件读一遍。看看 main.cmake 提供的选项,就能理解 LVGL 的构建有多"可配置":LV_BUILD_CONF_PATH 让你指定 lv_conf.h 的位置;LV_BUILD_USE_KCONFIG 决定是否走 Kconfig;LV_BUILD_DEFCONFIG_PATHLV_BUILD_DOTCONFIG_PATH 用于 Kconfig 的默认配置与现有 .config。还有 dependencies.cmake,它按 find_package → pkg-config → FetchContent 的顺序依次尝试解析可选依赖,找不到就自动拉源码编译。

# env_support/cmake/main.cmake(节选)
set(LV_BUILD_CONF_PATH "" CACHE PATH
    "Use this to specify the location of and/or filename of lv_conf.h")
option(LV_BUILD_USE_KCONFIG "Use Kconfig" OFF)
set(LV_BUILD_DEFCONFIG_PATH "" CACHE PATH
    "Supply the default Kconfig configuration - used with Kconfig")
option(LV_USE_FIND_PACKAGE "Resolve dependencies via find_package" ON)

读这份地图的价值在于:以后遇到"我在某某平台集成 LVGL",第一反应不该是去搜博客,而是先看 env_support/ 下有没有对应的宿主目录,再顺着它找到具体的 cmake 脚本。仓库已经把答案按目录摆好了——包青天的方法就是:先翻证物,再下结论。

15.3 主流平台:ESP-IDF / Arduino / PlatformIO / Zephyr / MicroPython

先看 ESP-IDF。仓库根的 idf_component.yml 让 LVGL 可以直接作为 ESP-IDF 组件被引用(README 的链接指向 components.espressif.com 上的官方组件,通过 idf.py add-dependency 引入);component.mk 则是旧版 make 构建的遗留物。配合上一节说的 env_support/cmake/esp.cmake,在 idf_component_register() 里注册源文件与头文件,即可把整个 src/ 编进去。

# idf_component.yml(仓库根,节选)
description: LVGL - Light and Versatile Graphics Library
url: https://lvgl.io/
repository: https://github.com/lvgl/lvgl.git
documentation: https://docs.lvgl.io/
issues: https://github.com/lvgl/lvgl/issues

再看 Arduino 与 PlatformIO。仓库根同时放着 library.json(PlatformIO 的清单,声明 "frameworks": "*""platforms": "*"、license 为 MIT)和 library.properties(Arduino IDE 的清单,architectures=*includes=lvgl.h)。也就是说,两个生态的包管理器都直接认识这个仓库,开箱即用。这里有个值得留意的细节:这两份元数据里写的版本号是 9.5.0,略滞后于源码自身的 9.6.0-dev——注册表的版本与 dev 分支不同步,是常见且正常的事。

Zephyr 则是标准的 module 集成:仓库根的 zephyr/ 目录声明它是一个 Zephyr module。zephyr/module.yml 写着模块名 lvgl、启用 CMake 扩展、并把 Kconfig 入口指回 zephyr/Kconfig;而 zephyr/Kconfig 的内容只有三行——if LVGLrsource "../Kconfig" 回到根配置。这样在 Zephyr 工程的 prj.conf 里打开 CONFIG_LVGL=y,整个 LVGL 的菜单就都接进来了。

# zephyr/module.yml
name: lvgl
build:
  cmake-ext: True
  kconfig: zephyr/Kconfig

# zephyr/Kconfig(全文)
if LVGL
rsource "../Kconfig"
endif

MicroPython 在仓库里不是顶层目录,但集成点是实实在在的:env_support/cmake/micropython.cmake 把 LVGL 编成一个 lvgl_interface 接口库(INTERFACE),供上层绑定库链接;内存后端 src/stdlib/micropython/lv_mem_core_micropython.c 让 LVGL 直接从 MicroPython 堆上分配。官方 MicroPython 绑定(lv_bindings/lv_micropython)生成的 lv_mp.c 等胶水文件,正是挂在这个接口库之上。

类别README「Pre-integrated」列出的平台
芯片厂商ESP32、NXP MCUXpresso 组件、Renesas FSP、STM32
RTOSZephyr、NuttX、RT-Thread
框架Arduino、PlatformIO、CMSIS-Pack
板卡厂商Seeed Studio、Elecrow、Riverdi、VIEWE 等
包青天提示

判断一个平台"预集成"到什么程度,别只看 README 的宣传,要看三个信号:有没有对应的 env_support/ 宿主目录、有没有包管理器清单(idf_component.yml / library.json / library.properties)、有没有模块声明(zephyr/module.yml)。三者占全的,才是"真·开箱即用"。

15.4 桌面模拟器与真实硬件部署

在动手画板子之前,先在 PC 上把 UI 跑起来是迭代最快的路径。LVGL 把桌面窗口驱动直接做进了库:src/drivers/ 下的 sdl/x11/wayland/ 提供三种窗口后端。用 SDL 时,lv_sdl_window_create() 打开一个窗口并返回 lv_display_t *lv_sdl_mouse_create() / lv_sdl_keyboard_create() 接上鼠标键盘;Wayland 下对应 lv_wayland_window_create()。README 的 "Built-in drivers" 一节把这三者列为 Simulator / desktop 类别——开发期主力。

/* 桌面模拟器:SDL 窗口 + 输入 */
lv_display_t * disp = lv_sdl_window_create(800, 480);
lv_indev_t * mouse = lv_sdl_mouse_create();
lv_indev_t * kbd   = lv_sdl_keyboard_create();

while(1) {
    uint32_t t = lv_timer_handler();   /* 让 LVGL 处理计时器/刷新/事件 */
    lv_delay_ms(t);                     /* 睡到下一个计时器到期 */
}

从模拟器过渡到真实硬件,要核对的无非几件事:一是分辨率与面板一致(lv_display_create 的参数就是显示分辨率);二是缓冲大小与色深匹配(16 位色深每像素 2 字节,缓冲 = 宽 × 高 × 字节数 × 缓冲份数);三是主循环按周期喂 lv_timer_handler();四是排查问题时打开 LV_USE_LOG。除了窗口驱动,README 的驱动清单还覆盖了嵌入式 Linux 的 fbdev(framebuffer)、DRM/KMS、libinput/evdev,以及 ILI9341、ST7789 这类常见 LCD 控制器。

工程层面,仓库根部的 CMakePresets.json 给了两套现成的构建预设:linux-baseNinja Multi-Config 生成器,windows-baseVisual Studio 17 2022cl.exe,输出目录统一是 ${sourceDir}/build/${presetName},还带 debug/release 变体(linux-base_dbg / linux-base_rel 等)与 Kconfig 变体(linux-kconfig)。配合第 14 章见过的 configs/defconfigs/sdl2.defconfiglinux.defconfigdrm.defconfig),从"桌面验证"到"目标机裁剪"就串成了一条可复制的流水线。

# CMakePresets.json(节选)
{
  "name": "linux-base",
  "generator": "Ninja Multi-Config",
  "binaryDir": "${sourceDir}/build/linux-base"
},
{
  "name": "windows-base",
  "generator": "Visual Studio 17 2022",
  "binaryDir": "${sourceDir}/build/windows-base"
}
阶段用什么解决什么
开发预览SDL / X11 / Wayland 驱动不碰硬件快速迭代 UI
构建预设CMakePresets.json统一桌面端的编译环境
目标裁剪configs/defconfigs/按目标机关闭功能、定色深
量产部署面板驱动 + lv_timer_handler() 主循环分辨率/缓冲/刷新全部对齐

15.5 LVGL Pro 一键生成工程

如果你觉得"手动搭工程 + 写 C"还是太重,LVGL Pro 提供了第三条路。README 的 "In LVGL Pro" 小节原话是:In LVGL Pro you can create ready-to-use UI-only, VSCode, Zephyr, and Linux projects with a single click——也就是说,在设计器里画完界面,导出的不只是纯 C 代码,而是带好构建系统的完整工程:UI-only、VSCode、Zephyr、Linux 四种模板,一次点击生成。

这与本工作区的 LVGL_Pro_CLI-2.0.2-rc1-darwin/ 工具一一对应。看仓库里 examples/xml_project/ 的结构就能明白 XML 工程长什么样:project.xml(工程文件)、globals.xml(全局配置)、images/(图片资源)、fonts/(字体资源)各司其职。LVGL Pro 把这份 XML 翻译成 C,再套进你选的目标模板,编译就是全自动的。

# examples/xml_project/ —— LVGL Pro XML 工程布局
xml_project/
├── project.xml    工程描述
├── globals.xml    全局配置
├── images/        图片资源
└── fonts/         字体资源
# README.md —— In LVGL Pro(节选)
In LVGL Pro you can create ready-to-use UI-only, VSCode, Zephyr, and
Linux projects with a single click.

把这条路与本系列串起来看:手写 C(前面所有章节)+ CMake/平台集成(本章)+ LVGL Pro 一键工程化,三者是互补而非互斥。小 demo 手写最快;要交付一个完整产品,先让工具把工程骨架生成好,再在需要的地方手写微调——这才是现代嵌入式 UI 开发的常态分工。包青天最后提醒一句:无论走哪条路,动手前把目标平台的芯片手册、面板时序、以及仓库里对应的驱动文件都核实一遍,"证据齐了,才能下判决书"。

注意

仓库内 src/drivers/ 下的头文件大多只是"转发头"(顶部带 #warning 提示已废弃),真正的新头文件在 include/lvgl/drivers/。写驱动代码时请包含 include/lvgl/drivers/sdl/lv_sdl_window.h 这类路径,或直接 #include "lvgl/lvgl.h"

章末练习

练习 1:最小的 CMake 集成 入门

写出把一个应用集成到 LVGL 所需的最小 CMakeLists.txt(至少包含版本声明、工程声明、add_subdirectory 与链接)。指出你链接的 CMake 目标名,并说明为什么不需要手动列 src/ 下的全部源文件。

提示

照抄 15.1 的第一个代码块,再把 CMake 最低版本换成仓库要求的 3.12.4。目标名在 CMakeLists.txt 里就是 lvgl

参考答案

cmake_minimum_required(VERSION 3.12.4) + project(my_ui C) + add_subdirectory(lvgl) + target_link_libraries(my_ui PRIVATE lvgl)lvgl 目标是 LVGL 的 CMakeLists.txt 暴露出来的库目标,它已经把 src/ 下的源码、include/ 头文件路径全部打包进去了,应用只需要链接它,CMake 会负责编译与头文件搜索。

练习 2:读 env_support 地图 进阶

打开 env_support/,说出 cmake/esp/rt-thread/qnx/cmsis-pack/ 各自服务什么宿主。再解释:为什么绝大多数普通工程走 env_support/cmake/main.cmake 这一支就够了?

提示

对照 15.2 的目录地图。想想 main.cmake 里那些 LV_BUILD_* 选项为什么对"任何没有专有构建系统的工程"都够用。

参考答案

cmake/ 是所有 CMake 集成的底座;esp/ 服务 ESP-IDF;rt-thread/ 服务 RT-Thread;qnx/ 服务 QNX;cmsis-pack/ 服务 ARM CMSIS-Pack 打包。普通工程没有任何专用构建系统,唯一的需求就是"把 src/ 编成库、把 include/ 加进头文件搜索路径、并允许我指定 lv_conf.h 的位置"——这三件事 main.cmakeLV_BUILD_CONF_PATHLV_BUILD_USE_KCONFIG 等选项全能覆盖,所以它是默认分支。

练习 3:换平台,能换到什么程度 挑战

你的 UI 代码目前跑在 SDL 模拟器上。现在要把它部署到一块 STM32 + ILI9341(320×240、RGB565)的板子上。请列出你需要改动与需要新增的内容(UI 代码本身、lv_conf.h、显示初始化、主循环、构建系统各是什么状态),并说明 zephyr/module.yml 在这条链路里能扮演什么角色。

提示

对照 15.4 的表格:分辨率、色深、缓冲、刷新、日志开关。想想 UI 代码为什么可以一行不改,以及 Zephyr module 的 cmake-ext 与 Kconfig 接入意味着什么。

参考答案

改动清单:UI 代码(widgets/样式/事件)不变,因为后端差异对 UI 透明;lv_conf.h 要改——LV_COLOR_DEPTH 从模拟器的 32 降到 16(RGB565),按需关闭模拟器专用开关(如 LV_USE_SDL);显示初始化从 lv_sdl_window_create() 换成面板驱动(ILI9341 通过 MIPI-DBI/SPI 接入)+ lv_display_create() + lv_display_set_buffers();主循环保持 lv_timer_handler() 模式,但睡眠由裸机延时或 RTOS 延时承担;构建系统换成目标机的(裸机 Make / ESP-IDF / Zephyr 任选)。zephyr/module.yml 的角色:如果目标平台走 Zephyr,它就是接入的"插头"——声明 CMake 扩展与 Kconfig 入口,让 LVGL 作为一个 Zephyr module 被 CONFIG_LVGL=y 拉进构建,面板驱动则由 Zephyr 的 devicetree/驱动栈提供。核心结论:跨平台时,变的是驱动与配置,不变的是 UI 层代码。