STM32 – Part 2: Configuring CMake for Embedded

28.05.2025 9 minutes Author: animator404

In this part of the STM32 series, we will look at configuring CMake for embedded projects using STM32 microcontrollers. This approach allows developers to bypass standard tools like STM32CubeIDE and create lightweight, flexible builds that integrate easily into CI/CD pipelines and cross-platform environments.

CMake

Previously, we looked at the three most common development environments that beginners usually encounter when working with STM32 microcontrollers. These environments not only allow you to write program code in the editor window, but also provide automatic compilation of the program using a single button on the panel.

Everything that happens inside is automated: the environment itself includes header files, processes dependencies, sets compilation parameters, creates a firmware file and can even immediately write it to the microcontroller. This time, we will pay attention to how the process of compiling and building projects occurs in real development in the industry – without the mediation of an IDE and automatic settings.

Continuous Integration Systems (CI/CD)​

In no IT company does the software build process take place manually in an IDE on the computer of the best developer of the month. Instead, a special continuous integration system is used, which runs either in the cloud or on the company’s internal servers.

Such systems can trigger the build process manually or automatically – for example, when they detect new changes in the code base. In addition, they are able to perform other actions before or after the build, but the build process remains their main function.

Typically, such systems have a web interface for convenient management, but in fact they basically just launch external utilities that directly perform the firmware build.

CMake + Ninja​

The build process typically involves using tools such as CMake, a build script generator, and Ninja, a direct builder. Programs written in C or C++ are first compiled, and then the resulting object files are assembled into a final binary using a linker (or compiler).

If you’ve ever had to manually compile and link a project with more than ten files, you’ve probably seen how cumbersome the compile and link commands can be (often called a “toolchain”). In addition, even small changes to one file can lead to lengthy re-compilation of the entire project.

These are the challenges that CMake and Ninja are designed to address. CMake is a tool that allows you to describe the structure of a project, how it will be built, and the tools used. Simply having the source code is not enough to determine what program needs to be built. For example, the same program can be designed for both Windows and a microcontroller — but the process of building it will be completely different due to the differences in hardware architecture.

CMake allows you to specify which program to build, with what tools and with what parameters. Ninja, based on these instructions, performs the build itself. There are also other build tools compatible with CMake, but Ninja is considered the fastest among them — which is why it was chosen.

Below is a flowchart that demonstrates the complete build process in the context of CI/CD.

At first glance, the process above seems unnecessarily complicated, but it actually adds flexibility and automation to the assembly process. Each tool in the flowchart performs a separate function:

  • Git can automatically notify the CI/CD system about new changes in the code.

  • CI/CD starts the capacities (virtual machines) on which the build process will then take place.

  • CMake generates a script file, according to which the builder will then perform the build.

  • Ninja Builder starts the compiler and linker according to this script.

Example​

Let’s consider only the last 2 stages, which you can try yourself locally on your computer. Let’s take the STM32G030F6P6 microcontroller and a simple blink program as an example. So, the project has 3 main files:

  • CMakeLists.txt – the main CMake file

  • main.cpp – the actual program

  • STM32G0_toolchain.cmake – the auxiliary CMake file

CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

set(CMAKE_TOOLCHAIN_FILE "STM32G0_toolchain.cmake")
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

project(blink LANGUAGES C CXX ASM)

# Source files
add_executable(${PROJECT_NAME}
  src/main.cpp
  stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal.c
  stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal_gpio.c
  stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Src/stm32g0xx_hal_cortex.c
  stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Source/Templates/system_stm32g0xx.c
  stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Source/Templates/gcc/startup_stm32g030xx.s
)

# Directories with headers
target_include_directories(${PROJECT_NAME} PRIVATE
  include
  stm32_g0_hal/Drivers/CMSIS/Include
  stm32_g0_hal/Drivers/STM32G0xx_HAL_Driver/Inc
  stm32_g0_hal/Drivers/CMSIS/Device/ST/STM32G0xx/Include)

# Linker options
target_link_options(${PROJECT_NAME} PRIVATE
  -Wl,--start-group
  -Wl,--end-group
  -mcpu=cortex-m0
  -mthumb
  -static)

# Define compile options
target_compile_options(${PROJECT_NAME} PRIVATE
  -mcpu=cortex-m0
  -mthumb
  -Os
  -ffunction-sections # Place functions in separate sections
  -fdata-sections # Place data in separate sections
  -fno-exceptions # Disable exceptions for bare-metal code
)

# Define defines here
target_compile_definitions(${PROJECT_NAME} PRIVATE
  STM32G030xx # Define your G0 series MCU
)

