大量的配置开关#
LVGL 有大量可以在编译期配置的选项。它们决定了 LVGL 具备哪些能力,让你能够按照自己的项目来裁剪 LVGL:编译进哪些渲染器和驱动、 使用多少内存、缓存有多大。
配置 LVGL 的主要方式是一个头文件 lv_conf.h。它定义了大部分可用选项,你只需启用需要的那些。
Kconfig#
Kconfig 最初是为 Linux 内核创建的配置系统,用来取代手工编辑文件。如今它已经成为很多 RTOS 和框架的首选配置系统, 例如 ESP-IDF、Zephyr、NuttX 和 U-Boot。
正是因为它被广泛采用,LVGL v7 加入了 Kconfig 支持,你可以选择用 Kconfig 或 lv_conf.h 来配置构建。
从那时起就一直有一个反复出现的问题:这两个文件会不同步。以前没有任何自动化手段来保证它们声明相同的选项, 所以我们隔一段时间就要审查一个「补上缺失的 Kconfig 条目」的 PR,提交者往往是某个 Kconfig 用户,发现自己需要的选项不存在。
作为一件早该完成的工作,LVGL v9.6 终于解决了这个问题:把 Kconfig 作为配置系统的唯一事实来源,并由它生成
lv_conf_template.h,也就是你通常复制并重命名为 lv_conf.h 的那个文件。
如果你一直在用 lv_conf.h,那很好,不用担心。我们承诺让 LVGL 始终可以只用一个 C 编译器构建,不依赖任何其他外部工具。
这样一来,Kconfig 和 lv_conf.h 就不可能再各走各的了,因为其中一个是由另一个生成的。
不过这需要不少工作。C 预处理器比 Kconfig 更强大,能表达一些 Kconfig 无法表达的东西,所以在 Kconfig 能够描述全部选项之前, 有几个选项必须重新设计。本文大部分内容讲的就是这些。
这个过程也顺带清理和替换了一些配置,因此出现了一些废弃项。
如果你要升级到 v9.6,请查看我们的迁移文档,其中列出了配置方面的变更。 在 v9.6 中,旧符号会在编译时产生警告或错误,在 v10.0 中它们被移除。
涉及的文件#
在进入细节之前,先了解一下涉及哪些文件会有帮助。其中两个现在由 一个 Python 脚本从 Kconfig 树生成:
KconfigLVGL 的每个选项都在这里声明,包括类型、默认值和依赖关系(配置的事实来源)lv_conf_template.h你可以复制并重命名为lv_conf.h来配置 LVGL 的文件(生成)lv_conf_internal.h内部配置头文件,为配置文件中未设置的值填入默认值。
两条配置路径在 lv_conf_internal.h 汇合,LVGL 的其余部分完全不需要知道你用的是哪一条。
这项工作中,Kconfiglib,一个 Kconfig 的 Python 实现,负责帮我们解析 Kconfig 树。
本文接下来讲的是我们在这个过程中遇到的问题,每个问题一节。
source 与 rsource#
使用 lv_conf_template.h 时,我们原本必须把每一个选项都放在同一个地方。毕竟目标是给你一个可以复制并重命名为 lv_conf.h
的文件,所以没法拆分。
而使用 Kconfig 时你不需要复制任何文件,这意味着我们可以把 Kconfig 拆成多个文件,每个子系统或目录一个,
再用 rsource "subfolder/Kconfig" 让每个文件包含它的子文件。
问题: 这看起来很直接,直到我们发现 rsource 并不是标准 Kconfig 语法的一部分,它是 kconfiglib 的扩展。标准的写法是 source,
它接受相对于根 Kconfig 文件的路径。可惜这对 LVGL 行不通,因为大多数情况下 LVGL 的 Kconfig 是被外部项目引入的,
根文件是他们的,不是我们的。
解决方案: 在解析这棵树之前,生成脚本会把树中的每一个 Kconfig 文件(它们可以使用 rsource)拼接成 LVGL 树中的
单个根 Kconfig 文件。外部项目只需引入这一个文件,不必自己解析相对路径。
我们发现这种做法唯一不便的地方是,现在每个配置在树里会出现两次,一次在它自己的子系统文件中,一次在拼接后的文件中, 所以 grep 一个配置会看到两条结果。
LV_CONF_MINIMAL#
LV_CONF_MINIMAL 以前只在 Kconfig 中可用,它的用意是给你一个大部分设置都关闭的起点,让你只启用需要的选项,
而不是关闭不需要的选项。
问题: 要让这个开关生效,树中的每一个配置都必须主动配合:
config LV_USE_LABEL
default y if !LV_CONF_MINIMAL把它乘以 LVGL 的全部选项数量,你就得到一个重复了几百次的条件,而且每个新增配置的人都必须记得写上它。这是一种反模式。
解决方案: 同样的效果用 defconfig 表达更合适,也就是一个列出起始值的文件。因此这个选项被移除,
改为 LVGL 位于 configs/defconfigs/empty.defconfig 的「空」defconfig,它在一个地方把所有东西关掉,而不是几百个地方。
代码型配置#
有些配置值根本不是值,而是由预处理器粘贴进源码的 C 代码片段:
#define LV_FONT_DEFAULT &my_font_24
#define LV_ASSERT_HANDLER while(1) ;c问题: 在 Kconfig 中无法定义一个展开成这种「原始」值的配置。它只有 int、hex、string 和 tristate,
而 string 传过来时是带引号的,所以 &my_font_24 最终会变成字面文本 "&my_font_24",而不是你字体的地址。
解决方案: 这需要一个新的模式,一个在所有类似场景都能复用的模式,那就是 LV_XXX_CUSTOM_INCLUDE
配合一个 LV_XXX_USE_CUSTOM_INCLUDE 布尔开关。你不再把 C 片段放进 Kconfig,而是让 Kconfig 指向你自己的、包含该片段的头文件。
CONFIG_LV_FONT_USE_CUSTOM_INCLUDE=y
CONFIG_LV_FONT_CUSTOM_INCLUDE="my_font_custom_include.h"#ifndef MY_FONT_CUSTOM_INCLUDE_H
#define MY_FONT_CUSTOM_INCLUDE_H
#define LV_FONT_DEFAULT &my_font_24
#endif /*MY_FONT_CUSTOM_INCLUDE_H*/c因为这个模式是标准化的,生成器会把下面这几行加进 lv_conf_internal.h,所以你的自定义头文件绝不会被遗漏。
#if LV_FONT_USE_CUSTOM_INCLUDE
#include LV_FONT_CUSTOM_INCLUDE
#endifc如果你使用 lv_conf.h,这些都可以直接内联定义。LVGL 会包含你的 lv_conf.h,所以内联的 #define 会被直接采用。
列表不需要自定义头文件#
有几个配置是整个数组,而不是单个值,比如日历控件的星期名称和 libinput 的 xkb 键盘映射:
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}c这里用自定义头文件当然可行,但为了七个短字符串搞这么大阵仗并不值得,而且没有任何东西能阻止你传入八个星期名称, 从而得到意料之外的结果。这些配置改成了每个元素一个字符串配置:
config LV_MONDAY_STR
string "Shortened string for Monday"
default "Mo"
config LV_TUESDAY_STR
string "Shortened string for Tuesday"
default "Tu"
...数组由 LVGL 自己组装,所以一周只可能有七天。
表达依赖关系#
到目前为止讲的都是 Kconfig 表达不了的东西。这一节反过来:功能之间互相依赖,而 Kconfig 表达这一点比扁平的头文件好得多。
问题: 控件并不是彼此独立的。比如 Canvas 控件需要同时启用 Image 控件。在普通的 lv_conf.h 里没有任何东西能表达这一点,
于是你启用了 LV_USE_CANVAS,一构建,才从链接错误中发现问题。
解决方案: 在 Kconfig 中,这个关系只需在选项本身上声明一次:
config LV_USE_CANVAS
bool "Canvas"
select LV_USE_IMAGE现在启用 LV_USE_CANVAS 会自动带上 LV_USE_IMAGE,不需要再去追查依赖。
lv_conf.h 用户享受不到自动选择,因为普通头文件无法替你启用某个选项。但由于这个关系是在 Kconfig 中声明的,
生成器可以把它带到两个生成的头文件里。在 lv_conf_template.h 中,它表现为一条提示你还需要打开什么的注释:
/* Enable: LV_USE_IMAGE*/
#define LV_USE_CANVAS 0c在 lv_conf_internal.h 中,它则是一个真正的检查,所以如果你漏了,会在编译时看到一条准确说明问题的消息:
#if LV_USE_CANVAS && !LV_USE_IMAGE
#error "LV_USE_IMAGE must be enabled: Kconfig selects it from LV_USE_CANVAS"
#endifc无提示配置#
无提示(promptless)配置是另一个值得一说的 Kconfig 特性。无提示配置不会出现在 menuconfig 中,它的值由你设置的其他选项推导出来。
我们用它解决两类不同的问题。
归结为单个值的选择项#
config LV_STDLIB_CLIB
int
default 1生成脚本会识别这样的无提示整数配置,并把它们定义在 lv_conf_internal.h 的开头,这样它们就能当作具名常量使用:
#define LV_STDLIB_CLIB 1c于是我们可以这样写:
choice
prompt "Malloc functions source"
default LV_USE_BUILTIN_MALLOC
config LV_USE_BUILTIN_MALLOC
bool "LVGL built-in"
config LV_USE_CLIB_MALLOC
bool "C standard library (malloc/realloc/free)"
endchoice
config LV_USE_STDLIB_MALLOC
int
default LV_STDLIB_BUILTIN if LV_USE_BUILTIN_MALLOC
default LV_STDLIB_CLIB if LV_USE_CLIB_MALLOCchoice 让你只能在成员中选一个,而 LV_USE_STDLIB_MALLOC 是无提示的:你从不设置它,它只是取到与你所选成员对应的那个常量。
问题: choice 在底层是一组各自独立的布尔配置,「同时只能选一个」这条规则活在 Kconfig 解析器里,而不在它产生的值里。
把成员原样导出会得到这样的结果:
#define LV_USE_BUILTIN_MALLOC 1
#define LV_USE_CLIB_MALLOC 0clv_conf.h 用户没有解析器替他们把关,所以没有任何东西阻止他们把两个都设为 1,把构建搞坏。而且这也不符合 LVGL 代码真正想要的东西,
它想要的是一个可以用来比较的单一值。
解决方案: 生成器会识别这个模式,导出推导出来的那个配置,而不是各个成员,形成一个取值范围有明确文档的选项:
/** Malloc functions source
* Possible values:
* - LV_STDLIB_BUILTIN: LVGL built-in
* - LV_STDLIB_CLIB: C standard library (malloc/realloc/free)
*/
#define LV_USE_STDLIB_MALLOC LV_STDLIB_BUILTINc一个选项,一个值,同时选中两个内存分配器不再是能表达出来的事情。
依赖能力而非后端的功能#
无提示配置的第二个用途,是让后端声明自己能做什么。
问题: 有些控件需要的是一种能力,而不是某个特定实现。SVG 需要矢量图形,但它不在乎由哪个绘制单元提供。 如果直接写出来,这个依赖就是一份「凡是符合条件的后端」的清单:
config LV_USE_SVG
depends on LV_USE_DRAW_VGLITE || LV_USE_DRAW_NANOVG || (LV_USE_DRAW_SW && LV_USE_THORVG) || LV_USE_NEMA_GFX ...你懂的,这很快就会变得无法维护。每加一个新的绘制单元,就要回头去改每一个可能用到它的控件。
解决方案: 把这种能力声明为一个无提示布尔配置,让每个绘制单元去 select 它:
config LV_DRAW_HAS_VECTOR_SUPPORT
bool
default n
config LV_USE_DRAW_VGLITE
bool "VGLite"
default n
select LV_DRAW_HAS_VECTOR_SUPPORT现在控件依赖的是这个能力,而不是那份清单:
config LV_USE_SVG
depends on LV_DRAW_HAS_VECTOR_SUPPORT新增一个支持矢量图形的绘制单元只需一行 select,每个需要矢量图形的控件都自动受益。
小结#
把 Kconfig 作为事实来源,意味着别人以这种方式集成 LVGL 时我们可以更放心。现在我们可以有把握地说,
Kconfig 用户和 lv_conf.h 用户站在同一起跑线上,两边都不会少任何功能。
它也促使 LVGL 只使用 Kconfig 真正支持的配置形式,也就是普通的字符串、整数和布尔配置。
这是一次很大的重构,作为用户你不应该明显感觉到它,但如果你以前被 Kconfig 和 lv_conf.h 不同步困扰过,希望你会喜欢这个变化。
从 v9.6 开始,LVGL 的配置系统就是这样工作的,而我们已经在准备下一批想法,让它用起来更顺手。如果你也有想法,我们很想听听。
