Tools · 10 pages
Build systems
CMake, Make, Meson, west, scons — what to use for a new Cortex-M project, and how to glue the toolchain together.
A new Cortex-M project today should use CMake. It's not the prettiest build system, but it's universally supported by IDEs, vendor SDKs, CI systems, and toolchains. Once you have it set up, you'll spend more time writing code than wrestling with builds — which is the actual goal.
The minimum viable CMake
cmake_minimum_required(VERSION 3.20)
# Toolchain — must come before project()
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_C_COMPILER_WORKS 1) # skip CMake's host test compile
set(CMAKE_CXX_COMPILER_WORKS 1)
# CPU flags
set(CPU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_C_FLAGS_INIT "${CPU_FLAGS} -ffunction-sections -fdata-sections -Wall -Wextra")
set(CMAKE_CXX_FLAGS_INIT "${CPU_FLAGS} -ffunction-sections -fdata-sections -fno-exceptions -fno-rtti -Wall -Wextra")
set(CMAKE_ASM_FLAGS_INIT "${CPU_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS_INIT "${CPU_FLAGS} -Wl,--gc-sections -Wl,-Map=output.map -nostartfiles --specs=nano.specs")
project(firmware C CXX ASM)
add_executable(firmware
src/main.c
src/startup.c
drivers/uart.c
)
target_include_directories(firmware PRIVATE include)
target_link_options(firmware PRIVATE -T${CMAKE_SOURCE_DIR}/linker.ld)
# Generate .bin and .hex outputs + print size
add_custom_command(TARGET firmware POST_BUILD
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:firmware> firmware.bin
COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:firmware> firmware.hex
COMMAND ${CMAKE_SIZE} $<TARGET_FILE:firmware>
)Save as CMakeLists.txt. Build:
cmake -B build -G Ninja
cmake --build buildThat's it. You'll get build/firmware (ELF), build/firmware.bin, build/firmware.hex, and a size summary printed at the end.
Using a toolchain file
For cleaner separation, factor the toolchain bits into a separate arm-gcc-toolchain.cmake:
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR ARM)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)
set(CMAKE_OBJCOPY arm-none-eabi-objcopy)
set(CMAKE_SIZE arm-none-eabi-size)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)Then invoke CMake with:
cmake -B build -G Ninja --toolchain arm-gcc-toolchain.cmakeThe toolchain file is reusable across projects.
Configuration types
CMake's default is single-config. For multi-config (Debug/Release/MinSizeRel):
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "MinSizeRel")
endif()
set(CMAKE_C_FLAGS_DEBUG_INIT "-Og -g3 -DDEBUG")
set(CMAKE_C_FLAGS_RELEASE_INIT "-O3 -DNDEBUG")
set(CMAKE_C_FLAGS_MINSIZEREL_INIT "-Os -DNDEBUG")Most embedded projects use MinSizeRel for production and Debug for development.
Vendor SDKs
STM32CubeMX → CMake
STMicro's CubeMX generates Makefiles or vendor IDE projects. To use CMake instead, project tooling has caught up: the STM32CubeIDE → CMake templates, or stm32-cmake, wrap the Cube HAL as importable CMake targets.
find_package(CMSIS COMPONENTS STM32F407VG REQUIRED)
find_package(HAL COMPONENTS STM32F407VG RCC GPIO UART REQUIRED)
target_link_libraries(firmware PRIVATE
CMSIS::STM32::F407VG
HAL::STM32::F4::RCC
HAL::STM32::F4::GPIO
HAL::STM32::F4::UART
)Nordic nRF Connect SDK
Built on CMake + west (the Zephyr meta-tool). Use it as-is — they've done the work.
Raspberry Pi Pico SDK
CMake-native. Add their SDK as a submodule and include(pico_sdk_import.cmake). Excellent reference for a clean modern CMake setup.
NXP MCUXpresso SDK
Ships as either Eclipse projects or CMake-buildable. Pick the CMake variant and skip the Eclipse IDE.
Make — when it still makes sense
If you have a small, single-target project and don't need IDE integration, a plain Makefile is fine and faster to read:
TARGET = firmware
CPU = -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard
CFLAGS = $(CPU) -Os -ffunction-sections -fdata-sections -Wall
LDFLAGS = $(CPU) -Wl,--gc-sections -nostartfiles -T linker.ld --specs=nano.specs
SRCS = src/main.c src/startup.c
OBJS = $(SRCS:.c=.o)
$(TARGET).elf: $(OBJS)
arm-none-eabi-gcc $(LDFLAGS) -o $@ $^
%.o: %.c
arm-none-eabi-gcc $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(TARGET).elfFor anything beyond hobby scale, CMake wins because of:
- IDE integration (CLion, VS Code with CMake Tools, Qt Creator).
- Dependency management (vendor SDKs).
- Cross-platform reproducibility (Windows + macOS + Linux + CI).
Meson — the contender
Meson is a younger build system with arguably nicer syntax than CMake. Cortex-M support exists (meson cross files) but the ecosystem is smaller. Excellent for pure-application projects; less excellent when you need to pull in vendor SDKs that ship CMake files.
west — Zephyr's meta-build
If you're on Zephyr, west is your build entry point. It wraps CMake under the hood:
west init -l my-app
west update
west build -b nucleo_h745zi_q my-app
west flashYou still configure your application with CMake + Kconfig, but west handles the workspace, the dependencies, and the per-board build directory structure.
CI for embedded
A typical CI job:
- run: |
cmake -B build -G Ninja --toolchain arm-gcc.cmake
cmake --build build
arm-none-eabi-size build/firmware.elf > size.txt
cat size.txtAdd static analysis (cppcheck, clang-tidy) and a "size budget" guard:
- name: Enforce flash budget
run: |
size=$(arm-none-eabi-size -d build/firmware.elf | awk 'NR==2 {print $1}')
if [ $size -gt $((128*1024)) ]; then
echo "Flash usage $size exceeds 128 KB budget"; exit 1
fiHardware-in-the-loop (HIL) tests are an upgrade — connect a board to your CI runner, flash from CI, run integration tests. Worth doing for any product that's gone past prototype.
See also
- Toolchains — the flags CMake invokes.
- Linker scripts — referenced by
target_link_options. - Zephyr — uses
weston top of CMake.