Cmake Helpers

Cmake helper modules help to create cross-compiled builds for AVR / STM32 / x86. The goal is to reuse working solutions in different projects and minimize duplicate work such as:

  • Setting up paths to source files and libraries
  • Enabling of unit testing on x86 platform for embedded platform code
  • Re-typing boilerplate code around initialization and checks

Platforms and requirements

Most of the dependent packages can be installed using system package manager. When a package manager doesn’t support a dependency, the package is downloaded from GitHub (for example, gtest) or directly from a developer’s website (for example, Arm GNU Embedded Toolchain, FreeRTOS).

Example Project

Directory “example” contains a simulated cross-compilation project. Within it:

  • src/avr/main.cpp
    Uses AVR-based microcontroller to blink a built-in LED and call a function Example::hello()
  • src/stm32/main.cpp
    Uses libopencm3 and FreeRTOS to start a task which blinks a built-in LED and calls Example::hello() while running on STM32F103C8 microcontroller
  • src/x86/main.cpp
    Calls Example::hello() while running on MacOS or Linux

Project Structure

Cloned repository contains this directory branch with an example project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
example/
|-- CMakeLists.txt
|-- make.sh
|-- src
    |-- avr
        |-- CMakeLists.txt
        |-- main.cpp
    |-- common
        |-- example.cpp
        |-- example.hpp
    |-- stm32
        |-- CMakeLists.txt
        |-- FreeRTOSConfig.h
        |-- main.cpp
        |-- opencm3.c
        |-- stm32f103c8t6.ld
    |-- x86
        |-- CMakeLists.txt
        |-- main.cpp
|-- test
    |-- CMakeLists.txt
    |-- main.cpp
    |-- example_test.cpp

Building

make.sh is used to start cmake build. To build binaries for all three platforms (AVR/STM32/x86) and unit tests, run:

1
2
cd example
./make.sh -x -s -a -t

The script creates a separate output directory per platform and then calls cmake/make:

1
2
3
4
5
6
example/
|-- ...
|-- build-avr
|-- build-stm32
|-- build-x86
|-- ...

Testing

To run an example unit test (defined in test/example_test.cpp), run:

1
2
cd build-x86
make test

Uploading

Once the build is complete, upload the firmware from within the target platform directory. For example, for avr microcontroller:

1
2
cd build-avr
make example-flash

Details

The following sections describe the functionality in a context of the example project above.

example/make.sh

The script makes it convenient to set up the environment and pass the required parameters to cmake. The parameters include project’s home, cmake-helpers’ home, locations of dependent libraries, target platforms for which to build the project. The script can be adapted to other projects by changing the dependencies it refers to:

1
2
3
4
5
6
7
if [ -z ${XTRA_HOME} ]; then
  XTRA_HOME=${PWD}/../xtra
fi

export GTEST_HOME=${XTRA_HOME}/gtest
export LIBOPENCM3_HOME=${XTRA_HOME}/libopencm3
export FREERTOS_HOME=${XTRA_HOME}/FreeRTOSv10.1.1

Command line options:

1
2
3
4
5
6
7
8
Usage: make.sh [-x] [-s] [-a] [-d] [-t] [-p _project_home_] [-h]
        -x - build x86
        -s - build stm32 (board stm32f103c8t6)
        -a - build avr
        -d - pull dependencies
        -t - enable unit tests
        -p - absolute path to projects home
        -h - this help
  • -x, -s, -a
    The flags indicate which platform to build for. Multiple options can be specified. To build unit tests, “-x” must be specified
  • -d
    The flag instructs to update dependent libraries from GitHub. Alternatively, module project_setup.cmake can achieve similar goal but as part of the building process
  • -t
    The flag instructs to build unit tests. Must also specify “-x” when unit tests are made to run on x86

example/CMakeLists.txt

The file sets up project’s name, root directory and module path. It then tells cmake to process the code for starting cross-compiled project in main_project.cmake:

1
2
3
4
project(example)
set(CMAKE_MODULE_PATH $ENV{CMAKEHELPERS_HOME}/cmake/Modules)
set(ROOT_SOURCE_DIR ${PROJECT_SOURCE_DIR})
include(main_project)

