CMake
What Actually Is CMake?
CMake is a compilation tool designed to simplify C++ compilation by turning it into a series of terminal commands that use config files to provide all the compilation flags that would otherwise have to be set by hand each time you build. It has its own syntax that needs to be learned, but once the build is reasonably configured, it tends to be pretty set-and-forget. The process for using it takes a bit of getting used to, but it's relatively easy to learn.
How CMake Works
CMake is very directly tied to the directory structure of the project that you're trying to build. Both the parent directory and each subdirectory with .cpp files that you want to compile will have a CMakeLists.txt file in it - this is that config file mentioned earlier. The parent directory's CMakeLists.txt will have all of the compiler definitions (such as the versions of C and C++ you're using, the locations of your library files, the compiler flags and versions, and other overhead things), whereas each subdirectory's file will just have the specific definitions for the .cpp files in that directory. In practice, this means you'll just edit the CMakeLists.txt file in the subdirectory containing the files you're working on if you add any or modify the way they need to be included.
When CMake is running, it will dump all the intermediary steps of the process to a configured directory. In almost all cases, this will be a subdirectory in the project called build/; we'll go over how to configure this directory and use it for the actual build process below.
Example CMakeLists.txt File
Below is the file found at src/CMakeLists.txt from the Active Drag System; this notably is not the parent directory - it is specifically just the compile targets for the src/active_drag_system.cpp file, and all of the necessary header paths that it needs to include.
add_executable(active_drag_system
${PROJECT_SOURCE_DIR}/src/active_drag_system.cpp
${PROJECT_SOURCE_DIR}/src/ms5607.cpp
${PROJECT_SOURCE_DIR}/src/adxl375.cpp
${PROJECT_SOURCE_DIR}/src/iim42653.cpp
${PROJECT_SOURCE_DIR}/src/mmc5983ma.cpp
${PROJECT_SOURCE_DIR}/src/pwm.cpp
${PROJECT_SOURCE_DIR}/src/log_format.cpp
${PROJECT_SOURCE_DIR}/src/heartbeat.cpp
${PROJECT_SOURCE_DIR}/src/serial.cpp
)
pico_set_binary_type(active_drag_system copy_to_ram)
target_link_libraries(active_drag_system pico_stdlib pico_logger pico_flash pico_rand pico_multicore pico_sync hardware_i2c hardware_adc hardware_timer hardware_pwm FreeRTOS-Kernel FreeRTOS-Kernel-Heap4 libfixmath libfixmatrix libfixkalman)
target_include_directories(active_drag_system PUBLIC ${PROJECT_SOURCE_DIR}/include)
target_compile_definitions(active_drag_system PRIVATE
USE_FREERTOS=1
# DEBUG=1
PICO_STDIO_STACK_BUFFER_SIZE=64 # use a small printf on stack buffer
)
pico_enable_stdio_usb(active_drag_system 1)
pico_enable_stdio_uart(active_drag_system 0)
pico_add_extra_outputs(active_drag_system)
Let's break down what each of these lines means step-by-step.
-
add_executable()is how we define the output of the build process. When you complete a compilation, it will output a.uf2file that is then copied to the RP2040 (either manually or viapicotool); this is where the name of that executable is defined, and which files are included in it. Note that this means the "main" file (the one that will be executed on the RP2040) foractive_drag_systemissrc/active_drag_system, and all of the other.cppfiles are included into it. If you add any additional files to the project that need to be included, you need to add their.cppfiles to this list for the linker to be able to find them. You can, for reference, add multiple compilation targets to the sameCMakeLists.txtfile to compile them all at once, and then select which one you want to actually upload after the fact; see the file in our tools directory for an example of how to do this.add_executable(active_drag_system ${PROJECT_SOURCE_DIR}/src/active_drag_system.cpp ${PROJECT_SOURCE_DIR}/src/ms5607.cpp ${PROJECT_SOURCE_DIR}/src/adxl375.cpp ${PROJECT_SOURCE_DIR}/src/iim42653.cpp ${PROJECT_SOURCE_DIR}/src/mmc5983ma.cpp ${PROJECT_SOURCE_DIR}/src/pwm.cpp ${PROJECT_SOURCE_DIR}/src/log_format.cpp ${PROJECT_SOURCE_DIR}/src/heartbeat.cpp ${PROJECT_SOURCE_DIR}/src/serial.cpp ) -
pico_set_binary_type()is just how we define where the flash process will actually put the compiled code when you upload it. The above option shouldn't need to be changed; the alternative is storing the code directly in SRAM as opposed to in-memory, which we don't want. -
target_link_libraries()is where we define which library headers to include in the project. When you add a#include <hardware/gpio.h>statement to a file, the underlying library ("hardware_gpio" in this case) needs to be added to this block for the compiler to be able to find that library; all libraries are added in a single call, separated by newlines or spaces in the call. -
target_include_directories()is where we define the locations of the header files for our.cppfiles. As you can see, this is literally done by providing the path from the parent directory to anyinclude/subdirectories that are needed. For our case, this usually won't have to change. -
target_compile_definitions()is where we set any in-code compiler flags, i.e. whether to#definevarious flags or not. For example, below the CMake snippet provided is a block from insidesrc/active_drag_system.cppthat includes a header file full of old launch data only if theDEBUGflag is defined; if you wanted to do so, the corresponding flag in theCMakeLists.txtfile just needs to be uncommented so the compiler knows it. -
Finally, we have a couple of definitions just to define various things about the compilation process.
pico_enable_stdio_usb()andpico_enable_stdio_uart()are both called with their respective flags (1 or 0) to tell the compiler to setstdio, the standard serial bus, to run a USB instance instead of a UART instance; all this does is tell the system that we want to be able to talk to it over a USB cable. Similarly,pico_add_extra_outputs()just tells the compiler that we want it to convert the native output file type (.elf) to a.uf2, the one that we use; for practical purposes, none of these will ever need to change.
CMake Usage
CMake, to put it bluntly, is entirely command based. There are ways to bind it to various plugins for VSCode or other editors, but for this documentation, we'll use just the raw terminal commands to make it as platform-agnostic as possible. First, let's look at a brief high-level list of the steps to compile any CMake project from when you first clone its repository:
- Clone the repository of whatever project you're working on (wherever you want it to be)
- Initialize the git submodules of the directory, if necessary
- Make the
build/directory within the project's parent directory - Initialize CMake inside the
build/directory - Confirm the settings in all
CMakeLists.txtfiles - Navigate to the project parent directory before building
- Run the build process by calling CMake, and telling it where the
build/directory is - Take the output
.uf2file that CMake generates, and use your desired tool to upload it!
Steps 1-4 will usually only have to be done once, the first time you clone the repository; once the build/ directory is made and initialized, it usually won't have to be touched. Now, let's go over the actual commands necessary for each of these steps.
-
We'll start by cloning the repository somewhere we can find it. We'll make and
cdinto a test directory calleddemo, and clone it via SSH:> mkdir demo > cd demo > git clone git@github.com:RocketryVT/active-drag-system.git Cloning into 'active-drag-system'... remote: Enumerating objects: 938, done. remote: Counting objects: 100% (166/166), done. remote: Compressing objects: 100% (88/88), done. remote: Total 938 (delta 94), reused 86 (delta 78), pack-reused 772 (from 1) Receiving objects: 100% (938/938), 7.03 MiB | 17.61 MiB/s, done. Resolving deltas: 100% (578/578), done. > cd active_drag_system > pwd /home/cayenne/demo/active_drag_system/ >_ -
Following this, we need to make sure that the git submodules are initialized before configuring CMake for the first time.
> git submodule update --init --recursive Submodule 'include/FreeRTOS-Kernel' (https://github.com/FreeRTOS/FreeRTOS-Kernel.git) registered for path 'include/FreeRTOS-Kernel' Submodule 'include/eigen' (https://gitlab.com/libeigen/eigen.git) registered for path 'include/eigen' Submodule 'include/libfixkalman' (https://github.com/sunsided/libfixkalman.git) registered for path 'include/libfixkalman' # [there is a lot of output from this command, I'll skip most of it] Submodule path 'include/pico-sdk/lib/mbedtls': checked out '5a764e5555c64337ed17444410269ff21cb617b1' Submodule path 'include/pico-sdk/lib/tinyusb': checked out '86ad6e56c1700e85f1c5678607a762cfe3aa2f47' >_ -
Next, we'll make the
build/directory inside the project andcdinto it. -
Now, we'll initialize CMake inside the
build/directory. This is done just by callingcmakeand giving it the..path to the parent directory, with no flags.> cmake .. PICO_SDK_PATH is /home/cayenne/demo/active-drag-system/include/pico-sdk Target board (PICO_BOARD) is 'rp2040_micro'. Using board configuration from /home/cayenne/demo/active-drag-system/include/rp2040_micro.h Defaulting platform (PICO_PLATFORM) to 'rp2040' since not specified. -- Defaulting build type to 'Release' since not specified. Defaulting compiler (PICO_COMPILER) to 'pico_arm_cortex_m0plus_gcc' since not specified. Configuring toolchain based on PICO_COMPILER 'pico_arm_cortex_m0plus_gcc' Setting FREERTOS_KERNEL_PATH to /home/cayenne/demo/active-drag-system/include/FreeRTOS-Kernel based on location of FreeRTOS-Kernel-import.cmake Build type is Release -- Found Python3: /usr/bin/python3.13 (found version "3.13.7") found components: Interpreter TinyUSB available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040; enabling build support for USB. BTstack available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/btstack cyw43-driver available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/cyw43-driver lwIP available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/lwip mbedtls available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/mbedtls -- Configuring done (0.2s) -- Generating done (0.0s) -- Build files have been written to: /home/cayenne/demo/active-drag-system/build >_ -
Looking at the
CMakeList.txtfiles can be done however you prefer; usually, they'll already be ready to go from when you clone the repository, so I won't add any additional terminal commands here. - Once you've confirmed that your
CMakeLists.txtfiles are set up appropriately, we need to make sure that we're in the project's parent directory; if you're still inbuild/,cdback down to the parent before continuing. - Finally, to actually run the cmake build itself, there are two steps that need to be done. The first is to set the path to the
build/directory you made viacmake -B, and the second is to actually perform the build withcmake --build. This is done in two separate steps, because in the first one you'll also set any flags for the CMake process that you want; for our purposes, this mainly means settingCOMPILE_TOOLSto 0 or 1, depending on if you want to build the additional scripts in thetools/subdirectory or not.> cmake -B build -DCOMPILE_TOOLS=0 PICO_SDK_PATH is /home/cayenne/demo/active-drag-system/include/pico-sdk Target board (PICO_BOARD) is 'rp2040_micro'. Using board configuration from /home/cayenne/demo/active-drag-system/include/rp2040_micro.h Pico Platform (PICO_PLATFORM) is 'rp2040'. Build type is Release TinyUSB available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/tinyusb/src/portable/raspberrypi/rp2040; enabling build support for USB. BTstack available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/btstack cyw43-driver available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/cyw43-driver lwIP available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/lwip mbedtls available at /home/cayenne/demo/active-drag-system/include/pico-sdk/lib/mbedtls -- Configuring done (0.1s) -- Generating done (0.0s) -- Build files have been written to: /home/cayenne/demo/active-drag-system/build > cmake --build build [ 2%] Built target bs2_default [ 4%] Built target bs2_default_library [100%] Built target active_drag_system # [there will usually be a lot more output during this stage] >_ - With the above done, the code has been compiled and is ready to upload using the method of your choice! The outputs can be found inside the
build/directory you made earlier; they'll be in a subdirectory inside it matching the one theCMakeLists.txtwhere you calledadd_executable()was in in the parent directory. For example, theactive_drag_systemexecutable is defined in theCMakeLists.txtfile found in the src subdirectory, so our compiled output file will be inbuild/src.To use your freshly compiled code, you would upload> ls build/src active_drag_system.bin active_drag_system.elf active_drag_system.hex CMakeFiles Makefile active_drag_system.dis active_drag_system.elf.map active_drag_system.uf2 cmake_install.cmake >_build/src/active_drag_system.uf2using the method of your choice.
Helpful CMake Commands
- To configure and build your code at the same time, you can just run them sequentially; running this line as one command will do the full build process.
- To do the above and then upload the code afterwards, you can keep appending more commands to the end; for example, you can add a
picotoolcall after the build file is done if you know the name of your desired executable.