Why use CMake and Arm GCC?

CMake and Arm GCC (arm-none-eabi-gcc) are the perfect combination for developing your embedded applications. CMake is a cross platform tool for building software, and if you have ever got tired of jumping from one chip manufacturer IDE to another, then CMake can be an attractive alternative as it allows you to easily define your project build environment.

Additionally, getting full control over how the application is built from within IDE can be difficult, and usually requires jumping through different dialog boxes to set the various required flags which can often this leads to misconfiguration.

What is a CMake toolchain

They key component for utilizing CMake with embedded programming is through the definition of the toolchain. The toolchain describes how to compile and link your application, including where the compiler location and the flags. CMake has native GCC support, so adding support for the Arm GCC compiler is relatively easy.

At the core of the toolchain is the definition of the compiler and linker binaries, which is accomplished through setting the following variables:

On top of this, there are a bunch of additional tools specific to the Arm GCC compiler used for printing information about or optimizing the resulting binary:

Target specific definitions

Each Arm processor in the Cortex family has slightly different capabilities. Here we will define the specific flags needed to support each processor type.

Cortex-M4 definition

The Cortex-M4 has a v4 single precision floating point unit with 16 FPU registers.

set(MCPU_FLAGS "-mthumb -mcpu=cortex-m4")
set(VFP_FLAGS "-mfloat-abi=hard -mfpu=fpv4-sp-d16")

Cortex-M7 definition

The Cortex-M7 has a v5 single precision floating point unit with 16 FPU registers. The M7 is also available as a double precision version in which case you could use -mfpu=fvp5-d16.

set(MCPU_FLAGS "-mthumb -mcpu=cortex-m7")
set(VFP_FLAGS "-mfloat-abi=hard -mfpu=fpv5-sp-d16")

Toolchain Arm specific

First define some platform specific components. Setting the CMAKE_SYSTEM_NAME to Generic forces CMake to enable the CMAKE_CROSS_COMPILING flag.

To avoid the compiler test failures (because the target binaries can't run on your workstation machine), set CMAKE_TRY_COMPILE_TARGET_TYPE to STATIC_LIBRARY.

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

To support compilation across different platforms (Windows/Linux/MacOS), define the platform specific extension for the compiler and associated tools.

if(WIN32)
    set(TOOLCHAIN_EXT ".exe")
else()
    set(TOOLCHAIN_EXT "")
endif(WIN32)

Next define the different Arm executables needed to produce your binary. Not all these definitions would necessary be required to build your application, and this is also not a complete list, however these are what I commonly use.

A target triplet is a term used when defining the toolchain name and associated compiler. It consists of the three key pieces of information and is typically in the format of <arch>-<system>-<abi> and for the ARM GCC compiler, the triplet is arm-none-eabi.

set(TARGET_TRIPLET "arm-none-eabi-")
set(CMAKE_C_COMPILER   ${TARGET_TRIPLET}gcc${TOOLCHAIN_EXT})
set(CMAKE_CXX_COMPILER ${TARGET_TRIPLET}g++${TOOLCHAIN_EXT})
set(CMAKE_ASM_COMPILER ${TARGET_TRIPLET}gcc${TOOLCHAIN_EXT})
set(CMAKE_LINKER       ${TARGET_TRIPLET}gcc${TOOLCHAIN_EXT})
set(CMAKE_SIZE_UTIL    ${TARGET_TRIPLET}size${TOOLCHAIN_EXT})
set(CMAKE_OBJCOPY      ${TARGET_TRIPLET}objcopy${TOOLCHAIN_EXT})
set(CMAKE_OBJDUMP      ${TARGET_TRIPLET}objdump${TOOLCHAIN_EXT})
set(CMAKE_NM_UTIL      ${TARGET_TRIPLET}gcc-nm${TOOLCHAIN_EXT})
set(CMAKE_AR           ${TARGET_TRIPLET}gcc-ar${TOOLCHAIN_EXT})
set(CMAKE_RANLIB       ${TARGET_TRIPLET}gcc-ranlib${TOOLCHAIN_EXT})

Toolchain Compiler Flags

Finally, the compiler and linker flags are defined, firstly we have common flags for all build types, followed by the debug and release specific flags.

For debug builds, optimizations are disabled. These stops code from being compiled out, as well as moved around and making stepping through the code difficult.

For release builds, optimization is for size, meaning we are looking for a smaller output binary. Link time optimization is also used which attempts to optimize code across all the different compiled units.

Compiler Optimizations

For more information on common optimization flags used when building, take a look at GCC Embedded Arm Compiler Optimizations.

set(CMAKE_COMMON_FLAGS "-g3 -ffunction-sections -fdata-sections -fno-strict-aliasing -fno-builtin -fno-common -Wall -Wshadow -Wdouble-promotion -Werror")

set(CMAKE_C_FLAGS "${MCPU_FLAGS} ${VFP_FLAGS} ${CMAKE_COMMON_FLAGS}")
set(CMAKE_CXX_FLAGS "${MCPU_FLAGS} ${VFP_FLAGS} ${CMAKE_COMMON_FLAGS}")
set(CMAKE_ASM_FLAGS "${MCPU_FLAGS} ${VFP_FLAGS} ${CMAKE_COMMON_FLAGS}")
set(CMAKE_EXE_LINKER_FLAGS "${LD_FLAGS} --specs=nano.specs -Wl,--gc-sections,-print-memory-usage")

set(CMAKE_C_FLAGS_DEBUG "-O0")
set(CMAKE_CXX_ASM_FLAGS_DEBUG "-O0")
set(CMAKE_ASM_FLAGS_DEBUG "")
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "")

set(CMAKE_C_FLAGS_RELEASE "-Os -flto")
set(CMAKE_CXX_FLAGS_RELEASE "-Os -flto")
set(CMAKE_ASM_FLAGS_RELEASE "")
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "-flto")

Executing CMake

To use the custom defined toolchain, the CMAKE_TOOLCHAIN_FILE parameter is passed at the command line to point to the specific toolchain we want to use for the CMake project. The CMAKE_BUILD_TYPE describes whether to build a debug or release target.

Debug build:

cmake -DCMAKE_TOOLCHAIN_FILE="arm-gcc-cortex-m4.cmake" -DCMAKE_BUILD_TYPE "Debug"

Release build:

cmake -DCMAKE_TOOLCHAIN_FILE="arm-gcc-cortex-m4.cmake" -DCMAKE_BUILD_TYPE "Release"

Finally

Putting everything together in this post should get you started your own toolchain for building Arm Cortex applications using CMake.

To see the final toolchain files, head over to my GitHub repository.