Try out LVGL Pro - A complete toolkit to build, test, share, and ship UIs efficiently!
LVGL
Announcement

Kconfig as the Single Source of Truth for LVGL's Configuration

From v9.6, LVGL's Kconfig tree is the source of truth for the configuration system, and lv_conf_template.h is generated from it. Here's why, and what it took.

Andre CostaAndre Costa11 min read

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.

lv_conf.h is not going anywhere

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.

Some configuration options are deprecated

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:

  • Kconfig Every option LVGL has is declared here, along with its type, default value, and dependencies (configuration source of truth)
  • lv_conf_template.h The file you can copy and rename to lv_conf.h to configure LVGL (generated)
  • lv_conf_internal.h Internal 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_MINIMAL

Multiply 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"
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

Because the pattern is standardized, the generator adds these lines to lv_conf_internal.h, so your custom header is never left out.

lv_conf_internal.h
#if LV_FONT_USE_CUSTOM_INCLUDE
    #include LV_FONT_CUSTOM_INCLUDE
#endif
c
Not required for lv_conf.h users

If 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:

v9.5
#define LV_CALENDAR_DEFAULT_DAY_NAMES {"Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"}
c

A 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_IMAGE

Now 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 0
c

And 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"
#endif
c

Promptless 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 1

The 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 1
c

Which 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_MALLOC

A 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:

lv_conf_template.h
#define LV_USE_BUILTIN_MALLOC 1
#define LV_USE_CLIB_MALLOC 0
c

An 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_BUILTIN
c

One 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_SUPPORT

Now the widget depends on the capability instead of the list:

config LV_USE_SVG
	depends on LV_DRAW_HAS_VECTOR_SUPPORT

Adding 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.


Frequently Asked Questions#

About the author

Andre Costa
Andre Costa

Team Lead - LVGL Open

Team lead at LVGL Open, driving the development and adoption of the open-source embedded graphics library.

Meet the people behind the blog

Discover the talented writers sharing their knowledge about LVGL

View Authors

Subscribe to our newsletter to not miss any news about LVGL. We will send maximum of 2 mails per month.

LVGL

LVGL is the most popular free and open source embedded graphics library targeting any MCU, MPU and display type to build beautiful UIs.

We also do services like UI design, implementation and consulting.

© 2026 LVGL. All rights reserved.
YouTubeGitHubLinkedIn