Skip to content
Create cmake builds for cross-compiled projects faster. Platforms AVR / STM32 / x86.
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
cmake/Modules
example
.gitignore
LICENSE
README.md

README.md

Overview

The repository contains Cmake modules and example files which help to save time when setting up new C/C++ projects. The main goal is to reuse working solutions to common problems such as:

  • Combining of cross-compilation for AVR / STM32 with unit testing on x86 platform
  • Downloading and refreshing of dependencies
  • Setting up paths to headers, sources and libraries
  • Re-typing boilerplate code around initialization and checks
  • Cluttering build files with large, duplicated blocks of instructions
  • Recurrent research on Cmake workings

Platforms and requirements

The example project was tested on MacOS and Linux. Depending on host and target platforms, not all the dependencies given below may apply.

Most of the packages can be installed using system package manager or supplemental tools like brew on MacOS. When a package manager doesn't support a package, the package is downloaded from GitHub (for example, gtest, stm32-cmake) or directly from developer's website (for example, Arm GNU Embedded Toolchain, FreeRTOS).

Table of Contents

Example

Directory "example" represents an example cross-compilation project. Within it:

  • src/avr/main.cpp
    Uses Arduino library to blink a built-in LED and call a function Example::hello() while running on AVR-based microcontroller
  • 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 (aka Blue Pill)
  • src/x86/main.cpp
    Calls Example::hello() while running on MacOS or Linux

Project structure:

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

To start cmake build, the project provides bash script make.sh. For example, to build programs for all three platforms, run:

./make.sh -x -s -a -b mega -c atmega2560 -p /dev/ttyACM0 -- -DENABLE_TESTS=ON

The script creates the following three directories. Cmake then generates the builds for corresponding toolchains within them. Lastly, make tool initiates the build in each of the directories:

example/
|-- ...
|-- build-avr
|-- build-stm32
|-- build-x86
|-- ...

To upload just-built firmware to a connected board, change into build-avr or build-stm32 and execute:

make example-flash

To run an example unit test which is defined in test/example_test.cpp, change into build-x86 and execute:

make test

Details

example/make.sh

The script makes it convenient to set up the environment and pass the required parameters to cmake program.

Example project and Cmake modules use specific variables from the environment. One such set includes the locations of dependent libraries. The locations should be changed to reflect personal preference:

if [ -z ${XTRA_HOME} ]; then
  XTRA_HOME=${PWD}/../xtra
fi

export GTEST_HOME=${XTRA_HOME}/gtest
export ARDUINOCMAKE_HOME=${XTRA_HOME}/arduino-cmake
export STM32CMAKE_HOME=${XTRA_HOME}/stm32-cmake
export LIBOPENCM3_HOME=${XTRA_HOME}/libopencm3
export FREERTOS_HOME=${XTRA_HOME}/FreeRTOSv10.1.1

Command line options:

Usage: make.sh [-x] [-s] [-a] [-b _board_] [-c _board_cpu_] [-p _serial_port_] [-u] [-h]
        -x - build x86
        -s - build stm32 (board stm32f103c8t6)
        -a - build avr (must specify board)
        -b - board (uno, mega, etc.)
        -c - board CPU (atmega328, atmega2560, etc.)
        -p - board serial port (e.g. /dev/ttyACM0)
        -u - clone/pull dependencies from GitHub
        -h - this help
  • -x, -s, -a
    Specify one or all options to build artifacts for the corresponding platform
  • -b
    When building for AVR platform (-a), this option specifies a target board. To see a complete list of the boards supported by arduino-sdk, uncomment instruction "print_board_list()" in example/avr/CMakeLists.txt
  • -c
    Some AVR boards have different CPUs (e.g. mega), -c option specifies which CPU the board uses. ArduinoToolchain will offer suggestions when it requires that parameter
  • -p
    Specify a serial port where AVR device is connected to
  • -u
    When specified, the script will try to pull the changes for the above libraries from GitHub. Alternatively, module project_setup.cmake can achieve similar goal but as part of the building process

example/CMakeLists.txt

Cmake processes this file first. It sits at the project's root and sets a couple of general parameters and then loads the common code for statring cross-compiled project from main_project.cmake:

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

main_project.cmake

Instructions in the file start with a regular version/project preamble. Next, the file sets the path to modules and instructs to include init.cmake, firmware.cmake and unit_testing.cmake modules:

