ncs3.1.1 LVGL+newlib


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?

Parents
  • Hi,

     

    It seems that the version of lvgl that you are using does come with this function:

    https://github.com/lvgl/lvgl/commit/84c8cf8573e41cd969375cf8937e4399ee85b846

     

    With the default CONFIG_LV_USE_BUILTIN_SPRINTF=y, calling snprintf() on floating-point values produces wrong output.

    LVGL is a 3rd party project. If you have issues with their floating point functions, I would recommend that you contact lvgl project and ask, but before doing that; please check with a newer ncs/zephyr version to verify if the problem still exists.

    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:

    Could you ensure that you build was fully deleted and re-generated between testing? 

     

    Kind regards,

    Håkon

  • Thank you for the suggestion — I did test with a newer NCS version and I have some findings to share.

    1. Testing on NCS 3.4.0 — the issue is resolved

    I built a fresh test project on NCS 3.4.0 (Zephyr 4.4.0) with the same configuration (CONFIG_LV_USE_CLIB_SPRINTF=y, CONFIG_LV_USE_BUILTIN_SPRINTF=n) and the problem is gone. Linker map confirms:

    snprintf / sprintf / vsnprintf → picolibc libc_tinystdio_*.c.o (not overridden by LVGL)

    lv_snprintf / lv_vsnprintf → lv_sprintf_clib.c.obj as independent symbols (the latter is only 4 bytes — a single tail branch, no recursion)

    libc_tinystdio_dtoa_ryu.c.o linked → float conversion active

    Runtime probe: %d→3, %f→3.140000, %g→3.14, %.2f→3.14

    So on 3.4.0 the CLIB-mode recursion is gone and no SDK patch is needed.

    2. But I cannot pin down what actually fixed it

    Looking at the changelogs, it appears that starting from NCS 3.2.0 (the earliest version I could find with this change), the default C library switched from newlib to picolibc. I could not determine whether the fix came from:

    the LVGL module's own update (e.g. removing the #define lv_snprintf / lv_vsnprintf macro conflict in lv_conf.h, or changing CMakeLists to conditionally compile lv_sprintf_clib.c), or

    the underlying libc switch to picolibc (which does not have newlib's _impure_ptr / sys/reent.h model, and enables float IO by default via CONFIG_PICOLIBC_IO_FLOAT=y)

    Could you clarify which one was the actual fix on the NCS side? That would help me judge whether it can be backported to 3.1.1.

    3. Why I still need to stay on NCS 3.1.1 for now

    My device does a significant amount of computation and currently calls many newlib-specific interfaces. I have not yet estimated the migration cost to picolibc or the cost of upgrading the whole product to 3.4.0. Since the issue only affects LVGL's snprintf in CLIB mode, I would prefer to fix it with a minimal change on NCS 3.1.1 rather than a full platform upgrade.

    4. Current plan on 3.1.1 — minimal CMakeLists change

    My current plan is to modify only:

    zephyr/modules/lvgl/CMakeLists.txt
    and change the compilation relationship so that lv_sprintf_clib.c is not compiled when CONFIG_LV_USE_CLIB_SPRINTF=y. This lets the #define lv_snprintf / lv_vsnprintf macro mapping in lv_conf.h take effect, which maps LVGL's calls directly to newlib's snprintf / vsnprintf and avoids the recursive self-call.

    Before I commit to this approach, I would like to ask:

    Is modifying CMakeLists.txt the recommended way to work around this on 3.1.1, or is there a Kconfig-based way to exclude lv_sprintf_clib.c without touching the SDK files?

    Best regards

  • Hi,

     

    Thank you for verifying and testing on ncs v3.4.0

    I would recommend that you try to look into this specific commit/PR:

    https://github.com/zephyrproject-rtos/zephyr/pull/94073

     

    There are two backports to zephyr v4.1-branch and v4.2-branch linked in the PR itself that you can try to cherry-pick or manually apply.

     

    Kind regards,

    Håkon

Reply Children
No Data
Related