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.ymlthe 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
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.
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
- PlatformIO installs the platform, toolchain and CMSIS by itself; one firmware —
one environment per board. The
pio runreport does not count the vector table: 388 and 396 bytes in the report, 664 and 796 for the whole image. - Pin the platform version: ststm32 20.0.0 brings GCC 7.2.1.
- west in Zephyr is a manifest of repositories and a build with one command.
- CMake is a toolchain file and presets: 232 and 252 bytes with our own startup code.
- In all five tools the main thing is the same: versions in a file, the build with one command.
Next: section 115а "A CMake firmware project: from .ioc to VS Code" — a CubeMX project for CMake.