Skip to main content

LVGL with SDL2

LVGL ships an SDL2 display and input driver, so a UI can be built and iterated on the desktop before it ever runs on hardware. The same lv_conf.h and application code then compile for the target.

CMake project

cmake_minimum_required(VERSION 3.10)
project(lvgl C)

set(CMAKE_C_STANDARD 11) # C11
set(CMAKE_C_STANDARD_REQUIRED ON)

include_directories(${PROJECT_SOURCE_DIR})

file(GLOB_RECURSE INCLUDES "lv_drivers/*.h" "lv_demos/*.h" "lvgl/*.h" "./*.h")
file(GLOB_RECURSE SOURCES "lv_drivers/*.c" "lv_demos/*.c" "lvgl/*.c")

set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

set(SDL2_DIR "C:/SDL2-2.30.1/x86_64-w64-mingw32")
include_directories(${SDL2_DIR}/include)
link_directories(${SDL2_DIR}/lib)

add_executable(main main.c mouse_cursor_icon.c ${SOURCES} ${INCLUDES})
target_compile_definitions(main PRIVATE LV_CONF_INCLUDE_SIMPLE)
target_link_libraries(main PRIVATE SDL2)

add_custom_target(run COMMAND ${EXECUTABLE_OUTPUT_PATH}/main)

Details worth getting right:

  • link_directories points at lib, not bin. The MinGW SDL2 package puts the import library libSDL2.dll.a in lib and the runtime SDL2.dll in bin. Pointing the linker at bin fails to resolve SDL symbols; the DLL in bin is what has to be on PATH or beside the executable at run time.
  • Optimisation flags follow the language. This is a C project, so CMAKE_C_FLAGS applies; setting CMAKE_CXX_FLAGS has no effect. Better still, leave it alone and let -DCMAKE_BUILD_TYPE=Release choose.
  • GLOB_RECURSE does not re-run on its own. Adding a source file does not trigger a reconfigure, so the build silently omits it. Either re-run CMake by hand after adding files, or add CONFIGURE_DEPENDS (CMake 3.12+), or list sources explicitly.
  • LV_CONF_INCLUDE_SIMPLE makes LVGL include lv_conf.h by plain name rather than by relative path, which is what lets the file sit at the project root. Defining it on the target rather than directory-wide keeps it scoped.

Finding SDL2 with find_package

The alternative to hard-coded paths. It requires SDL2's CMake config files, which ship in the development package:

find_package(SDL2 REQUIRED)
message(STATUS "SDL2 include dirs: ${SDL2_INCLUDE_DIRS}")
message(STATUS "SDL2 libraries: ${SDL2_LIBRARIES}")

find_package(SDL2 REQUIRED SDL2) — with the package name repeated as a component — is not valid; the second argument is read as a component name that SDL2 does not define.

When SDL2 is found this way, link against the imported target rather than the bare name so that include directories and interface flags come along automatically:

target_link_libraries(main PRIVATE SDL2::SDL2)

Point CMake at the config directory if it is not on the default search path:

-DSDL2_DIR="C:\\SDL2\\cmake"

or, when only the import library is available and there is no CMake config:

-DSDL2_DIR="C:\\SDL2\\cmake" -DSDL2_LIBRARY="C:\\SDL2\\x86_64-w64-mingw32\\lib\\libSDL2.dll.a"

Running

SDL2.dll must be findable at run time — copy it next to the executable, or add its directory to PATH. A missing DLL shows as the program exiting immediately with no output, which looks like a crash rather than a load failure.

The MSYS2 UCRT64 toolchain described on the C++ page builds this project as-is; use its pacman SDL2 package (mingw-w64-ucrt-x86_64-SDL2) instead of a downloaded SDK to avoid the path juggling above entirely.