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).
- x86 software/host platform
- build-essential (Linux)
- CMake
- Boost
- Google Test
- Fast C++ logging library
- To build AVR firmware
- avr-gcc
- avrdude
- To build STM32F103 firmware
- Arm GNU Embedded Toolchain
- libopencm3
- FreeRTOS
- st-link tool
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