example/src/avr/CMakeLists.txt

Cmake processes this file after it determined to build for AVR in main_project.cmake. To build the firmware, cmake re-initializes the environment with GCC AVR toolchain, which specifies its own build parameters and procedures. These instructions are given in avr_project.cmake.

See gcc_avr_toolchain.cmake for AVR compiler and linker flags for AVR boards.

Here, CMakeLists.txt instructs to set up for a specific AVR board connected to some port, and that it needs to build an executable:

1
2
3
4
5
6
7
8
set(CMAKE_MODULE_PATH $ENV{CMAKEHELPERS_HOME}/cmake/Modules)
set(AVR_MCU atmega168)
set(AVR_UPLOADTOOL_PORT "/dev/tty.usbmodemFD14511")

include(avr_project)
setup_avr()
find_srcs()
build_exe(SRCS ${SOURCES})

example/src/stm32/CMakeLists.txt

The same logic described for AVR above, applies here to STM32. There are additional steps that cmake goes through due to additional dependencies.

  • stm32_project.cmake defines building instructions for STM32 binaries
  • gcc_stm32_toolchain.cmake aggregates compiler and linker flags for cross-compiling code for stm32f103c8t6
  • libopencm3 is a C library and must be built using make. One option to achieve it is to download and build the library manually. Another option is to use project_setup.cmake module, which can automate the process a bit more
  • FreeRTOS is a real-time embedded OS. The parameters related to it are defined in freertos.cmake
1
2
3
4
5
6
7
8
include(stm32_project)
find_srcs()
...
include(libopencm3)
...
include(freertos)
...
build_exe(SRCS ${SOURCES} LIBS ${LIBS})

example/src/x86/CMakeLists.txt

The file instructs cmake to build a library/executable for x86 platform. The required parameters are defined in x86_project.cmake:

1
2
3
4
5
6
include(x86_project)
setup_x86()
...
find_srcs(FILTER ${MAIN_SRC})
build_lib(SRCS "${SOURCES}" LIBS ${CMAKE_THREAD_LIBS_INIT})
build_exe(OBJS "${SOURCES_OBJ}" SRCS "${MAIN_SRC}" LIBS ${PROJECT_NAME} SUFFIX "-exe")

example/test/CMakeLists.txt

The file tells cmake to build a test executable. The file’s logic is the same as for building a regular program:

1
2
3
4
include(x86_project)

find_test_srcs()
build_exe(SRCS ${SOURCES} LIBS ${PROJECT_NAME} SUFFIX "-tests")

Here, the file skips the call to setup_x86() as x86 was already configured in main_project.cmake else branch as part of add_subdirectory(“${PROJECT_SOURCE_DIR}/src/${BOARD_FAMILY}”)

The project is configured to use Google test framework. The framework is set up in gtest.cmake and is invoked from x86_project.cmake<

main_project.cmake

Cmake initializes common parameters and procedures via init.cmake, and firmware.cmake. It then determines which build to generate based on variables ${BTR_STM32}, ${BTR_AVR}, and ${BTR_X86}, which are defined and passed by make.sh.

For STM32 and AVR platforms, cmake switches the toolchain and generates a cross-compilation build based on the instructions in stm32/CMakeLists.txt or avr/CMakeLists.txt. Otherwise, cmake continues with the current toolchain to generate the build based on x86/CMakeLists.txt and unit tests based on test/CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
include(init)
include(firmware)

