ESP32 – How to Use CMake with ESP-IDF

In the previous tutorial, we looked at getting started with ESP-IDF and created our first blinking LED project for the ESP32. The next step to effectively using ESP-IDF is to understand a little about CMake, which is the build system generator commonly used for low-level languages, like C/C++, for operating system and embedded system development. This tutorial will guide you through the a few CMake examples (without ESP-IDF), and demonstrate how it can be used with ESP-IDF to compile and link projects for the ESP32.

Here is a video version of the guide:

What is CMake?

CMake is an open-source, cross-platform build system generator. It helps developers manage complex build configurations by generating build files for tools like Make, Ninja, and Visual Studio without requiring you to learn each system’s syntax. If you come from the world of GNU make, it means no longer having to write confusing Makefiles, as CMake handles that process for you.

Initially, ESP-IDF relied on GNU Make as its build system, but has transitioned to CMake for its modern features, cross-platform compatibility, and enhanced project management capabilities. This shift marks a significant step towards a more streamlined and powerful development experience. Approximately 65% of embedded projects now leverage community-driven projects, like CMake, leading to improved quality and reliability. By embracing CMake, ESP-IDF empowers developers to build robust and scalable projects for a variety of applications. A key advantage is the ability to manage dependencies efficiently, a crucial aspect for projects involving multiple libraries and components. For instance, adding a new sensor library becomes a simple process of updating the CMakeLists.txt file. Troubleshooting dependency issues is also simplified with CMake’s clear error reporting. Prerequisites include a basic understanding of C/C++ and the ESP-IDF ecosystem.

Stay in the loop email signup form

Setup

The custom Docker image with pre-installed ESP-IDF development environment used throughout this guide can be found here: https://github.com/ShawnHymel/course-iot-with-esp-idf/. See the instructions in the previous tutorial to set up the Docker image for ESP-IDF development. You do not need any hardware for this tutorial; we will create a simple software-only project and learn how to use CMake to build it.

Simple, Software-only Project with a Library

In the Docker container, navigate to /workspace/apps and create a new project called cmake_demo:

cd /workspace/apps
mkdir cmake_demo
cd cmake_demo

Inside this folder, create the following structure:

cmake_demo/
├── include/
│   └── my_lib.h
├── src/
│   └── my_lib.c
├── main.c
└── CMakeLists.txt

Create include/my_lib.h:

#ifndef MY_LIB_H
#define MY_LIB_H

void say_hello(void);

#endif // MY_LIB_H

This defines a simple function say_hello() and uses header guards to prevent multiple inclusions.

Create src/my_lib.c:

#include <stdio.h>
#include "my_lib.h"

void say_hello(void) {
    printf("Hello, World!\n");
}

Create main.c, which imports our my_lib library and calls the only function:

#include "my_lib.h"

int main(void) {
    say_hello();

    return 0;
}

In the project’s root, create CMakeLists.txt:

cmake_minimum_required(VERSION 3.16)

project(hello_world VERSION 1.0 DESCRIPTION "Classic Hello World" LANGUAGES C)

# Create a static library target
add_library(my_lib STATIC src/my_lib.c)

# Tell CMake where to find the header files
target_include_directories(my_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

# Create an executable target
add_executable(${PROJECT_NAME} main.c)

# Link the library to the executable
target_link_libraries(${PROJECT_NAME} PRIVATE my_lib)

Let’s take a look at what each of the CMake calls does:

  • cmake_minimum_required: Ensures we’re using CMake 3.16+.
  • project(...): Sets the project name, version, and language.
  • add_library: Builds src/my_lib.c into a static library.
  • target_include_directories: Tells CMake where to find header files.
  • add_executable: Builds main.c into an executable.
  • target_link_libraries: Links our library to the final executable.

Build the Project

First, we’ll demonstrate how to build the project using the old CMake method (manually create a build/ directory):

mkdir build/
cd build/
cmake ..
make
  • cmake .. generates build files in the build directory.
  • make compiles everything into an executable called hello_world.

Run it:

./hello_world
# Output:
# Hello, World!

Since CMake 3.13, you can simplify the process. Let’s try this again, but we’ll demonstrate using the newer method of generating the build artifacts. First, clean up the build by removing the build/ directory:

cd ..
rm -rf build/

Now, rebuild the project using the -S and -B flags with cmake:

cmake -S . -B build/
cmake --build build/
  • -S . – Source directory (current folder).
  • -B build – Build directory.
  • --build build – Compiles automatically using the correct build system.

CMake supports multiple generators. By default, it uses Make, but you can switch to Ninja for faster builds:

rm -rf build/
cmake -S . -B build/ -G "Ninja"
cmake --build build/

Check the generated files:

  • With Make: Makefile
  • With Ninja: build.ninja

Hopefully, this helps you see how to use CMake to generate build systems for simple software projects. CMake can be quite complex! To learn more about it, I recommend checking out the official documentation and getting started guide. Henry Schreiner’s Introduction to Modern CMake is also a fantastic reference for diving into CMake.

Rebuild Blinky

With a basic understand of CMake, we can rebuild the blinking LED example from the previous tutorial without using the idf.py metatool. Navigate to the blinky/ project and delete the sdkconfig files, as we’ll manually add parameters to our CMake call.

cd /workspace/apps/blinky/
rm sdkconfig sdkconfig.old

Then, call CMake, pass in our desired ESP32 target, and tell it to use Ninja (the default build system in ESP-IDF). From there, use CMake to call the appropriate build system to compile and link our project.

cmake -DIDF_TARGET=esp32s3 -G "Ninja" -S . -B build/
cmake --build build/

This performs a very similar process to what idf.py build does under the hood! Note that we left out the Kconfig details (e.g. the sdkconfig files) here to keep things simple. idf.py build does a lot more than those two cmake lines: it also looks at the configuration parameters set by Kconfig and build/links the appropriate software components as needed.

Feel free to flash the output binary from your manual build to verify that the blinking LED code still works!

ESP32 blinking LED with ESP-IDF

Conclusion

Hopefully, this helps you understand a little more about how to use CMake and what goes on when you enter idf.py build.

If you would like to dive deeper, I recommend checking out my IoT Firmware Development with ESP32 and ESP-IDF course. In it, we cover the basics of ESP-IDF, reading from sensors, connecting to the internet via WiFi, posting data via HTTP REST calls, securing connections with TLS, and interacting with MQTT brokers.

IoT Firmware Development with ESP32 and ESP-IDF

Leave a Reply

Your email address will not be published. Required fields are marked *