制造商零件编号 ESP32-S3-DEVKITC-1-N32R8V
ESP32-S3-WROOM-2-N32R8V DEV BRD
Espressif Systems
If you’ve done any C or C++ development, you’ve likely come across CMake. CMake is a powerful, open-source, cross-platform build system generator. Instead of forcing you to learn a specific build system like make or ninja in detail, CMake allows you to write a single set of configuration files that can generate the appropriate build system files for your environment. This approach simplifies your build process, helps keep your project structure organized, and makes your code more portable across different operating systems and platforms.
Previously, we installed Zephyr and ran a simple blinking LED demo. In this post, we’ll dive into the basics of CMake: why it’s useful, a simple “hello, world” sample application, and how to use it with Zephyr. By the end, you should feel comfortable setting up and building your own CMake-based project.
All example code and solutions for this series can be found in this repository: https://github.com/ShawnHymel/introduction-to-zephyr/
CMake is often described as a “meta” build system tool. Instead of building your project directly, CMake uses your configuration files (called CMakeLists.txt) to produce build system files. These output files can then be read by common build tools (like make, ninja, or even IDEs like Visual Studio or Xcode) to compile and link your project.
CMake is cross-platform, providing true portability, and it works with a wide array of build systems, like make and ninja. It is also scalable to large projects (like Zephyr), and it is widely used in industry for software and firmware projects.
You can install CMake on your local machine (use your OS’s package manager or one of the installers here), or you can run the Docker image from the Zephyr course’s GitHub repo (we showed how to run the image in the previous tutorial).
CMake configurations revolve around a file named CMakeLists.txt. Every directory with source files you want to compile typically has one. The top-level CMakeLists.txt file describes minimum CMake versions, project names, and the targets (executables, libraries) you’d like to build.
Note that we will demonstrate CMake 3.20 in this tutorial. You can reference the API documentation here: https://cmake.org/documentation/
We will start with a simple “Hello, World!” application to demonstrate the bare minimum for using CMake. Here is our directory structure:
02_demo_cmake/
├─ include/
│ └─ my_lib.h
├─ src/
│ ├─ my_lib.c
│ └─ main.c
└─ CMakeLists.txt
We’ll start by creating a simple header file that declares a single function say_hello().
File: 02_demo_cmake/include/my_lib.h
#ifndef MY_LIB_H
#define MY_LIB_H
void say_hello();
#endif
Next, we define say_hello() in our library source file. This function will print a line of text to the console.
File: 02_demo_cmake/src/my_lib.c
#include <stdio.h>
#include "my_lib.h"
void say_hello() {
printf("Hello, world!\r\n");
}
Our main() function calls say_hello().
File: 02_demo_cmake/src/main.c
#include "my_lib.h"
int main() {
say_hello();
return 0;
}
This is where the magic happens. Our CMakeLists.txt ties it all together.
File: 02_demo_cmake/CMakeLists.txt
# Specify a minimum CMake version
cmake_minimum_required(VERSION 3.20.0)
# Name the project
project(
hello_world
VERSION 1.0
DESCRIPTION "The classic"
LANGUAGES C
)
# Create a static library target named "my_lib"
add_library(my_lib
STATIC
src/my_lib.c
)
# Set the include directories for the library. PUBLIC adds the directory
# to the search path for any targets that link to this library.
target_include_directories(
my_lib
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Create an executable target with the same name as the project name
add_executable(
${PROJECT_NAME}
src/main.c
)
# Link the library to the executable. PRIVATE means that the library is not
# exposed to targets that depend on this target.
target_link_libraries(
${PROJECT_NAME}
PRIVATE
my_lib
)
1. cmake_minimum_required(VERSION 3.20.0): We tell CMake we need at least version 3.20. This ensures commands work as expected.
2. project(hello_world VERSION 1.0 DESCRIPTION "The classic" LANGUAGES C):
3. add_library(my_lib STATIC src/my_lib.c):
4. target_include_directories(my_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include):
5. add_executable(${PROJECT_NAME} src/main.c):
6. target_link_libraries(${PROJECT_NAME} PRIVATE my_lib):
Start from your project’s root directory (02_demo_cmake in our example):
You can use the -S and -B options with cmake to specify the source directory (current dir .) and build directory (build). For example:
cmake -S . -B build
This command runs CMake in the current directory’s context and writes the generated build system (like Makefiles) into a build directory. If you haven’t created build yet, it will be created automatically.
Once generation is complete, you can build your project:
cmake --build build
By default, CMake tries to use make on Unix-like systems.
./build/hello_world
This should print:
Hello, world!
Congratulations! You’ve just successfully built and run a simple project using CMake.
One of CMake’s strengths is its ability to generate different build systems. By default, CMake uses make, but you can use any one of the supported build systems. To view the supported generators, enter:
cmake -h
You should see the list of generators:
To explicitly use the make system, enter the following to generate a Makefile and build your project:
cmake -S . -B build -G "Unix Makefiles"
cmake --build build
You can also use Ninja (assuming you have it installed–it should come installed with Zephyr if you are using the Docker image):
cmake -S . -B build -G "Ninja"
cmake --build build
CMake takes care of generating the appropriate files. You don’t have to modify your CMake configuration—just specify the generator you want.
Our example was simple, but CMake shines as your project grows. Here are a few ways to organize bigger projects:
Here are some great resources if you want to dive deeper into CMake:
CMake excels at building projects for embedded devices or integrating with frameworks like Zephyr for IoT development. If you look at our blinky example from the previous tutorial, you can see how CMakeLists.txt is configured to use Zephyr. A few things to note:
Zephyr encourages you to use the west meta-tool to build your projects, which, admittedly, does make the process easier. However, west relies on CMake under the hood. Understanding CMake independently, as we did in this tutorial, helps clarify what’s going on behind the scenes.
Each video/tutorial going forward will issue you a challenge to test your understanding of the concepts covered. For CMake, your challenge is to combine the “Hello, World!” example presented here and the blinky example from the previous episode. Create a header and source file to simply call printf(“Hello!!!\r\n”); (as a library) in the blinky example. Modify your main.c application to call your say_hello(); function each time the LED toggles. Modify the CMakeLists.txt to include and link to your library.
When you compile, flash, and run your modified blink application, you should see “Hello!!” printed to the console (in addition to the LED state, if you left that print statement in the code):
If you run into any errors or just want to check your code, you can find my solution here: https://github.com/ShawnHymel/introduction-to-zephyr/tree/main/workspace/apps/02_solution_hello_blink
CMake is a powerful tool for any C or C++ developer aiming for cross-platform portability and maintainable build configurations.
As your projects grow, you’ll appreciate CMake’s flexibility even more. You can easily add tests using add_test(), integrate third-party dependencies with find_package(), and apply sophisticated configuration management. The fundamentals shown here form the bedrock upon which you can build more complex software.