I’ve previosly written about CMake and its benefits. The truth is — CMake is a beast of a technology and it takes some time to get good at it. However, it is possible to take advantage from it without being a complete pro. Below, I’ll try to list and explain the most common CMake commands to use when cross-compiling for a embedded device.
You can read the entire code on my github repository.
Create the project
A common thing to do when it comes to setup a independent development workflow is to try to mimic what the vendor provides. In the following examples, I’ll targeting the STM32F030R8 which is part of the NUCLEO-F030R8.
The STM32CubeIDE provides not only an Graphical User Interface (GUI) to develop for the ST devices, but also an abstraction from any build or flash process. The goal is to pull what’s is useful from the STM32CubeIDE and move to CMake as soon as possible.
In order to do that, after creating a project for the target board, the necessary files are created. The project directory will look something like this:
The Core
folder includes relevant system and application code (I’ll later separate them).
The Drivers
folder include the STM32 Hardware Abstraction Layer (HAL) and CMSIS libraries.
Info
CMSIS can be ignored.
Personally, I don’t like the way the project is structured, so I changed to this:
Application folder should be independent from the system (part of previous /Core
) and HAL IHMO.
I’ve also removed the main.h
file, copied all the defines inside and moved to another file named pin_list.h
. Again, personally, don’t think that a system file should include application code, only the other way around.
Warning
If this all seems a bit hard, I would recommend going through the readme of this github repository first.
Move to your IDE
You can take the previous project and move it to your IDE of preference. Of course, nothing will compile, but we will tackle that soon.
After moving all the previous files into your IDE workplace, clone the STM32F0 HAL repository as a submodule:
CMake joins the room
Now that all the boring requirements are met, it is time to play with CMake.
In the root folder, you can start by creating a CMakeLists.txt
file. If this doesn’t sound familiar, please read my previous post about it.
Let’s start with the minimum amount of code:
The CMakeLists.txt
always starts with a cmake_minimum_required
followed by a project
.
Although it is possible to define the toolchain using the command line, I personally prefer to set it up inside a file toolchain.cmake
and including it before the project is named. The toolchain is essential to be able to compile to whatever platform you are compiling to; in this case arm-gcc compiler.
The last command enable_language
does exactly what you think — enables the following languagues: C, C++ and Assembly.
The next lines of CMake set the path of the folders or files necessary to build.
Note that the way the paths are added to CMake follow the same structure as stated before.
I’ve deviced to include the compilation options and definitions inside a file, the stm32f0.cmake
(just another personal preference). Let’s take a look inside:
The COMPILER_OPTIONS
can change according to the user need. There different types of compilation optimization and you read more about here.
For example, the -Og
optimizes the code for debugging: good debugging experience while having a fast compilation time.
You can use the COMPILER_DEFINES
to include different defines within the codebase.
The LINKER_OPTIONS
will be later used to link all the objects together. The options come as default from STM32CubeIDE.
CMake inheritance
After setting all the options, definitions and paths, it is time to device about the structure of the libraries and the executable.
The CMake will have a final target, the application, in which will include as many libraries as possible:
/HAL
needs files from/system
/system
needs files from/HAL
/application
needs files from both,/HAL
and/system
The dependencies can be viewed as such:
flowchart TD
A[HAL] <--> B[system]
A --> C[application]
B --> C[application]
Although /HAL
dependends from /system
and vice-versa, /HAL
needs to have its compilation options and definitions private. You don’t want to print the amount of warning comming from the /HAL
. Therefore, the CMake code continues as follow:
In order to have its compilation options and defines private, it is necessary to create the static library hal
. This library will include all the /HAL
related files (e.g. ${hal_include}
) and the default compiling options and definitions.
The hal library is then linked to the application target and extra compilation options are added:
Since the hal
library had its compilation options and defines set as private, the application won’t inherit anything from it (nor vice-versa).
To finalize, the application target is linked with the default options and a .hex
is created after the target has been built.
CMake most wanted
From the all the code snippets stated above, it is possible to verify a pattern:
- Create a library and add sources to library
- Include directories
Set as PRIVATE
to include the directories only for this library.
Set as PUBLIC
to include the directories for this library and respective targets where this library is linked.
- Define compilation options
Set as PRIVATE
to define the options only for this library.
Set as PUBLIC
to define the options for this library and respective targets where this library is linked.
Note that some of the compilation options might already be set according to build types, e.g. CMAKE_C_FLAGS
.
- Define compilation definitions
Set as PRIVATE
to define the definitions only for this library.
Set as PUBLIC
to define the definitions for this library and respective targets where this library is linked.
If you know this, you can do anything. CMake is all about libraries, targets and inheritance.