CMake

In the rapidly evolving world of embedded development, efficient and reliable build systems play a crucial role in accelerating product development and ensuring code quality. For STM32 developers, mastering the build process is fundamental to fully leveraging the power of STM32 microcontrollers and its extensive ecosystem.

CMake has emerged as a modern, versatile, and widely adopted build system generator that addresses many challenges faced by embedded developers today. Unlike traditional IDE-specific project formats, CMake provides a unified, flexible, and portable approach to managing builds across different platforms, toolchains, and environments.

This page aims to introduce STM32 developers to the basics of CMake, focusing on practical use cases that simplify the learning curve and help avoid common pitfalls during onboarding.

Why Learn CMake for STM32 Development?

  1. IDE Independence and Choice

While STM32CubeIDE and Eclipse/CDT are popular development environments, relying solely on GUI-driven projects can limit flexibility. CMake allows developers to generate project files for multiple IDEs or build directly from the command line, offering freedom to choose the best tools for each task.

  1. Improved Project Portability and Scalability

CMake uses simple, text-based configuration files ( CMakeLists.txt) that are easy to version control, share, and maintain. This contrasts with IDE-specific project files that are often bulky and difficult to manage. As STM32 projects grow in complexity, CMake’s modular approach helps maintain a clean and scalable build system.

  1. Compiler abstraction

CMake’s toolchain abstraction can allow developers to switch seamlessly between toolchains without rewriting build scripts. This flexibility allows STM32 developers to more easily port projects between toolchains.

  1. Automation and Continuous Integration Ready

In modern embedded development, automated builds and testing pipelines are key to maintaining code quality and accelerating delivery. CMake’s command-line driven workflow integrates naturally with continuous integration (CI) systems, enabling STM32 teams to automate builds, run tests, and deploy firmware efficiently.

  1. Software component and library integration

Many STM32 software components, middleware, and libraries now provide native CMake support or integration examples. Learning CMake empowers developers to take full advantage of these resources, reducing integration effort and avoiding vendor lock-in with specific IDEs.

By understanding and adopting CMake, STM32 developers gain a powerful skillset that enhances productivity, fosters collaboration, and future-proofs their projects. This application note will guide you step-by-step through essential CMake concepts and practical examples tailored to STM32 development, ensuring a smooth and confident onboarding experience.

Further Reading

For more detailed information, visit the official CMake online documentation .

Create a CMake project

In order to create a new CMake project and open it in VS Code, follow the Import STM32CubeMX project guide.

After the project is created and imported into VS Code you can re-visit this page for additional CMake information.

STM32CubeMX Generated Files

STM32CubeMX is responsible for the generation of two types of CMake files:

  1. Tool-owned files - These files should not be modified, they will be overwritten by CubeMX.

  2. User-owned files - These files can be modified by the user to manage builds.

Screenshot of the selection of files in a STM32CubeMX CMake project.
  • CMakeLists.txt: Managed by CubeMX. Do not modify it!

  • gcc-arm-none-eabi.cmake: Contains toolchain settings. Users can modify this file.

  • CMakeLists.txt: Contains high-level project definition. Users can modify this file.

  • CMakePresets.json: Contains CMake presets. Users can modify this file.

Generated Files

How to create a configuration preset

You can create your own configuration and build preset in the CMakePresets.json file This an example of preset configuration:

"name": the machine-friendly name of the preset,
"hidden": specifying whether a preset should be hidden,
"inherits": preset name to inherit from,
"condition": to specify a condition to use this preset,
"vendor": vendor name,
"displayName": human friendly name,
"generator": representing the generator to use for the preset,
"toolchainFile": representing the path to the toolchain file,
"cacheVariables": {
    "CMAKE_BUILD_TYPE": the cache variable embedding the machine-friendly name of  the preset
}

For more detailed information, visit the official CMake presets documentation .

After creating the configuration preset, a build preset must be created in the same file. An example of this is displayed below:

Screenshot of an example of a build preset configuration.

To use this configuration preset you can select it from the “Project Status” tab of the CMake extension interface or via the command line with --preset=example:

