Using CMake with STM32CubeMX generated projects

This is an example for managing a STM32CubeMX project that uses HAL library by CMake.

The directory layout is something like this:

+-- CMakeLists.txt                            (1)
+-- arm-toolchain.cmake                       (2)
+-- CUBEMX/
    +-- CMakeLists.txt                        (3)
    +-- CUBEMX.ioc
    +-- startup_stm32f030x6.s
    +-- STM32F030K6Tx_FLASH.ld
    +-- include/
    +-- Drivers/
        +-- ...
    +-- Src/
        +-- ...
    +-- Inc/
        +-- ...
+-- b-cm0/                                    (4)
+-- b-build/                                  (5)

The CUBEMX is the STM32CubeMX project. We have to add 3 files which I marked with (1), (2) and (3) above.

In this example target micro-controller is in STM32F030x6 family.

This is CMakeLists.txt in the top-level directory ((1)):

# There are two categories of targets:
#   build  This PC (*_build)
#   host   ARM uC  (*_cm0)
#
# (adopted from gcc terminology)

cmake_minimum_required(VERSION 3.13)

project(STM32Firmware
  DESCRIPTION "Firmware for STM32"
  LANGUAGES C ASM
)

set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Og3 -DUSE_FULL_ASSERT")

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_library(base INTERFACE)
target_compile_options(base
  INTERFACE
    -Wall
    # -Werror
)
target_include_directories(base
  INTERFACE
    include/
)

if(CMAKE_CROSSCOMPILING)
  add_library(base_cm0 INTERFACE)
  target_link_libraries(base_cm0
    INTERFACE
      base
      c
      m
      nosys
  )
  target_compile_options(base_cm0
    INTERFACE
      -mcpu=cortex-m0
      -mthumb
      -fdata-sections
      -ffunction-sections
  )
  target_link_options(base_cm0
    INTERFACE
      -mcpu=cortex-m0
      -mthumb
      -specs=nano.specs
      -Wl,--gc-sections
  )
  target_compile_definitions(base_cm0
    INTERFACE
      USE_HAL_DRIVER
      STM32F030x6
  )
  set(plat cm0)
else()   # if(CMAKE_CROSSCOMPILING)
  add_library(base_build INTERFACE)
  target_link_libraries(base_build
    INTERFACE
      base
  )
  target_compile_options(base_build
    INTERFACE
      -fno-omit-frame-pointer
      -fsanitize=address
      -fsanitize=undefined
      # -fanalyzer  # gcc >= 10
  )
  target_link_options(base_build
    INTERFACE
      -fno-omit-frame-pointer
      -fsanitize=address
      -fsanitize=undefined
  )
  set(plat build)
endif()  # if(CMAKE_CROSSCOMPILING)

# add HAL target
function(add_hal_target target)
  file(GLOB_RECURSE
    ${target}_files
    "Drivers/*.c"
  )

  add_library(${target}
    ${${target}_files}
  )
  target_link_libraries(${target}
    PUBLIC
      base_${plat}
  )
  target_include_directories(${target}
    PUBLIC
      Inc/
      Drivers/STM32F0xx_HAL_Driver/Inc/
      Drivers/STM32F0xx_HAL_Driver/Inc/Legacy
      Drivers/CMSIS/Device/ST/STM32F0xx/Include
      Drivers/CMSIS/Include
  )
endfunction()

# auxiliary targets to make it easy to upload the firmware using `st-flash`
# utility
function(uc_add_support_targets target)
  # show size of produced executable for all targets
  add_custom_target(${target}_size
    ALL
    COMMAND ${UTL_SIZE} ${target}
    DEPENDS ${target}
    COMMENT "Size of ${target}"
  )

  add_custom_command(
    TARGET ${target}
    POST_BUILD
    COMMAND ${UTL_OBJCOPY} -O ihex ${target} ${target}.hex
    BYPRODUCTS ${target}.hex
    COMMENT "Generating ihex output format"
  )

  add_custom_target(${target}_bin
    ${UTL_OBJCOPY} -O binary -S ${target} ${target}.bin
    DEPENDS ${target}
    BYPRODUCTS ${target}.bin
    COMMENT "Generating binary output format"
  )

  add_custom_target(${target}_upload
    st-flash --reset write ${target}.bin 0x8000000
    DEPENDS ${target}.bin
    COMMENT "Uploading ${target}"
  )

  add_custom_target(${target}_erase_upload
    st-flash erase && st-flash --reset write ${target}.bin 0x8000000
    DEPENDS ${target}.bin
    COMMENT "Uploading ${target}"
  )
