Embedded HandbookApps
All sections › XIII. Other languages and their build tools
STM32F103, STM32F407 · level intermediate · 14 min read
Open in the interactive handbook

115а. A CMake firmware project: from .ioc to VS Code

Section 115, in subsection 115.4, built the firmware with CMake using a toolchain file and presets — with our own startup code, without HAL. Section 4в built a program with a Makefile and our own linker script, and section 145 (145.4) built a utility for the PC with CMake and Ninja. Here we look at the project that STM32CubeMX creates when you choose CMake in it: what lies in each file, what the next generation will overwrite, and how to grow it into a project for two boards with HAL as a separate library, a memory map, .hex and .bin.

The experiment is an LED blink on HAL for BluePill (STM32F103C8, pin PC13) and STM32F4 Discovery (STM32F407VG, pin PD12). The projects were created by STM32CubeMX 6.18.1 from the command line and built with CMake 3.25.1, Ninja 1.11.1 and arm-none-eabi-gcc 12.2.1 in the stm-arm:1 image. HAL for F103 comes from the STM32Cube FW_F1 V1.8.7 package (HAL 1.1.10), for F407 from the Keil STM32F4xx_DFP 2.17.1 pack (HAL 1.8.1), and the core headers core_cm4.h from the ARM CMSIS 5.9.0 pack.

Sources: the UM1718 manual (Rev 50) — the CubeMX command line, choosing CMake and the compiler, preserving user code; the CMake templates inside STM32CubeMX 6.18.1; a run of the build and of a repeated generation.

Terms used in the documentation

Term Meaning
toolchain file the toolchain file: what to build with for another machine
presets (CMakePresets.json) ready-made sets of settings: generator, directory, build type
target a CMake target: a program or library with its own flags
INTERFACE, PUBLIC, PRIVATE who gets a target's flags: only those who link it, everyone, or only the target itself
object library a library of object files without a .a archive
generator expression ($<CONFIG:Debug>) an expression CMake evaluates when it generates the build
LINK_DEPENDS files the link step depends on
POST_BUILD a command run after the target is built
compilation database (compile_commands.json) the list of compile commands for the editor
user code section (USER CODE BEGIN/END) a place in a CubeMX file that generation does not touch

115а.1 What CubeMX generates for CMake

A CMake project from CubeMX: the .ioc file and repeated generation; CMakeLists.txt, the presets, the toolchain file, the linker script and the startup file are not overwritten; the cmake/stm32cubemx list and code outside USER CODE are overwritten; CubeMX does not touch your own code or your own project; the build runs by preset through Ninja into fw.elf and then into hex and bin

In the CubeMX project settings, the Toolchain/IDE item is CMake; next to it a choice of compiler appears: GCC or Starm-Clang. From the command line the same is done by a script:

config load blink103.ioc
project toolchain CMake
project compiler GCC
project generate
exit

The script is passed with the -q switch — then CubeMX runs without a window: STM32CubeMX.exe -q script.txt. The F103 project from such a script got:

File What is in it
CMakeLists.txt the top-level file: the C11 standard, Debug as the default build type, the blink103 target, places for your own files
CMakePresets.json Debug and Release presets, the Ninja generator, the build/<preset> directory
cmake/gcc-arm-none-eabi.cmake the toolchain file: -mcpu=cortex-m3, warning and optimization flags, the path to the linker script
cmake/starm-clang.cmake the same for Starm-Clang
cmake/stm32cubemx/CMakeLists.txt the CubeMX lists: macros, paths, HAL and Core sources
STM32F103xx_FLASH.ld, startup_stm32f103xb.s the linker script and the startup file
Core/, Drivers/ the CubeMX code and a copy of HAL with CMSIS

The first lines of the top-level CMakeLists.txt say that the file is created once and never overwritten afterwards. Among the CubeMX templates there is also mx-generated.cmake marked "Do not modify", but F103 and F407 do not have it: it is included by the templates of projects with several contexts (two cores or TrustZone). For a single-core chip, everything CubeMX overwrites is collected in cmake/stm32cubemx/CMakeLists.txt.

What exactly generation overwrites was checked by an experiment: in a copy of the project a marker was appended to every file, CubeMX ran config load and project generate again, and after that you can see which markers survived:

File Marker after generation
CMakeLists.txt, CMakePresets.json kept
cmake/gcc-arm-none-eabi.cmake, cmake/starm-clang.cmake kept
cmake/stm32cubemx/CMakeLists.txt erased
STM32F103xx_FLASH.ld, startup_stm32f103xb.s kept
syscalls.c, sysmem.c, system_stm32f1xx.c kept
stm32f1xx_it.c, stm32f1xx_hal_conf.h (at the end of the file) erased
main.c inside USER CODE kept
main.c outside USER CODE erased
your own file Core/Src/extra.c kept

There was no Drivers/ directory in the copy — generation copied it again. UM1718 says the same: code preservation applies only to the USER CODE places in CubeMX files, and files you added by hand are not touched by generation. Hence the rule: your code goes in your own files or inside USER CODE, your CMake lines go in the top-level CMakeLists.txt, but not in cmake/stm32cubemx/CMakeLists.txt. That is why in the CubeMX project the blink call sits in the main.c loop:

    /* USER CODE BEGIN 3 */
    blink_step();
  }
  /* USER CODE END 3 */

115а.2 Targets instead of global variables

The CubeMX toolchain file does more than its name says: it writes the core flags into shared variables and the path to the linker script into the link flags:

set(TARGET_FLAGS "-mcpu=cortex-m3 ")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32F103xx_FLASH.ld\"")

Such a file is good for only one chip, and every target gets the shared variables at once. The list cmake/stm32cubemx/CMakeLists.txt is already organized per target: the stm32cubemx library without code (INTERFACE) carries the macros and paths, HAL is built into the object library STM32_Drivers, and the Core files and the startup file are added to the target named after the project.

Our own project for two boards (the fw directory) takes this list as it is, and moves everything that depends on the board out of the toolchain file:

cmake_minimum_required(VERSION 3.22)
project(fw C ASM)
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_EXTENSIONS ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)        # compile_commands.json для редактора

set(BOARD f103 CACHE STRING "плата: f103 или f407")
include(${CMAKE_SOURCE_DIR}/boards/${BOARD}.cmake)   # MX_DIR, LD_SCRIPT, MCU_FLAGS

add_executable(fw)
add_subdirectory(${MX_DIR}/cmake/stm32cubemx stm32cubemx)   # список CubeMX — как сгенерирован

# ядро — всем, кто подключает stm32cubemx: HAL, коду CubeMX, своим модулям
target_compile_options(stm32cubemx INTERFACE ${MCU_FLAGS})

# свой модуль — отдельной библиотекой, со строгими предупреждениями только для себя
add_library(blink STATIC app/blink.c)
target_include_directories(blink PUBLIC app)
target_link_libraries(blink PRIVATE stm32cubemx)
target_compile_options(blink PRIVATE -Wextra -Wconversion -Werror)

target_link_libraries(fw blink)              # без PRIVATE: CubeMX подключает без ключевых слов
target_link_options(fw PRIVATE ${MCU_FLAGS} -T${LD_SCRIPT} --specs=nano.specs
    -Wl,--gc-sections -Wl,-Map=fw.map -Wl,--print-memory-usage)
set_target_properties(fw PROPERTIES LINK_DEPENDS ${LD_SCRIPT})   # правка .ld — перекомпоновка

add_custom_command(TARGET fw POST_BUILD
    COMMAND ${CMAKE_OBJCOPY} -O ihex   $<TARGET_FILE:fw> fw.hex
    COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:fw> fw.bin
    COMMAND ${CMAKE_SIZE} $<TARGET_FILE:fw>)

A board is three lines:

# BluePill: STM32F103C8, 64 КБ флеш-памяти, 20 КБ ОЗУ
set(MX_DIR    ${CMAKE_SOURCE_DIR}/mx/blink103)
set(LD_SCRIPT ${MX_DIR}/STM32F103xx_FLASH.ld)
set(MCU_FLAGS -mcpu=cortex-m3 -mthumb)

The toolchain file cmake/arm-gcc.cmake stays shared: the compiler, objcopy, size, optimization flags and TOOLCHAIN_LINK_LIBRARIES m — the CubeMX list expects this variable. The CubeMX list adds files to the target named ${CMAKE_PROJECT_NAME}, so the target has the same name as the project, fw, and is declared before add_subdirectory.