You can also use the CMAKE_BUILD_TYPE as a condition to build/include files.

CMakeLists.txt

CMakeLists.txt is a one-time generated file. Any user change is thereby kept if STM32CubeMX or other ST tools are re-generating assets.

Two CMakeLists.txt will be available in the project: - A generic CMakeLists.txt in the main project folder. - An MX-Generated CMakeLists.txt included by the generic one containing STM32CubeMX project configuration.

How to add source files

It is advised to use relative paths intead of absolute paths to help share the project across development environments.

Use the target_sources() command to add source files to your target.

Syntax:

target_sources(<target> <scope>
  <source1>
  <source2>
  ...
)
  • <target> : Your target name (e.g., executable or library).

  • <scope> : PRIVATE , PUBLIC , or INTERFACE .

  • <source> : Source file paths relative to the CMakeLists.txt .

Example:

target_sources(my_project PRIVATE
  src/main.c
  src/stm32c0xx_it.c
  Drivers/STM32C0xx_HAL_Driver/Src/stm32c0xx_hal_gpio.c
  startup/startup_stm32c0l1xx.s
)

Steps:

  1. Identify your target name.

  2. List source files with correct relative paths.

  3. Add them using target_sources() with appropriate scope.

  4. Run CMake to configure and build.

Screenshot of a CMakeLists.txt snippet showing the target_sources() command used to add multiple source files to the target stm32cubemx with INTERFACE scope. The listed files include C source files such as main.c, interrupt handlers, HAL driver source files, and a startup assembly file, all specified with relative paths.

How to add include Files/Folders

Add source files with target_sources() and include directories with target_include_directories() .

  1. Add source files:

target_sources(<target> <scope>
  <source1>
  <source2>
  ...
)
  1. Add include directories:

target_include_directories(<target> <scope>
  <include_dir1>
  <include_dir2>
  ...
)

Example:

target_sources(stm32cubemx INTERFACE
  ../../Core/Src/main.c
  ../../Drivers/STM32C0xx_HAL_Driver/Src/stm32c0xx_hal_gpio.c
  ../../startup_stm32c0l1xx.s
)

target_include_directories(stm32cubemx INTERFACE
  ../../Core/Inc
  ../../Drivers/STM32C0xx_HAL_Driver/Inc
  ../../Drivers/CMSIS/Include
)

Notes:

  • Use PRIVATE for internal use, INTERFACE to propagate to dependents.

  • Paths are relative to the CMakeLists.txt location.

  • Include directories specify header file locations for compilation.

Screenshot of a CMakeLists.txt snippet showing the target_include_directories() command adding several include directories to the target stm32cubemx with INTERFACE scope. The directories listed include core and driver header folders, as well as CMSIS include paths, all specified with relative paths

How to add symbols

Use target_compile_definitions() to add preprocessor macros (compile-time symbols) to your target.

Syntax:

target_compile_definitions(<target> <scope>
  <definition1>
  <definition2>
  ...
)
  • <target> : The target name (executable or library).

  • <scope> : PRIVATE , PUBLIC , or INTERFACE .

  • <definition> : Preprocessor definitions (macros), optionally with values or generator expressions.

Example:

target_compile_definitions(stm32cubemx INTERFACE
  USE_HAL_DRIVER
  STM32C011xx
  $<$<CONFIG:Debug>:DEBUG>
)

Explanation:

  • USE_HAL_DRIVER and STM32C011xx are simple macros defined for all builds.

  • $<$<CONFIG:Debug>:DEBUG> defines the DEBUG macro only for Debug configuration builds.

  • Definitions added with INTERFACE scope propagate to targets that link to stm32cubemx .

Steps:

  1. Identify the target to which you want to add definitions.

  2. Choose the appropriate scope:

    • PRIVATE : For the target only.

    • PUBLIC : For the target and dependents.

    • INTERFACE : For dependents only.

  3. List the macros you want to define.

  4. Add conditional macros using generator expressions if needed.

  5. Run CMake to apply changes.