endfunction()

add_subdirectory(CUBEMX)

There are two categories of targets: one for the micro-controller and the other for the build machine (the machine that is currently building the sources). Tests for the software components should be compiled and run on the build machine (running tests on a micro-controller doesn't make sense for me). We enable address and undefined behavior sanitizers and if you're using gcc version 10 or newer, you can also enable the static analyzer using -fanalyze compiler option.

CMakeLists.txt inside the CUBEMX directory ((3)):

if(NOT plat STREQUAL build) # code for micro-controller
  set(
    cubemx_ld_script
    "${CMAKE_CURRENT_SOURCE_DIR}/STM32F030K6Tx_FLASH.ld"
  )

  add_hal_target(cubemx_hal_${plat})

  add_executable(cubemx_${plat}
    Src/main.c
    Src/stm32f0xx_hal_msp.c
    Src/stm32f0xx_it.c
    Src/system_stm32f0xx.c

    Inc/main.h
    Inc/stm32f0xx_hal_conf.h
    Inc/stm32f0xx_it.h

    startup_stm32f030x6.s
  )
  target_link_libraries(cubemx_${plat}
    PUBLIC
      base_${plat}
      cubemx_hal_${plat}
      cubemx
  )
  target_link_options(cubemx_${plat}
    PUBLIC
      -T${cubemx_ld_script}
  )
  set_target_properties(cubemx_${plat}
    PROPERTIES
      LINK_DEPENDS ${cubemx_ld_script}
  )

  uc_add_support_targets(cubemx_${plat})
else() # if(NOT plat STREQUAL build)
  ## code for build machine (tests)
  #
  # # Here you can add tests of your software components
  # # I commented out this section for simplification.
  # add_executable(component-a.test_${plat}
  #   Test/componenet-a.test.c
  # )
  # target_link_libraries(component-a.test_${plat}
  #   PRIVATE
  #     base_${plat}
  # )
endif()

To enable cmake to do the cross-compilation, we need a toolchain file in the top-level directory ((2)):

# arm-toolchain.cmake

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_PREFIX arm-none-eabi-)

set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++)
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER})
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -x assembler-with-cpp")

set(UTL_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy)
set(UTL_SIZE ${TOOLCHAIN_PREFIX}size)

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

Build

I highly recommend to use Ninja build system as the generator for cmake. (You can find the package name for you OS here). It's much faster than make. But if you don't want the Ninja generator, just omit the -G Ninja option from the following commands.

We build firmware in b-cm0 directory ((4)) and tests in b-build directory ((5)).

Firmware

To build the firmware:

  1. Prepare the build directory (should be done only once):
# in top-level directory
mkdir b-cm0
cd b-cm0
cmake -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_TOOLCHAIN_FILE=../arm-toolchain.cmake ..
  1. Build the target
cmake --build . --target cubemx_cm0

After building the firmware, it will show the output of size utility.

  1. Upload the firmware (using st-flash program)
cmake --build . --target cubemx_cm0_upload

# or,
# cmake --build . --target cubemx_cm0_erase_upload  # first erase the chip, and then upload

Tests

If you have tests (which you must have in any non-trivial project), you can build them easily:

# in top-level directory
mkdir b-build
cd b-build
cmake -G Ninja ..
cmake --build .

More notes

You can manage more than one STM32CubeMX project easily; you only need to add one extra add_subdirectory(...) at the end of top-level CMakeLists.txt ((1)), and one CMakeLists.txt file inside new project directory (equivalent to (3)).

You can also use CMake to run the tests.