I'm using nRF Connect SDK (NCS 3.1.1) with LVGL and newlib in a application. I encountered two related problems that I believe are caused by a conflict in the Zephyr LVGL module's build configuration and lv_conf.h macro definitions. I'd like to check whether there is an official or recommended solution before patching the SDK locally.
Environment
ncs3.1.1
LVGL 9.3.0 (CONFIG_LVGL=y)
newlib as C library (CONFIG_NEWLIB_LIBC=y, CONFIG_NEWLIB_LIBC=y,CONFIG_NEWLIB_LIBC_NANO=n,CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y,CONFIG_CBPRINTF_FP_SUPPORT=y)
conf
# C Library
CONFIG_NEWLIB_LIBC=y
# CONFIG_NEWLIB_LIBC_NANO is not set
CONFIG_NEWLIB_LIBC_FLOAT_PRINTF=y
# CONFIG_NEWLIB_LIBC_FLOAT_SCANF is not set
# CBPRINTF
CONFIG_CBPRINTF_COMPLETE=y
# CONFIG_CBPRINTF_NANO is not set
CONFIG_CBPRINTF_FP_SUPPORT=y
# CONFIG_CBPRINTF_LIBC_SUBSTS is not set
# LVGL Memory Settings (initial state, before any modification)
CONFIG_LV_USE_BUILTIN_MALLOC=y
# CONFIG_LV_USE_CLIB_MALLOC is not set
CONFIG_LV_USE_BUILTIN_STRING=y
# CONFIG_LV_USE_CLIB_STRING is not set
CONFIG_LV_USE_BUILTIN_SPRINTF=y <-- default, cause of Problem 1
# CONFIG_LV_USE_CLIB_SPRINTF is not set
# CONFIG_LV_USE_CUSTOM_SPRINTF is not set
Problem 1 — snprintf("%f") and snprintf("%g") output only f / g
Symptom
With the default CONFIG_LV_USE_BUILTIN_SPRINTF=y, calling snprintf() on floating-point values produces wrong output. A cJSON serialization result looks like this (all numeric fields become the letter g,is a single byte 0x67 ):
Minimal reproduction
char b[32]; snprintf(b, sizeof(b), "%d", 3); // -> "3" (OK) snprintf(b, sizeof(b), "%f", 3.14); // -> "f" (WRONG) snprintf(b, sizeof(b), "%g", 3.14); // -> "g" (WRONG) snprintf(b, sizeof(b), "%1.15g", 3.0); // -> "g" (WRONG) sprintf(b, "%f", 123.456); // -> "123.456000" (OK) sprintf(b, "%g", 123.456); // -> "123.456" (OK)
Note: sprintf() works correctly with floats, but snprintf() does not. This is the first clue that the two functions are resolved to different implementations.
Root cause of Problem 1
The linker map shows snprintf and vsnprintf are provided by LVGL's builtin sprintf implementation, not by newlib:
.text.snprintf 0x000582c4 0x28 modules/lvgl/libmodules__lvgl.a(lv_sprintf_builtin.c.obj)
.text.vsnprintf 0x000582ec 0x1c modules/lvgl/libmodules__lvgl.a(lv_sprintf_builtin.c.obj)
LVGL's builtin formatter does not support floating-point conversion, so %f / %g are emitted as the literal format character. sprintf is not overridden by LVGL in this version, so it still goes to newlib and works.
This is documented LVGL behavior for the builtin stdlib, but it means that with BUILTIN_SPRINTF, the whole project's snprintf() is silently hijacked by LVGL and loses float support. This is surprising for users who expect snprintf() to behave like the C library.
Problem 2 — Enabling CONFIG_LV_USE_CLIB_SPRINTF=y causes infinite recursion and system hang
To fix Problem 1, I switched LVGL's sprintf backend to CLIB:
conf
CONFIG_LV_USE_BUILTIN_SPRINTF=n
CONFIG_LV_USE_CLIB_SPRINTF=y
The Kconfig was applied correctly:
conf
# CONFIG_LV_USE_BUILTIN_SPRINTF is not set
CONFIG_LV_USE_CLIB_SPRINTF=y
# CONFIG_LV_USE_CUSTOM_SPRINTF is not set
Symptom
After this change the entire system hangs: no logs, no BLE, no button/LED response. In the debugger, execution repeatedly stops in:
// lv_sprintf_clib.c
int lv_vsnprintf(char * buffer, size_t count, const char * format, va_list va)
{
return vsnprintf(buffer, count, format, va);
}
Root cause of Problem 2
zephyr/modules/lvgl/include/lv_conf.h defines, when LV_USE_STDLIB_SPRINTF == LV_STDLIB_CLIB:
#define lv_snprintf snprintf
#define lv_vsnprintf vsnprintf
Meanwhile, CMakeLists.txt unconditionally compiles both the builtin and the clib implementation files:
cmake
${LVGL_DIR}/src/stdlib/builtin/lv_sprintf_builtin.c
...
${LVGL_DIR}/src/stdlib/clib/lv_sprintf_clib.c
In lv_sprintf_clib.c, the file's own function definitions are then rewritten by the preprocessor:
c
int vsnprintf(char * buffer, size_t count, const char * format, va_list va)
{
return vsnprintf(buffer, count, format, va); /* calls itself */
}
Global symbol override — the file now exports a global vsnprintf symbol, which overrides newlib's vsnprintf at link time. Every snprintf() / vsnprintf() in the project (logging, BLE stack, ISR printouts, application code) is routed into this recursive function.
This is why the entire system freezes, not just LVGL. Even code paths that never touch LVGL hit the broken vsnprintf.
Why this is not documented or caught at build time
lv_conf.h and lv_sprintf_clib.c both belong to the Zephyr LVGL module.
Upstream LVGL's design is either macro mapping or function wrapper, never both.
The Zephyr module does both simultaneously, and neither the compiler nor the linker emits a warning because the resulting symbols are still legally defined.
Is this conflict already known? If yes, is there a fix planned for an upcoming NCS release?
Is there a recommended configuration that provides both (a) correct floating-point support in snprintf() and (b) no symbol override of newlib, without patching the SDK? For example, is there a Kconfig we should not be setting, or a symbol we should be overriding in prj.conf?