CMake within ZeroPilot

Introduction

Fair warning here, I am no CMake expert. The extent of my knowledge, and many people’s knowledge towards CMake is based solely on how CMake is used in their project. It is no different here. We will go over a brief overview of what CMake is and how we are using it in our application.

What is CMake?

Taken directly from CMake’s homepage: “CMake is an open-source, cross-platform family of tools designed to build, test and package software. CMake is used to control the software compilation process using simple platform and compiler independent configuration files, and generate native makefiles and workspaces that can be used in the compiler environment of your choice.”

In other words, CMake is not a build system, it generates build systems. If that doesn’t make sense, no worries! All you have to know is that in our application, CMake generates a Makefile based on a script called CMakeLists.txt and that Makefile is the one that actually compiles our code (so CMake generates the build system, which is Makefile).

Why CMake?

So why in the world are we using CMake instead of the built in IDE compiler system within STM32CubeIDE. The answer is customizability.

As you already know, ZeroPilot is meant to support multiple board packages and multiple flight frames. What this means is that, depending on which target (the .ioc/processor we build for) and the flight frame we select to build it for, different files will have to be compiled together. This is either difficult or impossible to do within one STM32CubeIDE project.

So, yes, CMakes in general add complexity to a project since we are not letting the IDE do the building for us, but the good thing is, the majority of people will never have to touch the CMake file unless you are adding entire directories to the project (and this is not too difficult of a change to make). The most difficult part is getting it started for the first time and after that, if done well and documented, it should be pretty easy to work with for most of the developers.

How to Navigate our CMakeLists.txt