cmake_minimum_required(VERSION 3.5)
project(example)

set(CMAKE_MODULE_PATH $ENV{CMAKEHELPERS_HOME}/cmake/Modules)
include(init)
include(firmware)
include(unit_testing)

$ENV{CMAKEHELPERS_HOME} is set in the environment to point to cmake-helpers (this) project's root directory.

Now, when the modules are processed, cmake determines what build it needs to generate based on ${BOARD_FAMILY} (initialized in init.cmake):

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()

string(COMPARE EQUAL "${BOARD_FAMILY}" stm32 _cmp)
if (_cmp)
  set(TOOLCHAIN_FILE $ENV{STM32CMAKE_HOME}/cmake/gcc_stm32.cmake)
  ...
  add_target_config_args(...)
  add_target_build(...)
  add_target_flash(...)

else ()

  string(COMPARE EQUAL "${BOARD_FAMILY}" avr _cmp)
  if (_cmp)
    set(TOOLCHAIN_FILE $ENV{ARDUINOCMAKE_HOME}/cmake/ArduinoToolchain.cmake)
    ...
    add_target_config_args(...)
    add_target_build(...)
    add_target_flash(...)

  else()

    string(COMPARE EQUAL "${BOARD_FAMILY}" x86 _cmp)
    if (_cmp)
      add_subdirectory(${PROJECT_SOURCE_DIR}/src/x86)
    endif ()
endif ()

Cmake follows one of three decision branches for generating avr, stm32 or x86 build.

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

If ${BOARD_FAMILY} matches a microcontroller branch (avr or stm32), cmake switches the toolchain and generates a cross-compilation build based on the instructions in avr/CMakeLists.txt or stm32/CMakeLists.txt.

If ${BOARD_FAMILY} matches x86 or unit tests were enabled with -DENABLE_TESTS=ON, cmake continues to use the current x86 toolchain to generate the build based on x86/CMakeLists.txt and unit tests based on test/CMakeLists.txt.

example/src/avr/CMakeLists.txt

In order to build firmware with a different toolchain, cmake "re-initializes" the build with that new toolchain. Because of that, previously defined variables and functions must also be reinitialized except for those that were specifically passed in by the preceeding stage. These common tasks and more reside in avr_project.cmake

Example CMakeLists.txt instructs to build an executable with build_exe(...):

cmake_minimum_required(VERSION 3.5)
set(CMAKE_MODULE_PATH $ENV{CMAKEHELPERS_HOME}/cmake/Modules)

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

avr_project.cmake

First, the module sets a project name:

if (SUBPROJECT_NAME)
  project(${SUBPROJECT_NAME})
else ()
  project(${PROJECT_NAME})
endif ()

include(init)

Next, cmake executes usual instructions when setting up source files for compilation:

include_directories(...)
file(GLOB_RECURSE SOURCES ...)
add_definitions(-D${BOARD_FAMILY})
...

Here, cmake generates Arduino-specific instructions for building and flashing the firmware using functions defined in arduino-cmake:

function (build_lib)
  cmake_parse_arguments(p "" "SUFFIX" "SRCS;LIBS" ${ARGN})
...
function (build_exe)
  cmake_parse_arguments(p "" "SUFFIX" "SRCS;LIBS" ${ARGN})
...

example/src/stm32/CMakeLists.txt

This project file uses the same idea as described in avr/CMakeLists.txt, but here more instructions need to be specified.

The firmware for stm32f103c8 depends on libopencm3 library. Before the firmware can use it, libopencm3 must be built using make. One option to achieve it is to download and build the library manually. Another is to use project_setup.cmake module which can automate the process a bit more. These instructions are specified in libopencm3.cmake.

Another library that the firmware depends on is FreeRTOS, which has its own usage requirements. See freertos.cmake for details.

include(stm32f103c8t6)
include(stm32_project)

find_srcs()

include(libopencm3)
...
include(freertos)
...
build_exe(SRCS ${SOURCES} LIBS ${LIBS})

stm32_project.cmake

Most of what is described in avr_project.cmake applies here, but instead Cmake executes stm32-specific instructions defined in stm32-cmake toolchain:

function (setup)
  include_directories(
...
function (build_lib)
  cmake_parse_arguments(p "" "SUFFIX" "OBJS;SRCS;LIBS" ${ARGN})
  set(TARGET ${PROJECT_NAME}${p_SUFFIX})
...
function (build_exe)
  cmake_parse_arguments(p "" "SUFFIX" "OBJS;SRCS;LIBS" ${ARGN})

  STM32_SET_TARGET_PROPERTIES(...)
  STM32_ADD_HEX_BIN_TARGETS(...)
...

example/src/x86/CMakeLists.txt

The file instructs to build a library, an executable, and link the executable with the library:

include(x86_project)

set(CMAKE_MACOSX_RPATH 1)
find_package(Threads)

find_srcs(FILTER ${MAIN_SRC})
build_lib(SRCS "${SOURCES}" LIBS ${CMAKE_THREAD_LIBS_INIT} LIB_TYPE SHARED)
build_exe(OBJS "${SOURCES_OBJ}" SRCS "${MAIN_SRC}" LIBS ${PROJECT_NAME} SUFFIX "-exe")

SUFFIX for executable is required because x86_project uses ${PROJECT_NAME} for the library. Thus, executable target is named suing ${PROJECT_NAME}${SUFFIX} combination, e.g.: example-exe

x86_project.cmake

The module is similar in structure and purpose to avr_project.cmake and stm32_project.cmake, but target platform is x86.

example/test/CMakeLists.txt

The file specifies what to link unit testing executable with and loads test_project.cmake:

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

test_project.cmake

Instructions in this file are similar to x86_project.cmake, except only an executable is needed which should be linked with google test and Boost libraries:

include(gtest)
...
function (build_exe)
  cmake_parse_arguments(p "" "SUFFIX" "OBJS;SRCS;LIBS" ${ARGN})
...
target_link_libraries(${TARGET} ${p_LIBS} ${Boost_LIBRARIES} ${gtest_LIB_NAME})
add_test(NAME ${TARGET} COMMAND $<TARGET_FILE:${TARGET}>)

gtest.cmake

Per instructions in this file, cmake checks if googletest and its dependency (Boost) are already installed. If not, cmake uses project_setup.cmake to download the googletest sources from GitHub and add subdirectory with the content to the unit test build. If REQUIRED Boost libraries are not found, cmake will stop the build.

set(GTEST_HOME $ENV{GTEST_HOME})
find_package(Boost REQUIRED COMPONENTS system thread)
find_package(GTest)
...
add_project(
  PREFIX gtest
  URL "https://github.com/google/googletest.git"
  HOME "${GTEST_HOME}"
  INC_DIR "${GTEST_HOME}/googletest/include")
...

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

unit_testing.cmake

The module checks if the platform is x86 and enables unit testing if ${ENABLE_TESTS} option is set to ON. The option can be passed via make.sh (see Example).

project_setup.cmake

The module is used to set up external cmake/make project for use by a current project. It involves:

  • downloading the files from external source such as GitHub
  • exporting of source, header and/or built library names and locations
  • adding targets, which are defined by the external project, to global scope. To see all available targets after cmake completes, change into one of build-* directories and type: make help

Note, project_setup uses ExternalProject module. Many concepts can be clarified by reading that module's documentation.

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

include(project_setup)

set(LIBOPENCM3_HOME $ENV{LIBOPENCM3_HOME})

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

include_directories(${libopencm3_INC_DIR})
link_directories(${libopencm3_LIB_DIR})
list(APPEND LIBRARIES ${libopencm3_LIB_NAME})
  • 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
  • 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, the library name for use with stm32f103c8t6 board is libopencm3_stm32f1.a

project_download.cmake.in

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

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 by using make _project_name_
  • add_target_flash() adds a custom target to upload the built firmware to the target board; to be invoked with make _project_name_-flash

libopencm3.cmake

The file uses project_setup.cmake to set up external project dependency.

freertos.cmake

The module helps to set up FreeRTOS source/header file locations to be included as part of the build for stm32f103c8t6 board.

See FreeRTOS documentation for details about this interesting OS.

stm32f103c8t6.cmake

The module aggregates compiler and linker flags which are required to build firmware for stm32f103c8t6.

To some degree, this module defeats the purpose of using stm32-cmake. Moreover, most features of stm32-cmake remain unused and may cause difficulty during the build. A few functions that stm32/CMakeLists.txt refers to do not justify keeping that dependency around. TODO reassess the benefit of using stm32-cmake given the circumstances.

Contribute

Consider supporting our projects by contributing to their development. Learn more at boltrobotics.com

You can’t perform that action at this time.