Screenshot of a CMakeLists.txt snippet using target_compile_definitions() to add preprocessor macros like USE_HAL_DRIVER and STM32C011xx to the stm32cubemx target with INTERFACE scope, including a conditional DEBUG macro for Debug builds.

How to add Libraries

Use target_link_libraries() to specify libraries to link against your target.

Syntax:

target_link_libraries(<target> <scope>
  <library1>
  <library2>
  ...
)
  • <target> : The target to which libraries are linked (executable or library).

  • <scope> : Optional, can be PRIVATE , PUBLIC , or INTERFACE .

  • <library> : Names of libraries or targets to link.

Example:

target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE
  stm32cubemx
  # Add other user-defined libraries here
)

Explanation:

  • ${CMAKE_PROJECT_NAME} is the current project target.

  • stm32cubemx is a library target linked to the project.

  • Scope PRIVATE means libraries are linked only for this target.

  • Use PUBLIC or INTERFACE to propagate linkage to dependents.

Steps:

  1. Identify the target to link libraries to.

  2. List the libraries or targets to link.

  3. Specify the linkage scope if needed.

  4. Add the target_link_libraries() command in your CMakeLists.txt .

  5. Run CMake to configure and build the project.

Screenshot of a CMakeLists.txt snippet showing the target_link_libraries() command used to link the stm32cubemx library to the current project target represented by ${CMAKE_PROJECT_NAME}. A comment indicates where additional user-defined libraries can be added Screenshot of a CMakeLists.txt snippet showing the target_link_directories() command used to add private library search paths for the current project target ${CMAKE_PROJECT_NAME}. A comment indicates where user-defined library directories can be added.

Toolchain files

Toolchain files configure the compiler, linker, and build environment for cross-compiling or targeting specific platforms.

Purpose:

  • Define system name and processor architecture.

  • Specify compiler and toolchain executables.

  • Set compiler and linker flags.

  • Enable building with different toolchains (e.g., GCC or LLVM).

Common Toolchain Files:

  • gcc-arm-none-eabi.cmake : For GCC ARM embedded toolchain.

  • starm-clang.cmake : For LLVM/Clang ARM toolchain.

Project Setup:

  • When creating a new project, you can select: - GCC toolchain only. - LLVM toolchain only. - Hybrid environment with both GCC and LLVM toolchains.

  • Hybrid projects include both toolchain files, allowing flexible use of either compiler.

Example Snippet (GCC Toolchain):

set(CMAKE_SYSTEM_NAME generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_PREFIX arm-none-eabi-)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++)
set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}ld)

set(CMAKE_C_FLAGS "-mcpu=cortex-m4 -mthumb ...")

Usage:

  1. Specify the toolchain file when configuring CMake:

    cmake -DCMAKE_TOOLCHAIN_FILE=path/to/gcc-arm-none-eabi.cmake ..
    
  2. The toolchain file sets up the environment for cross-compilation automatically.

  3. Use different toolchain files to switch between GCC and LLVM compilers.

Screenshot of a CMakeLists.txt snippet showing the target_link_libraries() command used to link the stm32cubemx library to the current project target represented by ${CMAKE_PROJECT_NAME}. A comment indicates where additional user-defined libraries can be added. Screenshot of a project directory structure in a file explorer. It shows folders such as .settings, .vscode, build, cmake (containing stm32cubemx, gcc-arm-none-eabi.cmake, and starm-clang.cmake), Core, and Drivers. The root directory also includes files like .gitignore, CMakeLists.txt, CMakePresets.json, llvm.ioc, startup_stm32f030x6.s assembly file, and STM32F030XX_FLASH.ld linker script.

Compiler settings

This snippet configures the compiler environment in CMake by setting the system name, processor type, and forcing the use of GCC compilers. It defines the toolchain prefix and specifies the compiler, assembler, linker, and related tools for ARM cross-compilation.

Screenshot of a CMake toolchain file snippet setting compiler-related variables. It defines the system name as "Generic" and processor as "arm," forces the use of GCC compilers, sets the toolchain prefix to "arm-none-eabi-", and assigns compiler, assembler, linker, objcopy, objdump, and size tool executables using this prefix.