# Convert .elf firmware to .bin after build
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND ${CMAKE_OBJCOPY} -O binary ${PROJECT_NAME} ${PROJECT_NAME}.bin
)

# Print firmware Flash and RAM usage
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
  COMMAND ${CMAKE_SIZE} ${PROJECT_NAME}
)

main.cpp

#include "stm32g0xx_hal.h"

extern "C" void SysTick_Handler(void);
void GPIO_PA4_Init();

int main() {
  HAL_Init();
  GPIO_PA4_Init();

  while (1) {
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_4);
    HAL_Delay(100);
  }
  return 0;
}

void SysTick_Handler(void) { HAL_IncTick(); }

void GPIO_PA4_Init() {
  __HAL_RCC_GPIOA_CLK_ENABLE();
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

STM32G0_toolchain.cmake

# Set the C and C++ standards
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)

set(CMAKE_SYSTEM arm-cortex-m0plus)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR cortex-m0plus)

set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

set(CMAKE_EXE_LINKER_FLAGS
  "-T ${CMAKE_CURRENT_SOURCE_DIR}/STM32G030F6PX_FLASH.ld -Wl,--gc-sections --specs=nano.specs"
  CACHE INTERNAL ""
)



Let’s take a quick look at some of the instructions used in the CMakeLists.txt file above:

  • project – superficially describes the project – the name of the project, the programming languages ​​used.

  • set – simply writes some value to a variable, which will then be used later.

  • add_executable – declares the name of the binary file that we want to get and its c/cpp files.

  • target_include_directories – here we list the folders with headers.

  • target_link_options – we list the parameters we need for the linker.

  • target_compile_options – we list the parameters for the compiler, for example, for which platform to compile.

  • target_compile_definitions – you can set definitions right here, which will be visible in the code.

  • add_custom_command – you can call any other utility at different stages of the build.

The test project is published on GitHub. The project file structure looks like this:

blink                         - рут-тека проєкту
├── include                   - тека з власними хедерами
│   └── stm32g0xx_hal_conf.h  - хедер скопійований з stm32_g0_hal з налаштуваннями МК
├── src                       - основні сорс-файли тут
│   └── main.cpp              - сама блінк програма
├── stm32_g0_hal              - CMSIS та HAL драйвера
├── CMakeLists.txt            - власне головний файл CMake
├── STM32G030F6PX_FLASH.ld    - лінкер скрипт
└── STM32G0_toolchain.cmake   - додатковий файл CMake з описом тулчейну

The stm32_g0_hal directory includes CMSIS and HAL and is connected to the project as a git submodule that links to the official STMicroelectronics repository. Information about the toolchain used is moved to a separate file STM32G0_toolchain.cmake instead of placing it directly in CMakeLists.txt, which is done for convenience.

Video demonstration of building an LED blinking program

As you can see from the video, you can compile the program yourself and upload it to the microcontroller without using a graphical IDE. For the example, I specifically chose nvim as the IDE to demonstrate how you can develop entirely in the terminal: edit the code, compile and upload the firmware to the microcontroller.

A little about Neovim (nvim)

Neovim is a fork of the well-known console editor vim, but it is distinguished by its extended support for plugins, a large number of which have already been created thanks to an active community. Thanks to these plugins, a basic text editor acquires the functionality of a full-fledged IDE.

To implement syntax highlighting and convenient code navigation, the same approach is used as in the VS Code editor – the LSP (Language Server Protocol), which provides interaction between the development environment and a separate code analysis server. This protocol was created by Microsoft specifically for VS Code, but thanks to its successful architecture it has gained popularity outside of this editor.

Getting started with Neovim can be even easier than with classic vim, because there are already ready-made configurations with pre-installed plugins. Among the most famous such assemblies are LazyVim and LunarVim, which provide the user with a large set of features similar to those offered by traditional graphical IDEs.

LazyVim build and LazyGit plugin for working with Git

Conclusion

So, we found out that there are special tools that simplify the process of building firmware compared to directly using the toolchain. Although their use requires a better understanding of how the compilation and linking process works, in return they provide high flexibility and speed.

It is such tools that are the basis of build systems used in IT companies. It is worth noting that even development environments can also use similar build mechanisms in the background, and if this does not happen automatically, then there is usually an opportunity to manually activate such an option. For example, in STM32CubeIDE there is an opportunity to create a project using CMake instead of the standard template.

Link to the original source: https://solderkid-blog.netlify.app/stm32/neopixel

Subscribe
Notify of
0 Коментарі
Oldest
Newest Most Voted
Found an error?
If you find an error, take a screenshot and send it to the bot.