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,PRIVATEwho 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 .aarchivegenerator expression ( $<CONFIG:Debug>)an expression CMake evaluates when it generates the build LINK_DEPENDSfiles the link step depends on POST_BUILDa 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
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
- CubeMX 6.18.1 for CMake creates the top-level
CMakeLists.txt, Debug and Release presets, a toolchain file and the listcmake/stm32cubemx/CMakeLists.txt. Repeated generation overwrites the list and the code outsideUSER CODE; it does not touch the top-level file, the presets, the toolchain, the.ldor the startup file. - Your own project includes the CubeMX list as it is; the core flags and the script come from
the board; strict warnings only for your own library: on the whole CubeMX project
-Wextra -Wconversiongive 4 warnings on F103 and 13 on F407, and none in your ownblink.c. LINK_DEPENDS: after an edit to the.ldthe stack grew by 0x400, RAM 1584 → 2608 bytes; without it —no work to do.- F103 Release: Flash 3116 bytes out of 64 KB (4.75 %),
fw.bin3116 bytes,fw.hex8857; with an extra 65536 bytes —overflowed by 3116 bytes. - Four presets: 4868 and 3116 bytes on F103, 6056 and 4148 on F407; the images are byte for byte the same as those of the CubeMX projects.
compile_commands.jsonhas 19 commands forf103-debug, with the same flags as the compiler.
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.