cmake_minimum_required(VERSION 3.2.0) project(ZeroPilot C CXX ASM) set(ELF_NAME ${PROJECT_NAME}.elf) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) include_directories( ## System Manager Includes ## ## Attitude Manager Includes ## ## Path Manager Includes ## ## Telemetry Manager Includes ## ## Driver Includes ## ## Boardfiles Includes ## ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Drivers/${FAMILY_NAME}_HAL_Driver/Inc ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Core/Inc ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Drivers/CMSIS/Device/ST/${FAMILY_NAME}/Include ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Drivers/CMSIS/Include ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2 ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source/portable/GCC/ARM_${PORTABLE_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source/include ) ## Boardfile Sources ## set(HAL_DRIVERS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Drivers) set(HAL_DRIVERS_C_SOURCES "${HAL_DRIVERS_DIR}/${FAMILY_NAME}_HAL_Driver/Src/*.c") set(FREE_RTOS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Middlewares/Third_Party/FreeRTOS/Source) set(FREE_RTOS_C_SOURCES "${FREE_RTOS_DIR}/*.c" "${FREE_RTOS_DIR}/CMSIS_RTOS_V2/*.c" "${FREE_RTOS_DIR}/portable/GCC/ARM_${PORTABLE_NAME}/*.c" "${FREE_RTOS_DIR}/portable/MemMang/*.c") set(HAL_CORE ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${FOLDER_NAME}/Core) set(HAL_CORE_C_SOURCES "${HAL_CORE}/Src/*.c") set(HAL_CORE_CXX_SOURCES "${HAL_CORE}/Src/*.cpp") ## Actually find the sources, NOTE: if you add a new source above, add it here! ## file(GLOB_RECURSE C_SOURCES ${HAL_DRIVERS_C_SOURCES} ${FREE_RTOS_C_SOURCES} ${HAL_CORE_C_SOURCES}) message("MESSAGE: ${C_SOURCES}") file(GLOB_RECURSE CXX_SOURCES ${HAL_CORE_CXX_SOURCES}) ## Find the startup and linker script ## set(STARTUP_ASM_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${STARTUP_ASM}) set(LINKER_SCRIPT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/Boardfiles/${LINKER_SCRIPT}) add_executable(${ELF_NAME} ${C_SOURCES} ${CXX_SOURCES} ${STARTUP_ASM_FILE}) # Add project-specific linker flags (.ld script, .map file) set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T${LINKER_SCRIPT_FILE} -Wl,-Map=${PROJECT_BINARY_DIR}/${PROJECT_NAME}.map,--cref") set(BIN_FILE ${PROJECT_BINARY_DIR}/${PROJECT_NAME}.bin) # objcopy the .elf file to .bin file for programming add_custom_target("${PROJECT_NAME}.bin" ALL COMMAND ${CMAKE_OBJCOPY} -Obinary $<TARGET_FILE:${ELF_NAME}> ${BIN_FILE} DEPENDS ${ELF_NAME} ) # Print size information after compiling add_custom_command(TARGET ${ELF_NAME} POST_BUILD COMMENT "Binary size information:" COMMAND ${CMAKE_SIZE} $<TARGET_FILE:${ELF_NAME}> ) # the following is windows only set(INSTALL_CMD "ST-LINK_CLI") set(INSTALL_OPTS "-c SWD -P \"${BIN_FILE}\" 0x08000000 -NoPrompt -Rst -Run") install(CODE "execute_process( COMMAND ${INSTALL_CMD} ${INSTALL_OPTS} )" ) message("CMAKE_BUILD_TYPE = ${CMAKE_BUILD_TYPE}") IF(CMAKE_BUILD_TYPE MATCHES Debug) message("Debug build.") ELSEIF(CMAKE_BUILD_TYPE MATCHES Release) message("Release build.") ELSE() message("Some other build type.") ENDIF() add_custom_target(UPLOAD arm-none-eabi-gdb -iex "target remote tcp:127.0.0.1:3333" -iex "monitor program $<TARGET_FILE:${PROJECT_NAME}.elf>" -iex "monitor reset init" -iex "disconnect" -iex "quit")

Here is the CMake for ZeroPilot3.5 as of May 30, 2023. It is very much unfinished (as many of the directories are not populated yet) but the skeleton is there so I will go through each section and explain what it does at a high level.

One thing to note before we start though:

How does this CMake file support multiple board files?

You will notice that some variables such as ${FOLDER_NAME}, ${FAMILY_NAME}, ${LINKER_SCRIPT} and ${STARTUP_ASM} are not defined locally in the CMakeLists.txt. These are defined in an external .cmake files that lives in the specific boardfiles project. If you go to ZeroPilot-SW-3.5/Boardfiles/nucleol552zeq you should see a file named nucleol552zeq.cmake this is where we define a bunch of variables that we use in the general CMakeLists.txt, which is how one CMakeLists.txt can compile multiple board packages.

The CMakeLists.txt knows which .cmake file to use bec the build script (/Tools/build.bash) passes the file along in what is called a “tool chain file” based on arguments passed in the command line.

The line by line

One thing to note: whenever you see ${SOMETHING} it is either a user created variable using the set function or an environment variable found within the system.

Line 1: Specifies what the minimum version of CMake required to build.

Line 2: Specifies the project name and languages involved.

Line 4: This is an important one. All you need to know is the set function creates a variable (the first parameter) and sets it to the second input (second parameter).

For example: set(BANANAS "I love Bananas"). Now, every time you use the variable like so ${BANANAS}, it really just pastes in "I love Bananas”. Kind of like a C macro.

Line 8-29: The include_directories function takes in directories of which it should look for include (.h and .hpp) files. So when you encounter an error like no such file or directory you should check if you added the directory here.

Line 31-43: Here we are using the set function to set the variable provided to the directories provided using the wildcard operator *. The wildcard allows you to capture all files that follow the pattern. This can also be used on directories as well. This will make more sense in the next section, where it is used.

Line 46-50: The file function here takes 3 parameters, a command, the name to set the sources too, and finally, the sources themselves. In this case, we use the GLOB_RECURSE command which recursively searches in the directory provided for the files matching the pattern.

For example if we have the files apple.c, banana.c, and coconut.c in the /fruits directory, and we use the set function: set(FRUIT_FILES /fruits/*.c) then use the file function like so: file(GLOB_RECURSE FRUIT_C_SOURCES FRUIT_FLIES), the variable FRUIT_C_SOURCES will be set to apple.c banana.c coconut.c.

Line 53-54: Finding the startup and linker script generated by the STM32CUBEIDE required for compilation (the linker script) and boot up (startup file).

Line 56: Ah the moment we have all been waiting for. The function add_executible adds an executable to the project using the sources provided. This creates the .elf file.

Line 64-67: Here we are creating the .bin file used for flashing to the target (the microcontroller we are compiling for).

Line Everthing after: Don’t worry about these lines, its just a bunch options and random stuff, you will never have to touch this.

Where to add changes?

Adding new directories

If you add a new directory, you will have to add the path to the “Include” directory within the include_directories function. If you do not do this, you will end up with an error like no such file or directory.

Adding new source files

If you add new source files (.c and .cpp) you need to make sure it falls under one of the file path wild card directories currently in lines 31 - 43. If it is not here, you should make a new section with a comment that specifies which module it is from.

For example, if you start adding source files for attitude manager, you need to do something like this:

set(ATTITUDE_MANAGER_DIR ${CMAKE_CURRENT_SOURCE_DIR}/AttitudeManager/Src) set(ATTITUDE_MANAGER_CXX_SOURCES "${ATTITUDE_MANAGER_DIR}/*.cpp") set(ATTITUDE_MANAGER_C_SOURCES "${ATTITUDE_MANAGER_DIR}/*.c")

Then add this to the file function calls:

file(GLOB_RECURSE C_SOURCES ${HAL_DRIVERS_C_SOURCES} ${FREE_RTOS_C_SOURCES} ${HAL_CORE_C_SOURCES} ${ATTITUDE_MANAGER_C_SOURCES}) file(GLOB_RECURSE CXX_SOURCES ${HAL_CORE_CXX_SOURCES} ${ATTITUDE_MANAGER_CXX_SOURCES})

 

If this is not done, you will probably run into linker issues where it says undefined reference to blah blah blah. As the linker will try to find definitions to functions or references that don’t exist since you didn’t include all the files.

General Tip

I think the most helpful thing you can do is actually read the error message you get when you try to compile and think what could have gone wrong in the CMake (or something you forgot to add).

If you work with CMakes enough you will start the recognize patterns and see which errors them from which section, but this comes with many trials and many errors. Happy compiling :)