function(add_target_config_args)
  add_target_config(
    SRC_DIR ${PROJECT_SOURCE_DIR}/src/${BOARD_FAMILY}
    BIN_DIR ${PROJECT_BINARY_DIR}/src/${BOARD_FAMILY}
    TOOLCHAIN_FILE ${TOOLCHAIN_FILE}
  ...
endfunction()

if (BTR_STM32 GREATER 0)
  set(TOOLCHAIN_FILE $ENV{CMAKEHELPERS_HOME}/cmake/Modules/gcc_stm32_toolchain.cmake)
  ...
  add_target_config_args(...)
  add_target_build(...)
  add_target_flash(...)

elseif (BTR_AVR GREATER 0)
  set(TOOLCHAIN_FILE $ENV{CMAKEHELPERS_HOME}/cmake/Modules/gcc_avr_toolchain.cmake)
  ...
  add_target_config_args(...)
  add_target_build(...)
  add_target_flash(...)

else()
  ...
  add_subdirectory("${PROJECT_SOURCE_DIR}/src/${BOARD_FAMILY}")

  if (ENABLE_TESTS)
    if (EXISTS "${ROOT_SOURCE_DIR}/test")
      enable_testing()
      add_subdirectory(${ROOT_SOURCE_DIR}/test)
      ...
endif ()

Functions add_target_confg(), add_target_build(), add_target_flash() are defined in firmware.cmake.

init.cmake

The file checks and initializes common variables if undefined:

  • BOARD_FAMILY
  • CMAKE_BUILD_TYPE
  • EXECUTABLE_OUTPUT_PATH
  • LIBRARY_OUTPUT_PATH
  • CMAKE_CXX_STANDARD
  • CMAKE_RULE_MESSAGES
  • CMAKE_VERBOSE_MAKEFILE

project_setup.cmake

The module is used to set up external cmake/make project. It involves:

  • exporting of source, header and/or built library names and locations
  • adding targets, which are defined by the external project, to global scope
  • downloading the files from external source such as GitHub

For example, libopencm3.cmake uses add_project() to set up an external project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
include(project_setup)

set(LIBOPENCM3_HOME $ENV{LIBOPENCM3_HOME})

string(TOLOWER ${STM32_FAMILY} STM32_FAMILY_LOWER)
add_project(
  PREFIX libopencm3
  HOME "${LIBOPENCM3_HOME}"
  SRC_DIR "${LIBOPENCM3_HOME}"
  URL "https://github.com/libopencm3/libopencm3.git"
  BUILD_CMD "make TARGETS=stm32/${STM32_FAMILY_LOWER} VERBOSE=1"
  BUILD_IN 1
  FORCE_UPDATE $ENV{FORCE_UPDATE}
  LIB_DIR "${LIBOPENCM3_HOME}/lib"
  LIB_NAME opencm3_stm32${STM32_FAMILY_LOWER})

include_directories(${libopencm3_INC_DIR})
  • PREFIX is prepended to the names of external project’s artifacts. For example, in order to refer to libopencm3 include directory, cmake would use ${libopencm3_INC_DIR}
  • HOME is a directory of where to look for the project before trying to download it. If the directory doesn’t exist or FORCE_UPDATE is set to 1, add_project() will try to download the content into that location using download_project() function defined in the same module
  • SRC_DIR is a root directory of the source files
  • URL is a project’s external location
  • BUILD_CMD is a build command to execute
  • BUILD_IN is used to build projects in-source
  • LIB_DIR specifies of where the built library will be stored so as to export proper ${${PREFIX}_LIB_DIR} location
  • LIB_NAME is required if the project’s library name is non-standard. Often a library can be referred to in cmake by project’s name, i.e. ${PREFIX}. In case of libopencm3, static library name for use with stm32f103c8t6 board is libopencm3_stm32f1.a

project_download.cmake.in

When setting up a new external project using project_setup.cmake, this template is used to generate instructions for downloading and building that project:

1
2
3
4
5
6
7
8
9
10
11
12
13
include(ExternalProject)
ExternalProject_Add(${PREFIX}
  GIT_REPOSITORY    ${URL}
  GIT_TAG           master
  ${SOURCE_DIR}
  ${BINARY_DIR}
  ${CONFIG_CMD}
  ${BUILD_CMD}
  ${BUILD_IN}
  ${INSTALL_CMD}
  ${TEST_CMD}
  ${LOG_BUILD}
)

firmware.cmake

The module defines three functions:

  • add_target_config() configures a new cmake environment to be executed with a different toolchain
  • add_target_build() adds a custom target to start cross-compilation when using:
    1
    
    make _project_name_
    
  • add_target_flash() adds a custom target to upload the binary to the target board. To initiate the upload, use:
    1
    
    make _project_name_-flash