Why strict flags only for your own library was shown by an experiment: with -Wextra -Wconversion on the whole CubeMX project, F103 produced 4 warnings — 2 in stm32f1xx_hal_flash_ex.c and 2 in syscalls.c; F407 produced 13: 11 in HAL (8 in stm32f4xx_hal_flash_ex.c, 2 in stm32f4xx_hal_exti.c, 1 in stm32f4xx_hal_pwr.c) and 2 in syscalls.c. In blink.c — none. With -Werror on the whole project the build fails on someone else's HAL code; on the blink target it checks only your own.

After an edit to blink.c Ninja takes three steps: compiling blink.c, archiving libblink.a, linking fw.elf. HAL is not rebuilt.


115а.3 The linker script and the map

For CMake the linker script is not a source file but a -T flag, and CMake does not know about it. The experiment: in STM32F103xx_FLASH.ld the stack was increased from 0x400 to 0x800. The CubeMX project responded with ninja: no work to do. — the firmware kept the old stack. Our own project with the LINK_DEPENDS property relinked fw.elf in one step, and RAM grew from 1584 to 2608 bytes — exactly by 0x400.

The -Wl,--print-memory-usage flag prints how full the regions are on every build (F103, Release):

             RAM:        1584 B        20 KB      7.73%
           FLASH:        3116 B        64 KB      4.75%

The -Wl,-Map=fw.map flag writes the map. It contains the memory regions from the script — Flash at 0x08000000 with length 0x00010000 and RAM at 0x20000000 with length 0x00005000 — and the origin of each section:

 .text.blink_step
                0x08000bc0       0x1c libblink.a(blink.c.obj)

The blink_step function is 28 bytes at address 0x08000bc0, from the archive of our own library. arm-none-eabi-size -A breaks the image down by sections: .isr_vector 268 bytes, .text 2792, .rodata 36, .data 12, .bss 32, ._user_heap_stack 1540. The last one is the space for the heap 0x200 and the stack 0x400 from the script, 1536 bytes, plus 4 bytes of alignment to 8; it takes nothing in the image, but it stops the linker from silently giving this RAM to variables. RAM 1584 = 12 + 32 + 1540.

Where the Flash went is shown by arm-none-eabi-nm --size-sort -S: the largest function is HAL_RCC_OscConfig, 824 bytes, then HAL_GPIO_Init at 432 and HAL_RCC_ClockConfig at 312. For comparison: the whole program of section 4в, without HAL and without the C library, is 480 bytes. The -fstack-usage flag from the toolchain file puts a .su file next to each object file: blink_step takes 8 bytes of stack, marked static — the size is known at compile time.


115а.4 After the build: .hex, .bin, size and the Flash limit

The CubeMX project does not produce .hex and .bin: its list cmake/stm32cubemx/CMakeLists.txt has neither objcopy nor a post-build command. Our own project makes them with a POST_BUILD command (the listing in 115а.2). For F103 in Release:

File Size What it is
fw.bin 3116 bytes the Flash bytes in a row, from 0x08000000
fw.hex 8857 bytes Intel HEX: the same bytes as text, with addresses

The first line of fw.hex is :020000040800F2: the upper half of the address, 0x0800. The last is :00000001FF, end of file. The first two words of fw.bin are 0x20005000, the top of the stack (the end of 20 KB of RAM), and 0x08000b75 — the address of Reset_Handler with a one in the low bit: Thumb code.

size without switches prints text 3104, data 12, bss 1572; what goes into Flash is text and data — 3116 bytes, exactly the size of fw.bin.

The Flash limit is set by the FLASH region in the script, and the linker watches it. The experiment: a 65536-byte table was added to the F103 project (and to keep --gc-sections from throwing it out, the -Wl,--undefined flag). The build failed:

ld: region `FLASH' overflowed by 3116 bytes
           FLASH:       68652 B        64 KB    104.75%
collect2: error: ld returned 1 exit status

The overflow is exactly the size of the rest of the firmware: 65536 + 3116 = 68652. No separate threshold is needed in CMake: on overflow the target does not build, and in CI (section 110) the build goes red.


115а.5 Two boards and Debug/Release with presets

The board and the build type are cache variables, and the presets give them names:

        { "name": "f103-debug",   "inherits": "base", "cacheVariables": { "BOARD": "f103", "CMAKE_BUILD_TYPE": "Debug" } },
        { "name": "f103-release", "inherits": "base", "cacheVariables": { "BOARD": "f103", "CMAKE_BUILD_TYPE": "Release" } },

cmake --list-presets shows four: f103-debug, f103-release, f407-debug, f407-release; each builds into its own directory build/<preset>, and the builds do not get in each other's way. Debug is -O0 -g3 and the DEBUG macro (it is added by the expression $<$<CONFIG:Debug>:DEBUG> in the CubeMX list), Release is -Os -g0:

Preset Flash RAM Ninja steps
f103-debug 4868 bytes 1584 bytes 21
f103-release 3116 bytes 1584 bytes 21
f407-debug 6056 bytes 1584 bytes 23
f407-release 4148 bytes 1584 bytes 23

For F407 the report also shows the third region of the script: CCMRAM: 0 GB — this is how ld writes zero. The CubeMX projects as they are (Debug and Release presets) built in 20 and 22 steps — one fewer: they have no libblink.a archive. The .bin images from our own project and from the CubeMX projects matched byte for byte in all four combinations — even though CubeMX does not pass -mthumb and our own project does: for Cortex-M cores GCC emits only Thumb anyway.


115а.6 compile_commands.json and the editor

CMAKE_EXPORT_COMPILE_COMMANDS is enabled in the CubeMX template too, with the comment "for indexing, e.g. by clangd". The build directory gets a compile_commands.json — the full compile command for each file. For f103-debug it has 19 entries: 11 HAL files from Drivers, the startup file in assembly, and the rest are Core and blink.c. For blink.c it has exactly the flags that built the target:

-DDEBUG -DSTM32F103xB -DUSE_HAL_DRIVER -Wall -O0 -Wextra -Wconversion -Werror -mcpu=cortex-m3 -mthumb -std=gnu11

From this file the editor knows the chip macro, the paths to HAL and the C standard — go-to-definition and completion work the same way the compiler builds. Each preset has its own file: for f407-debug the same blink.c gets -DSTM32F407xx and -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard.

VS Code with the CMake Tools extension reads CMakePresets.json and offers the same presets; the clangd language server takes compile_commands.json from the build directory of the selected preset. Flashing and debugging from the editor are the same OpenOCD and GDB as in section 35б, launched by the extension. Repeated builds in CI are sped up by ccache: it is plugged in through the CMAKE_C_COMPILER_LAUNCHER variable. Neither VS Code nor ccache was tested here — they are not in the image.


115а.7 Pitfalls

Editing the linker script changes nothing. ninja: no work to do. — CMake does not know that the .ld takes part in linking. Set the LINK_DEPENDS property on the target.

Your line disappeared after generation. It was in cmake/stm32cubemx/CMakeLists.txt or outside USER CODE. Your own files and lines go in the top-level CMakeLists.txt and in your own directories.

The plain signature for target_link_libraries has already been used. The CubeMX list links libraries without PRIVATE/PUBLIC, and CMake does not allow mixing the two forms on one target. On the project target, also link without a keyword.

-Werror on the whole project — the build fails in HAL. On F103, stm32f1xx_hal_flash_ex.c and syscalls.c do not build; on F407, stm32f4xx_hal_exti.c, stm32f4xx_hal_flash_ex.c, stm32f4xx_hal_pwr.c and syscalls.c. Strict flags only for your own targets.

ERRORGenerated code requires C11 or higher, yet the build goes on. With CMAKE_C_STANDARD 99 the CubeMX list prints this, but message(ERROR …) is not a CMake stop mode, and configuration gets as far as Generating done. Use a standard no lower than 11.

fatal error: core_cm4.h: No such file or directory. CubeMX took the library not from its own repository but from a Keil pack (project SetCustomFwPath): in the pack, Drivers/CMSIS has only Device, without the core headers. Take them from the ARM CMSIS pack.

**regionFLASH' overflowed.** The firmware did not fit in the script's region; the number afterby` says by how much.

CCMRAM: 0 GB. Not an error: this is how --print-memory-usage writes an empty region.


115а.8 Summary

Next: section 115б "The serial port from the computer side: bridges, drivers, terminals, latency" — how the firmware talks to the PC over UART and what stands between them.

Open in the interactive handbook