Build output suffix

This snippet sets the output file suffix for assembly, C, and C++ executables to .elf in the CMake build configuration.

Screenshot of a CMakeLists.txt snippet defining output file suffixes for build artifacts. It sets .elf as the executable suffix for assembly, C, and C++ source files using CMAKE_EXECUTABLE_SUFFIX_ASM, CMAKE_EXECUTABLE_SUFFIX_C, and CMAKE_EXECUTABLE_SUFFIX_CXX.

Cortex settings

This snippet sets MCU-specific compiler flags for the Cortex-M4 processor, enabling floating-point and specific instruction set options.

Screenshot of a CMake snippet defining MCU-specific compiler flags. It sets TARGET_FLAGS with options for Cortex-M4 CPU architecture, floating-point unit support, and ABI configuration.

Build optimization settings

The settings depend on the selected context. By default, only release and debug are available, if you have more build context you can add more configurations in this area.

Screenshot of a CMakeLists.txt snippet configuring build optimization flags. It sets compiler flags to -O0 -g3 for Debug builds to disable optimization and enable debugging, and -Os -g0 for Release builds to optimize for size without debug information.

The compiler supports the following optimization levels:

  • -O (Same as -O1)

  • -O0 (do no optimize, the default if no optimization level is specified)

  • -O1 (optimize minimally, favouring compilation time)

  • -O2 (optimize more, without speed/size trade-off)

  • -O3 (optimize even more, favouring speed)

  • -Ofast (optimize very aggressively to the point of breaking standard compliance, favoring speed. May change program behavior)

  • -Og (Optimize debugging experience. -Og enables optimizations that do not interfere with debugging. It should be the optimization level of choice for the standard edit-compile-debug cycle, offering a reasonable level of optimization while maintaining fast compilation and a good debugging experience.)

  • -Os (Optimize for size. -Os enables all -O2 optimizations that do not typically increase code size. It also performs further optimizations designed to reduce code size. -Os disables the following optimization flags: -falign-functions -falign-jumps -falign-loops -falign-labels -freorder-blocks -freorder-blocks-and-partition -fprefetch-loop-arrays -ftree-vect-loop-version)

ASM Configuration

This snippet sets assembler flags to enable C++ preprocessing and configures C++ compiler flags to disable RTTI, exceptions, and thread-safe statics.

Screenshot of a CMake snippet configuring assembler and C++ compiler flags. It sets CMAKE_ASM_FLAGS to enable assembler preprocessing and CMAKE_CXX_FLAGS to disable RTTI, exceptions, and thread-safe statics for optimized builds.

Linker Configuration

This snippet sets linker flags to specify the linker script, memory map output, garbage collection of unused sections, library groups, and memory usage printing for the build.

Screenshot of a CMake snippet configuring assembler and C++ compiler flags. It sets CMAKE_ASM_FLAGS to enable assembler preprocessing and CMAKE_CXX_FLAGS to disable RTTI, exceptions, and thread-safe statics for optimized builds.

STM32CubeMX Generated Project Structure

Two types of projects could be generated, single context-projects and multi-context projects.

Multi-Context project is generated for: - Dual core products - Flash less products - Secure products

In the next figure you can see the difference between these two types of projects.

Diagram illustrating the generated CMake project structure with different project types. The top level contains CMakeLists.txt, gcc-arm-none-eabi.cmake, and CMakePresets.json. It shows a single-context project on the left with one CMakeLists.txt and CMakePresets.json. On the right, two multi-context projects (Context 2 and Context 3) each have their own CMakeLists.txt and CMakePresets.json. Arrows indicate external project additions. A legend explains that single context projects are shown in dark blue and multi-context projects in pink. Notes mention that each subproject is self-contained, has its own preset file, and uses the same toolchain file

For the multi context projects you will find the same files described before but with more specificities:

  • A couple of top level Cmakelists.txt and CMakePresets.json) (all subprojects will inherit from those two files)

  • A couple of specific Cmakelists.txt and CMakePresets.json) for each sub project where you can add/modify settings for only the related sub-project