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? ¶
-
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.
-
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.
-
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.
-
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.
-
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:
-
Tool-owned files - These files should not be modified, they will be overwritten by CubeMX.
-
User-owned files - These files can be modified by the user to manage builds.
-
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:
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: ¶
-
Identify your target name.
-
List source files with correct relative paths.
-
Add them using target_sources() with appropriate scope.
-
Run CMake to configure and build.
How to add include Files/Folders ¶
Add source files with target_sources() and include directories with target_include_directories() .
-
Add source files:
target_sources(<target> <scope>
<source1>
<source2>
...
)
-
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.
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: ¶
-
Identify the target to which you want to add definitions.
-
Choose the appropriate scope:
-
PRIVATE : For the target only.
-
PUBLIC : For the target and dependents.
-
INTERFACE : For dependents only.
-
-
List the macros you want to define.
-
Add conditional macros using generator expressions if needed.
-
Run CMake to apply changes.
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: ¶
-
Identify the target to link libraries to.
-
List the libraries or targets to link.
-
Specify the linkage scope if needed.
-
Add the target_link_libraries() command in your CMakeLists.txt .
-
Run CMake to configure and build the project.
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: ¶
-
Specify the toolchain file when configuring CMake:
cmake -DCMAKE_TOOLCHAIN_FILE=path/to/gcc-arm-none-eabi.cmake .. -
The toolchain file sets up the environment for cross-compilation automatically.
-
Use different toolchain files to switch between GCC and LLVM compilers.
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.
Build output suffix ¶
This snippet sets the output file suffix for assembly, C, and C++ executables to .elf in the CMake build configuration.
Cortex settings ¶
This snippet sets MCU-specific compiler flags for the Cortex-M4 processor, enabling floating-point and specific instruction set options.
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.
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.
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.
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.
For the multi context projects you will find the same files described before but with more specificities:
-
A couple of top level
Cmakelists.txtandCMakePresets.json) (all subprojects will inherit from those two files) -
A couple of specific
Cmakelists.txtandCMakePresets.json) for each sub project where you can add/modify settings for only the related sub-project