CMake

Efficient and reliable build systems are essential in embedded development to accelerate product development and maintain code quality. For STM32 developers, mastering the build process is fundamental to fully leveraging STM32 microcontrollers and their extensive ecosystem.

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

This page introduces 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

STM32CubeIDE and Eclipse/CDT are popular development environments. However, relying solely on graphical user interface (GUI)-driven projects can limit flexibility. CMake enables developers to generate project files for multiple IDEs or build directly from the command line, offering freedom to select 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 toolchain abstraction enables developers to switch seamlessly between toolchains without rewriting build scripts. This flexibility allows STM32 developers to port projects between toolchains more easily.

  1. Automation and continuous integration ready

Automated builds and testing pipelines are key to maintaining code quality and accelerating delivery in embedded development. 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 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 projects. This page guides readers step-by-step through essential CMake concepts and practical examples tailored to STM32 development, ensuring a smooth onboarding experience.

Further reading

For more detailed information on CMake, see the official CMake online documentation.

For more information on CMake integration and usage, see the CMake integration in STM32CubeMX and usage in STM32CubeIDE for Visual Studio Code community article.

Create a CMake project

To create a new CMake project and open it in Visual Studio Code, follow the Import STM32CubeMX project guide.

After the project is created and imported into Visual Studio Code, revisit this page for additional CMake information.

STM32CubeMX generated files

STM32CubeMX generates two types of CMake files:

  1. Tool-owned files - These files must not be modified. CubeMX overwrites them.

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

Screenshot of the selection of files in an 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

Create a configuration and build preset in the CMakePresets.json file. The following is an example of preset configuration:

"name": Machine-friendly name of the preset
"hidden": Specifies whether a preset is hidden
"inherits": Preset name to inherit from
"condition": Condition to use this preset
"vendor": Vendor name
"displayName": Human friendly name
"generator": Generator to use for the preset
"toolchainFile": Path to the toolchain file
"cacheVariables": {
    "CMAKE_BUILD_TYPE": Cache variable embedding the machine-friendly name of  the preset
}

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

After creating the configuration preset, create a build preset in the same file. An example is shown below:

Screenshot of an example build preset configuration.

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

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

CMakeLists.txt

CMakeLists.txt is generated once. Any user modification is preserved if STM32CubeMX or other ST tools regenerate assets.

Two CMakeLists.txt files are available in the project:

  • A generic CMakeLists.txt in the main project folder

  • An MX-Generated CMakeLists.txt included by the generic file, containing STM32CubeMX project configuration.

How to add source files

Use relative paths intead of absolute paths to facilitate project sharing across development environments.

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

Syntax:

target_sources(<target> <scope>
  <source1>
  <source2>
  ...
)
  • <target>: Target name, such as an executable or library

  • <scope>: PRIVATE, PUBLIC, or INTERFACE

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

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 the target name.

  2. List source files with correct relative paths.

  3. Add the files using target_sources() with the appropriate scope.

  4. Run CMake to configure and build the project.

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 and 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 file 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 the target_compile_definitions() command to add preprocessor macros (compile-time symbols) to a target.

Syntax

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

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

  • <definition>: Preprocessor definitions (macro), optionally with a value or generator expression.

Example

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

Explanation

  • USE_HAL_DRIVER and STM32C011xx are macros defined for all builds.

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

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

Steps

  1. Identify the target to add definitions to.

  2. Choose the appropriate scope:

    • PRIVATE: For the target only.

    • PUBLIC: For the target and dependents.

    • INTERFACE: For dependents only.

  3. List the macros to define.

  4. Add conditional macros using generator expressions if required.

  5. Run CMake to apply the changes.

Screenshot of a CMakeLists.txt snippet using target_compile_definitions() to add preprocessor macros such as 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 the target_link_libraries() command to specify libraries to link to a target.

Syntax

target_link_libraries(<target> <scope>
  <library1>
  <library2>
  ...
)
  • <target>: Target to link libraries to (executable or library).

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

  • <library>: Name of the library or target 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.

  • The PRIVATE scope 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 required.

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

  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, for example 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, select one of the following options: - 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 enforcing 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," enforces 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 build optimization settings depend on the selected context. By default, only release and debug configurations are available. If additional build contexts are required, you can add more configurations in this area.

Screenshot of a CMakeLists.txt snippet that configures build optimization flags. The snippet 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 (no optimization, the default if no optimization level is specified)

  • -O1 (minimal optimization, prioritizing compilation time)

  • -O2 (more optimization, without speed or size trade-off)

  • -O3 (increased optimization, prioritizing speed)

  • -Ofast (very aggressive optimization that may break standard compliance and change program behavior)

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

  • -Os (optimizes for size. -Os enables all -O2 optimizations that do not typically increase code size and performs further optimizations to reduse 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 that configures assembler and C++ compiler flags. The snippet 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

STM32CubeMX can generate two types of projects; single-context projects and multi-context projects.

A multi-context project is generated for the following cases: - Dual-core products - Flashless products - Secure products

The following figure illustrates the differences 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. The diagram 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 multi-context projects, you will find the same files described above, with additional specificities:

  • A top-level Cmakelists.txt and CMakePresets.json file (all subprojects inherit from these files)

  • A specific Cmakelists.txt and CMakePresets.json for each subproject, where you can add or modify settings for only the related subproject