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

115. Build managers for firmware: PlatformIO, west (Zephyr), CMake presets

Alire for Ada and Cargo for Rust do the same job: they install the compiler, fetch dependencies of the right versions and build the project with one command. C for microcontrollers has no such single tool, and the role is taken on by PlatformIO, west for Zephyr, or CMake with a toolchain file and presets. This section builds one and the same firmware — the CRC-32 and the blink from sections 113 and 114 — with PlatformIO and with CMake presets, for BluePill and STM32F4 Discovery, in the stm-pio:1 image, and shows where each one pins its versions.

Ada and Alire are section 113, Rust and Cargo are section 114, CI/CD is section 110.

Terms used in the documentation

Term Meaning
environment ([env:…]) a PlatformIO environment: board, platform, flags
platform, framework the kit for a chip family, a library on top of the registers
package, tool a PlatformIO package: toolchain, framework, tool
manifest, west.yml the list of repositories and their versions in Zephyr
workspace the west working tree with all projects
toolchain file the CMake toolchain file: what to build with for another machine
presets (CMakePresets.json) ready-made sets of CMake settings
generator (Ninja) what CMake uses to run the build
cross-compilation building on one machine for another
pinning fixing a version

115.1 PlatformIO: one firmware, two boards

Three ways to build one firmware: what each installs by itself and where the versions are pinned

A PlatformIO project is a platformio.ini file and a src directory. Each environment is a board of its own; the platform, framework and flags are shared:

[env]
platform = ststm32@20.0.0             ; версия закреплена: иначе завтра приедет другая
framework = cmsis
build_flags = -Wall -Wextra -Os

[env:bluepill]
board = bluepill_f103c8

[env:disco407]
board = disco_f407vg

pio run installed by itself the ststm32 20.0.0 platform, the toolchain toolchain-gccarmnoneeabi 1.70201.0 — that is GCC 7.2.1 — CMSIS for F1 4.3.5 and for F4 2.6.11 with startup files. The pio run report gives for BluePill 388 bytes of Flash and 28 bytes of RAM, for Discovery 396 and 28. But as Flash it counts only .text, .rodata and .data, and it leaves out the vector table (.isr_vector, 268 and 392 bytes) and the eight bytes of .init_array and .fini_array: the whole image is 664 and 796 bytes. The code learns the chip from a macro defined by the board:

#if defined(STM32F103xB)

115.2 Versions: what to pin

An unpinned PlatformIO platform will arrive in a new version tomorrow — with a different compiler and a different image size. So the version is written right in platform = ststm32@20.0.0. The default toolchain of this platform is GCC 7.2.1, which is far from new: a different compiler is set as a separate package in platform_packages. In Alire the same is done by the line gnat_arm_elf = "=16.1.0"; in Cargo the crate versions are pinned by Cargo.lock, and the compiler itself by the rust-toolchain.toml file with the version and targets: rustup installs them by itself on the first build.


115.3 west and Zephyr

Zephyr is an OS for microcontrollers, and its projects are built by west. The west.yml file lists the repositories — the kernel itself, HAL modules, libraries — and the revision of each; west update brings the working tree to them, and west build -b <board> builds a CMake project with the board description from the device tree. A Zephyr working tree takes gigabytes, so it is not built here; it works the same way as everything in this section: versions in the manifest, the build with one command.


115.4 CMake: the toolchain file and presets

Without a manager the same firmware is built with CMake. The toolchain file says that we are not building for this machine, and what to build with:

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)   # пробная сборка без компоновки: у голого камня нет libc-заглушек
file(GLOB _pio_gcc "$ENV{HOME}/.platformio/packages/toolchain-gccarmnoneeabi*/bin")
find_program(CMAKE_C_COMPILER arm-none-eabi-gcc HINTS ${_pio_gcc} REQUIRED)
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})

The key line is CMAKE_C_COMPILER: without it CMake takes this machine's compiler — the one that builds programs for the PC. Here arm-none-eabi-gcc is looked for first in the PlatformIO package, then in PATH. The core switches — -mcpu=cortex-m3 -mthumb for F103 and -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 for F407 — are set by CMakeLists.txt according to the MCU variable, and that variable is passed by the presets.

Presets reduce long command lines to a name:

    { "name": "f103", "inherits": "base", "cacheVariables": { "MCU": "f103" } },
    { "name": "f407", "inherits": "base", "cacheVariables": { "MCU": "f407" } }

cmake --preset f103 and cmake --build --preset f103 built the firmware with the same GCC 7.2.1 from the PlatformIO package, with our own startup code from section 113: 232 bytes for F103 and 252 for F407. That is the whole image, with the vector table, and it must be compared with the PlatformIO image — 664 and 796 bytes. The difference is the chip's vector table in the CMSIS startup file (268 and 392 bytes against our four words) and its longer startup code: __libc_init_array alone is 72 bytes. In the experiment: CMake 4.4.3, PlatformIO 6.2.0.

The size of one firmware with PlatformIO and with CMake on two chips


115.5 What to choose

Installs the toolchain Versions pinned in One command
PlatformIO yes platformio.ini pio run
west (Zephyr) no, the SDK separately west.yml west build
CMake presets no the image or the SDK cmake --build --preset
Alire (Ada) yes alire.toml alr build
Cargo (Rust) via rustup from rust-toolchain.toml Cargo.lock and rust-toolchain.toml cargo build

For CI (section 110) the most convenient is whatever builds in a container with one command and with pinned versions — any of the five, as long as the versions really are pinned.


115.6 Pitfalls

It built yesterday, not today. The PlatformIO platform is not pinned and got updated. Write ststm32@20.0.0.

The compiler turned out to be old. The default toolchain of ststm32 20.0.0 is GCC 7.2.1.

CMake fails on the compiler check. The test build tries to link a program for a bare chip; set CMAKE_TRY_COMPILE_TARGET_TYPE = STATIC_LIBRARY.

The linker script cannot find common.ld. INCLUDE searches the -L paths.

A build on a laptop and in CI gives different images. The toolchain is not from the image and not pinned.


115.7 Summary

Next: section 115а "A CMake firmware project: from .ioc to VS Code" — a CubeMX project for CMake.

Open in the interactive handbook