#10732·lvgl

lv_st_ltdc: clean_dcache() hard-faults on Cortex-M7 when D-Cache is disabled (bootloader use case)

Author: wenzh3333Created Sep 18, 2026Updated Sep 18, 2026

LVGL version

v9.6.0

Platform

Custom STM32H7-based board (Cortex-M7), bare-metal (no RTOS), GCC + CMake, built-in st_ltdc driver enabled (LV_USE_ST_LTDC 1).

Context: a second-stage bootloader uses LVGL to draw its splash screen. At this boot stage the D-cache is intentionally not enabled — there is no SCB_EnableDCache() call anywhere in the bootloader.

What happened?

After upgrading our vendored LVGL tree from v9.4 to v9.6.0 (no other display-path changes), the bootloader began hard-faulting on the first frame flush, rebooting in a loop on every cold power-up. The same code ran fine on v9.4.

Fault evidence

  • HardFault with CFSR = 0x400 (PRECISERR).
  • The faulting PC sits inside the set/way loop of SCB_CleanDCache(), reached from clean_dcache() in src/drivers/display/st_ltdc/lv_st_ltdc.c. (We recovered the true PC from a shadow stack copied to a .noinit region at crash time, cross-checked with breadcrumb instrumentation — the raw stacked PC initially pointed at an unrelated frame-buffer address because the C fault wrapper pushes registers before the frame is captured.)
  • Behavior was not fully deterministic: every cold boot crashed, while some warm resets appeared to recover — consistent with UNPREDICTABLE rather than a guaranteed fault.

Root cause

clean_dcache() assumes the D-cache is enabled whenever the core is a Cortex-M7:

c
#if defined(__CORTEX_M) && __CORTEX_M == 7
    SCB_CleanDCache();
#elif defined(__CORTEX_A) && __CORTEX_A == 7
    L1C_CleanDCacheAll();
#endif

On targets that never call SCB_EnableDCache() (bootloaders and splash screens being the obvious case), the ARMv7-M Architecture Reference Manual marks cache maintenance operations executed while the cache is disabled as UNPREDICTABLE — on this MCU the DCCSW set/way loop takes a precise bus fault mid-loop. __DCACHE_PRESENT cannot guard this: it is a compile-time "this core implements a cache" constant, set to 1 by the vendor headers regardless of the runtime configuration.

Expected behavior

The in-tree driver should not assume the D-cache is enabled just because the core is an M7. With the cache disabled there are no dirty lines to clean, so skipping the maintenance is semantically a no-op and exactly matches the v9.4 behavior.

Note that this call was added to fix #8963 (stale LTDC reads with the cache enabled, PR #9192). The runtime gate below keeps that fix fully intact for cache-on users and only restores v9.4 behavior when the cache is off.

Suggested fix — one line; we carry it locally as a vendored-tree patch (5/5 cold power-on cycles clean since; previously every cold boot crashed):

c
static void clean_dcache(void)
{
#if defined(__CORTEX_M) && __CORTEX_M == 7
    if((SCB->CCR & SCB_CCR_DC_Msk) == 0) return; /* cache off: no dirty lines to clean, and maintenance is UNPREDICTABLE */
    SCB_CleanDCache();
#elif defined(__CORTEX_A) && __CORTEX_A == 7
    L1C_CleanDCacheAll();
#endif
}

Happy to send a PR with this change if you agree with the approach.

Corroborating reports of the same CPU-level behavior (vendor community):

How to reproduce?

Any Cortex-M7 target using the built-in st_ltdc driver with the D-cache left disabled:

  1. Enable the driver: LV_USE_ST_LTDC 1.
  2. Do not enable the D-cache (no SCB_EnableDCache(), i.e. SCB->CCR.DC = 0) — e.g. a bootloader that has not brought up cache/MPU yet.
  3. lv_init(), create the LTDC display, render the first frame.
  4. First flush → clean_dcache()SCB_CleanDCache() → HardFault (CFSR = 0x400).

Observed on STM32H7: every cold power-up reboots in a loop; the identical application code with the v9.4 tree runs fine, since v9.4 has no cache maintenance call in this driver.