A Lot of Knobs#
LVGL has a lot of settings that can be configured at compile time. They change what LVGL is capable of, and they're there so you can fit LVGL to what you're building: which renderers and drivers get compiled in, how much memory to use, how big the caches are.
The main way to configure LVGL is a single header file, lv_conf.h. It defines most of the available options, and you
enable the ones you need.
Kconfig#
Kconfig is a configuration system originally created for the Linux kernel to replace manual file edits. Today it's the go-to configuration system for a lot of RTOSes and frameworks: ESP-IDF, Zephyr, NuttX, and U-Boot, just to name a few.
With that wider adoption, LVGL v7 added Kconfig support, which meant you could choose to configure your build with
either Kconfig or lv_conf.h.
Ever since, there's been a recurring problem: both files go out of sync. There was never an automated way to make sure they declared the same options, so every once in a while we'd end up reviewing an "add missing Kconfig entries" PR, opened by someone using Kconfig who realised an option they needed was missing.
As a long overdue task, LVGL v9.6 finally solves this and treats Kconfig as the source of truth for its configuration system, generating
lv_conf_template.h from it, the file you usually copy and rename to lv_conf.h.
If you've been using lv_conf.h, that is great, don't worry about it. We're committed to making sure LVGL can be built
with a single C compiler and no other external tools.
With this, Kconfig and lv_conf.h can't drift apart anymore, since one is generated from the other.
This took some work, though. The C preprocessor is more powerful than Kconfig and can represent things Kconfig can't, so a few options had to be rethought before Kconfig could describe all of them. That's most of what this post is about.
The process also meant cleaning up and replacing some configurations, so there have been some deprecations.
If you're upgrading to v9.6, check out our migration document, which lists the configuration changes. In v9.6 the old symbols produce warnings or errors at compile time, and in v10.0 they're removed.
The Files Involved#
Before getting into the details, it helps to know which files are in play. Two of them are now generated from the Kconfig tree by a Python script:
KconfigEvery option LVGL has is declared here, along with its type, default value, and dependencies (configuration source of truth)lv_conf_template.hThe file you can copy and rename tolv_conf.hto configure LVGL (generated)lv_conf_internal.hInternal configuration header that fills in default values for any value not set by the configuration file.
The two configuration paths meet at lv_conf_internal.h, and the rest of LVGL never needs to know which one you used.
For this work, Kconfiglib, a Python implementation of Kconfig, does the parsing of the Kconfig tree for us.
The rest of this post is the problems we hit getting there, one section each.
source vs rsource#
With lv_conf_template.h we originally had to keep every single option in a single place. After all, the goal is to
give you a file you can copy and rename to lv_conf.h, so there was no way to split it up.
With Kconfig you don't have to copy any files around, which means we can split Kconfig into multiple files, one per
subsystem or folder, and have each file include its children with rsource "subfolder/Kconfig".
Problem: this seemed straightforward until we realised that rsource is not part of the standard Kconfig syntax,
it's a kconfiglib extension. What is standard is source, which takes a path relative to the root Kconfig file.
Unfortunately that doesn't work for LVGL, since most of the time LVGL's Kconfig is sourced by an outside project, and
the root file is theirs, not ours.
Solution: before parsing the tree, the generator script concatenates every Kconfig file in the tree (which can use
rsource) into a single root Kconfig file in the LVGL tree. Outside projects source that one file and never have to
resolve a relative path themselves.
The only inconvenience we found with this approach is that every config now appears twice in the tree, once in its own subsystem file and once in the concatenated one, so a quick grep for a config will show two hits.
LV_CONF_MINIMAL#
LV_CONF_MINIMAL was only available as a Kconfig option, and it was meant to give you a starting point with most
settings turned off, so you could enable just the ones you need instead of disabling the ones you don't.
Problem: for that switch to work, every single configuration in the tree had to opt into it:
config LV_USE_LABEL
default y if !LV_CONF_MINIMALMultiply that by every option LVGL has and you get a condition repeated hundreds of times, one that everyone adding a new config has to remember. That's an anti-pattern.
Solution: the same result is better expressed as a defconfig, a file listing the values to start from. So the
option is removed and replaced by LVGL's "empty" defconfig at configs/defconfigs/empty.defconfig, which turns
everything off in one place instead of hundreds.
Code Configurations#
Some configuration values aren't values at all, they're fragments of C that the preprocessor pastes into the source:
#define LV_FONT_DEFAULT &my_font_24
#define LV_ASSERT_HANDLER while(1) ;Problem: in Kconfig it's not possible to define a config that expands into a "raw" value like this. There's only
int, hex, string, and tristate, and a string would arrive quoted, so &my_font_24 would end up as the literal
text "&my_font_24" rather than the address of your font.
Solution: this meant coming up with a new pattern, something that could be reused everywhere this comes up, and
that is LV_XXX_CUSTOM_INCLUDE accompanied by a LV_XXX_USE_CUSTOM_INCLUDE boolean flag. Instead of putting the C
fragment in Kconfig, you point Kconfig at a header of your own that contains it.
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*/cBecause the pattern is standardized, the generator adds these lines to lv_conf_internal.h, so your custom header is
never left out.
#if LV_FONT_USE_CUSTOM_INCLUDE
#include LV_FONT_CUSTOM_INCLUDE
#endifcIf you're using lv_conf.h you can define all of these inline. LVGL includes your lv_conf.h, so an inline #define
is picked up directly.
Lists Don't Need a Custom Include#
A few configs were whole arrays rather than single values, like the calendar's day names and libinput's xkb keymap:
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}cA custom include would work here, but it's a lot of ceremony for seven short strings, and nothing stopped you from passing eight day names and getting something unexpected out of it. These became one string config per element instead:
config LV_MONDAY_STR
string "Shortened string for Monday"
default "Mo"
config LV_TUESDAY_STR
string "Shortened string for Tuesday"
default "Tu"
...LVGL assembles the array itself, so a week can only have seven days in it.
Expressing Dependencies#
So far this has been about things Kconfig can't represent. This one goes the other way: features depend on each other, and Kconfig expresses that far better than a flat header does.
Problem: widgets are not independent. The Canvas widget, for instance, needs the Image widget to be enabled too. In
a plain lv_conf.h nothing captures that, so you enable LV_USE_CANVAS, build, and find out from a linker error.
Solution: in Kconfig the relationship is declared once, on the option itself:
config LV_USE_CANVAS
bool "Canvas"
select LV_USE_IMAGENow enabling LV_USE_CANVAS pulls in LV_USE_IMAGE automatically, and there are no dependencies to chase.
lv_conf.h users don't get automatic selection, since a plain header can't enable an option for you. But because the
relationship is declared in Kconfig, the generator can carry it across to both generated headers. It shows up in
lv_conf_template.h as a note telling you what else to turn on:
/* Enable: LV_USE_IMAGE*/
#define LV_USE_CANVAS 0cAnd as an actual check in lv_conf_internal.h, so if you miss it you find out at compile time with a message that
says exactly what's wrong:
#if LV_USE_CANVAS && !LV_USE_IMAGE
#error "LV_USE_IMAGE must be enabled: Kconfig selects it from LV_USE_CANVAS"
#endifcPromptless Configs#
Promptless configs are the other Kconfig feature worth talking about. A promptless config is one that never shows up in
menuconfig: its value is derived from the options you did set.
We use them to solve two different problems.
Choices That Become One Value#
config LV_STDLIB_CLIB
int
default 1The generator script picks up promptless integers like this one and defines them at the top of lv_conf_internal.h,
so they can be used as named constants:
#define LV_STDLIB_CLIB 1cWhich lets us write this:
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_MALLOCA choice lets you pick exactly one of the members, and LV_USE_STDLIB_MALLOC is promptless: you never set it, it
just picks up the constant matching whichever member you chose.
Problem: a choice is a group of separate boolean configs under the hood, and the "only one at a time" rule lives
in the Kconfig parser, not in the values it produces. Exporting the members as they are would give you this:
#define LV_USE_BUILTIN_MALLOC 1
#define LV_USE_CLIB_MALLOC 0cAn lv_conf.h user has no parser enforcing anything, so nothing stops them from setting both to 1 and breaking their
build. And it doesn't match what LVGL's code actually wants, which is a single value it can compare against.
Solution: the generator recognises this pattern and exports the derived config instead of the members, as one option with a documented set of values:
/** 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_BUILTINcOne option, one value, and picking two allocators is no longer expressible.
Features That Depend on a Capability, Not a Backend#
The second use for promptless configs is letting a backend advertise what it can do.
Problem: some widgets need a capability rather than a specific implementation. SVG needs vector graphics, and it doesn't care which draw unit provides them. Spelled out directly, that dependency is a list of every backend that happens to qualify:
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 ...You get the point, it becomes unmaintainable real quick. Every new draw unit means going back and editing every widget that could possibly use it.
Solution: declare the capability as a promptless boolean, and have each draw unit select it:
config LV_DRAW_HAS_VECTOR_SUPPORT
bool
default n
config LV_USE_DRAW_VGLITE
bool "VGLite"
default n
select LV_DRAW_HAS_VECTOR_SUPPORTNow the widget depends on the capability instead of the list:
config LV_USE_SVG
depends on LV_DRAW_HAS_VECTOR_SUPPORTAdding a new draw unit with vector support is one select line, and every widget that needs vector graphics picks it
up for free.
Wrap Up#
Adopting Kconfig as the source of truth means we can feel comfortable whenever people integrate LVGL that way. We can
now safely say that Kconfig users and lv_conf.h users are on the same footing, with no features lost either way.
It also forces LVGL to use configurations that Kconfig actually supports, which means plain string, integer, and boolean configs.
This is a very big restructure that shouldn't jump out at you as a user, but if you've dealt with Kconfig going out of
sync with lv_conf.h before, hopefully you'll appreciate it.
From v9.6 forward this is how LVGL's configuration system works, and we're already working on the next round of ideas to make it nicer to live with. If you have one of your own, we want to hear it.
Hit a Problem, or Have an Idea?
The restructure touches every option LVGL has. If a build breaks, if an option you rely on is missing from either file, or if you have an idea for the configuration system, tell us and we will look at it.
