41. CMake
现在学到了嵌入式 / C 高级开发的一个关键技能 —— CMake。
CMake 是现代 C/C++ 工程的灵魂工具,几乎所有大型项目(包括 Linux、ROS、Qt、OpenCV、TensorFlow)都用它。
我会从零开始,系统、深入地教你掌握 CMake ——
让你从 “听说过” → “能看懂” → “能写” → “能设计复杂工程”。
🧩 一、CMake 是什么?为什么要用它?
✅ 一句话解释:
CMake 是一个跨平台的自动化构建系统生成器。
🔍 问题背景:传统方式的痛点
在学 gcc 和 Makefile 时我们可能这样写:
gcc main.c add.c sub.c -o calc
或写个 Makefile:
calc: main.o add.o sub.ogcc main.o add.o sub.o -o calc
这在小项目没问题,但当工程变大时会出问题:
| 问题 | 说明 |
|---|---|
| 📦 工程文件太多 | 手动维护 Makefile 太累 |
| ⚙️ 平台差异 | Windows、Linux、ARM 参数不一样 |
| 🧩 第三方库依赖 | 依赖路径、库版本复杂 |
| 🔁 可移植性差 | 切换平台要重新改 Makefile |
✅ CMake 解决的痛点:
CMake 不直接编译代码,它会:
自动生成对应平台的 Makefile 或 Visual Studio 工程文件。
然后你再用 make 或 IDE 编译。
流程如下:
CMakeLists.txt ──→ CMake ──→ 生成 Makefile ──→ make ──→ 编译程序
🧱 二、CMake 的基本使用流程
假设项目结构如下:
project/
├── CMakeLists.txt
├── main.c
├── add.c
└── add.h
1️⃣ 写一个最简单的 CMakeLists.txt
# 指定最小 CMake 版本
cmake_minimum_required(VERSION 3.10)# 工程名称
project(MyCalc C)# 指定源文件并生成可执行文件
add_executable(calc main.c add.c)
2️⃣ 编译运行
mkdir build
cd build
cmake ..
make
CMake 会自动生成 Makefile,然后 make 负责编译。
结果:
[100%] Built target calc
生成的可执行文件在 build/calc。
✅ 这是最基础的 CMake 使用方式。
🧠 三、CMake 基本语法讲解
| 指令 | 作用 | 示例 | |
|---|---|---|---|
cmake_minimum_required(VERSION 3.10) | 指定最低 CMake 版本 | ||
project(<name> [LANGUAGES]) | 声明项目名 | project(MyApp C CXX) | |
add_executable(<name> [sources...]) | 生成可执行文件 | add_executable(app main.c) | |
| `add_library( [STATIC | SHARED] [sources…])` | 生成库文件 | add_library(math STATIC add.c) |
target_link_libraries(target lib1 lib2) | 链接库文件 | target_link_libraries(app math) | |
include_directories(<dir>) | 添加头文件路径 | include_directories(include) | |
link_directories(<dir>) | 添加库路径 | link_directories(lib) | |
set(var value) | 设置变量 | set(SRC main.c add.c) | |
${var} | 引用变量 | ${SRC} |
⚙️ 四、变量与目录结构实战
示例工程:
project/
├── src/
│ ├── main.c
│ ├── add.c
│ └── sub.c
├── include/
│ └── calc.h
└── CMakeLists.txt
CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyCalc C)# 添加头文件目录
include_directories(include)# 定义源文件变量
set(SRCsrc/main.csrc/add.csrc/sub.c
)# 生成可执行文件
add_executable(calc ${SRC})
🧩 五、构建静态库与动态库
1️⃣ 生成静态库 .a
add_library(math STATIC add.c sub.c)
add_executable(calc main.c)
target_link_libraries(calc math)
2️⃣ 生成动态库 .so
add_library(math SHARED add.c sub.c)
add_executable(calc main.c)
target_link_libraries(calc math)
区别:
STATIC→.aSHARED→.so
CMake 会自动处理链接。
🧠 六、多级目录工程
结构:
project/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ ├── main.c
│ ├── add.c
│ └── sub.c
└── include/└── calc.h
根目录 CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyCalc C)
add_subdirectory(src)
子目录 src/CMakeLists.txt:
include_directories(../include)
add_executable(calc main.c add.c sub.c)
👉 这样大型项目就可以模块化管理。
🔍 七、条件控制与平台判断
if(WIN32)message("This is Windows")
elseif(UNIX)message("This is Linux")
endif()
还能根据编译器判断:
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")message("Using GCC")
endif()
🧰 八、调试与信息输出
message(STATUS "Project source dir: ${PROJECT_SOURCE_DIR}")
message(WARNING "This is a warning")
message(FATAL_ERROR "Stop building")
🧩 九、CMake 构建选项
CMake 支持定义选项(类似宏开关):
option(USE_MYMATH "Use custom math library" ON)if(USE_MYMATH)add_subdirectory(math)target_link_libraries(calc mymath)
endif()
启用或禁用:
cmake -DUSE_MYMATH=ON ..
🧠 十、安装与导出
install(TARGETS calc DESTINATION bin)
install(FILES include/calc.h DESTINATION include)
执行:
make install
会将可执行文件复制到 /usr/local/bin。
🧩 十一、CMake + Cross Compile(交叉编译)
在嵌入式系统中,CMake 非常方便地支持交叉编译。
创建一个工具链文件 arm-toolchain.cmake:
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
然后使用:
cmake -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake ..
make
这样生成的二进制就可以在 ARM 板上运行。
📦 十二、CMake 与第三方库(如 OpenCV)
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(demo main.cpp)
target_link_libraries(demo ${OpenCV_LIBS})
🧱 十三、CMake 的现代写法(target思想)
现代 CMake(3.10+)推荐用 target 而非全局命令:
add_library(math STATIC add.c)
target_include_directories(math PUBLIC include)add_executable(calc main.c)
target_link_libraries(calc PRIVATE math)
👉 优点:
- 更清晰的依赖关系
- 避免全局污染
- 支持大型工程与多目标构建
🧠 十四、学习路径总结
| 阶段 | 学习内容 | 示例 |
|---|---|---|
| 入门 | 单文件项目 | add_executable |
| 进阶 | 模块化工程 | add_subdirectory |
| 实战 | 静态库 / 动态库 | add_library |
| 高级 | 条件控制 + 变量 | if / set |
| 专业 | 交叉编译 + 外部库 | toolchain / find_package |
| 现代 | target-based 写法 | target_link_libraries |
🚀 十五、练习建议
1️⃣ 写一个支持选项开关的程序:
cmake -DUSE_DEBUG=ON ..
2️⃣ 构建一个包含静态库的工程
3️⃣ 尝试交叉编译到 ARM 平台
4️⃣ 用 find_package(OpenCV REQUIRED) 编译图像项目
是否希望我下一步继续教你:
🔧 “手写一个完整的多模块 CMake 工程实战”
包含静态库、头文件、条件宏、可执行文件和交叉编译适配?
非常好 ✅
你已经掌握了 CMake 的基础与理论,接下来我们进入 实战阶段。
我们要 从零手写一个完整的多模块 CMake 工程,涵盖:
- 📦 多目录结构
- ⚙️ 静态库与头文件管理
- 🧩 CMake 变量与选项控制
- 🪛 条件编译
- 🔧 可移植构建(含交叉编译)
🧱 一、项目目标
我们要写一个简单的计算器程序:
calc = add + sub + main
结构清晰、可扩展,还能打包安装。
🗂️ 二、项目目录结构
CalcProject/
├── CMakeLists.txt # 顶层配置
├── include/ # 公共头文件
│ └── calc.h
├── src/ # 主程序
│ ├── main.c
│ └── CMakeLists.txt
├── math/ # 数学库模块
│ ├── add.c
│ ├── sub.c
│ ├── CMakeLists.txt
│ └── math.h
└── build/ # 构建输出目录(自动生成)
🧩 三、顶层 CMakeLists.txt
# 1. 指定最低版本
cmake_minimum_required(VERSION 3.10)# 2. 声明工程
project(CalcProject C)# 3. 添加子目录(模块)
add_subdirectory(math)
add_subdirectory(src)# 4. 安装规则(可选)
install(DIRECTORY include/ DESTINATION include)
🔢 四、math 模块(生成静态库)
文件:math/CMakeLists.txt
# 指定头文件目录
include_directories(${PROJECT_SOURCE_DIR}/include)# 源文件
set(MATH_SRC add.c sub.c)# 生成静态库 libmath.a
add_library(math STATIC ${MATH_SRC})# 公开头文件(modern CMake 写法)
target_include_directories(math PUBLIC ${PROJECT_SOURCE_DIR}/include)
math.h:
#ifndef MATH_H
#define MATH_Hint add(int a, int b);
int sub(int a, int b);#endif
add.c:
#include "math.h"
int add(int a, int b) {return a + b;
}
sub.c:
#include "math.h"
int sub(int a, int b) {return a - b;
}
💻 五、src 模块(生成可执行文件)
文件:src/CMakeLists.txt
# 指定头文件目录
include_directories(${PROJECT_SOURCE_DIR}/include)# 源文件
set(SRC main.c)# 生成可执行文件
add_executable(calc ${SRC})# 链接 math 库
target_link_libraries(calc PRIVATE math)# 安装规则
install(TARGETS calc DESTINATION bin)
main.c:
#include <stdio.h>
#include "math.h"int main() {int a = 8, b = 3;printf("%d + %d = %d\n", a, b, add(a, b));printf("%d - %d = %d\n", a, b, sub(a, b));return 0;
}
🧰 六、编译与运行
mkdir build
cd build
cmake ..
make
./src/calc
输出:
8 + 3 = 11
8 - 3 = 5
恭喜!🎉
你现在已经构建了一个多模块工程。
🧠 七、添加选项控制(宏开关)
我们希望能通过选项启用调试模式:
修改顶层 CMakeLists.txt:
option(USE_DEBUG "Enable debug info" OFF)if(USE_DEBUG)message(STATUS "Debug mode enabled")add_compile_definitions(DEBUG_MODE)
endif()add_subdirectory(math)
add_subdirectory(src)
修改 main.c:
#include <stdio.h>
#include "math.h"int main() {int a = 8, b = 3;printf("%d + %d = %d\n", a, b, add(a, b));printf("%d - %d = %d\n", a, b, sub(a, b));#ifdef DEBUG_MODEprintf("[DEBUG] Program finished successfully.\n");
#endifreturn 0;
}
构建时启用:
cmake -DUSE_DEBUG=ON ..
make
⚙️ 八、支持动态库(.so)
在 math/CMakeLists.txt 中:
add_library(math SHARED ${MATH_SRC})
CMake 会自动生成 libmath.so 并调整链接。
🧩 九、交叉编译支持(嵌入式)
创建工具链文件 toolchain-arm.cmake:
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
SET(CMAKE_FIND_ROOT_PATH /opt/arm/sysroot)
使用时:
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake ..
make
这样你生成的 calc 就可以在 ARM 板(比如嵌入式 Linux)运行。
🧱 十、CMake 输出路径控制(可选)
在顶层添加:
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
生成的文件结构:
build/
├── bin/calc
└── lib/libmath.a
🧩 十一、安装与卸载
安装:
sudo make install
默认安装到 /usr/local/bin 与 /usr/local/include
卸载:
sudo xargs rm < install_manifest.txt
📦 十二、总结:你现在掌握的技能
| 模块 | 内容 |
|---|---|
| ✅ 基础结构 | project / add_executable / add_library |
| 🧩 模块化 | add_subdirectory、include_directories |
| ⚙️ 控制编译 | option / if / define |
| 📦 库管理 | STATIC / SHARED |
| 🔧 可移植性 | toolchain 跨平台编译 |
| 🏗️ 安装 | install() 导出结构 |
| 🧠 现代化 | target_include_directories / target_link_libraries |
🚀 高级 CMake 实战:构建一个嵌入式驱动框架(支持多平台交叉编译 + 动态配置)
我们现在进入 CMake 高级实战篇 ——
一步步教你构建一个可扩展的 嵌入式驱动框架工程。
这一部分内容会帮你真正理解:
🔧 “CMake 不只是构建工具,它是跨平台自动化工程系统的核心。”
🧩 一、项目目标:跨平台嵌入式驱动框架
我们要创建一个框架,能:
- 在 PC(x86)上编译测试;
- 在 ARM 板上交叉编译;
- 根据硬件类型(比如 STM32、Raspberry Pi)自动切换驱动;
- 输出静态库或动态库;
- 一行命令完成构建、安装。
📦 二、目录结构
EmbeddedDriver/
├── CMakeLists.txt # 顶层配置
├── include/
│ └── driver.h
├── core/
│ ├── driver_core.c
│ ├── CMakeLists.txt
├── drivers/
│ ├── stm32/
│ │ ├── stm32_driver.c
│ │ ├── CMakeLists.txt
│ ├── rpi/
│ │ ├── rpi_driver.c
│ │ ├── CMakeLists.txt
├── app/
│ ├── main.c
│ ├── CMakeLists.txt
└── toolchain-arm.cmake # 交叉编译配置
⚙️ 三、顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(EmbeddedDriver C)# 输出路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)# 添加 include 目录
include_directories(${PROJECT_SOURCE_DIR}/include)# 自定义选项
option(USE_STM32 "Use STM32 driver" OFF)
option(USE_RPI "Use Raspberry Pi driver" OFF)
option(BUILD_SHARED "Build shared library" OFF)# 条件判断:平台驱动选择
if(USE_STM32)message(STATUS "✅ Using STM32 driver")add_subdirectory(drivers/stm32)
elseif(USE_RPI)message(STATUS "✅ Using Raspberry Pi driver")add_subdirectory(drivers/rpi)
else()message(STATUS "⚙️ Default: generic driver")add_subdirectory(core)
endif()# 主程序
add_subdirectory(app)# 安装公共头文件
install(DIRECTORY include/ DESTINATION include)
🧱 四、核心模块 core/CMakeLists.txt
set(CORE_SRC driver_core.c)if(BUILD_SHARED)add_library(driver_core SHARED ${CORE_SRC})
else()add_library(driver_core STATIC ${CORE_SRC})
endif()target_include_directories(driver_core PUBLIC ${PROJECT_SOURCE_DIR}/include)
driver_core.c:
#include "driver.h"
#include <stdio.h>void driver_init() {printf("Core driver initialized.\n");
}
⚙️ 五、STM32 驱动模块
drivers/stm32/CMakeLists.txt
set(STM32_SRC stm32_driver.c)add_library(driver_core STATIC ${STM32_SRC})
target_include_directories(driver_core PUBLIC ${PROJECT_SOURCE_DIR}/include)
stm32_driver.c:
#include "driver.h"
#include <stdio.h>void driver_init() {printf("STM32 driver initialized.\n");
}
⚙️ 六、Raspberry Pi 驱动模块
drivers/rpi/CMakeLists.txt
set(RPI_SRC rpi_driver.c)add_library(driver_core STATIC ${RPI_SRC})
target_include_directories(driver_core PUBLIC ${PROJECT_SOURCE_DIR}/include)
rpi_driver.c:
#include "driver.h"
#include <stdio.h>void driver_init() {printf("Raspberry Pi driver initialized.\n");
}
💻 七、主程序 app/CMakeLists.txt
set(APP_SRC main.c)add_executable(demo ${APP_SRC})
target_link_libraries(demo PRIVATE driver_core)install(TARGETS demo DESTINATION bin)
main.c:
#include "driver.h"int main() {driver_init();return 0;
}
🧰 八、驱动头文件 include/driver.h
#ifndef DRIVER_H
#define DRIVER_Hvoid driver_init(void);#endif
⚙️ 九、编译与运行
🧩 默认编译(核心驱动)
mkdir build && cd build
cmake ..
make
./bin/demo
输出:
Core driver initialized.
🧩 切换为 STM32 驱动
cmake -DUSE_STM32=ON ..
make
./bin/demo
输出:
STM32 driver initialized.
🧩 构建动态库版本
cmake -DBUILD_SHARED=ON ..
make
输出:libdriver_core.so
🔧 十、交叉编译配置
创建 toolchain-arm.cmake:
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
SET(CMAKE_FIND_ROOT_PATH /opt/arm/sysroot)
构建:
cmake -DCMAKE_TOOLCHAIN_FILE=../toolchain-arm.cmake -DUSE_STM32=ON ..
make
生成的可执行文件可直接拷贝到 ARM 板(如 STM32MP157、Raspberry Pi)运行。
🧠 十一、CMake 宏调试技巧
CMake 脚本中最常见的问题是“变量没生效”。
以下是常用调试命令:
| 命令 | 用途 |
|---|---|
message(STATUS "变量X = ${X}") | 输出变量值 |
message(WARNING "警告内容") | 输出黄色警告 |
message(FATAL_ERROR "中止构建") | 输出错误并停止 |
cmake --trace-expand | 显示宏展开细节 |
cmake --debug-output | 显示 CMake 变量解析过程 |
示例:
message(STATUS "C compiler: ${CMAKE_C_COMPILER}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
🧩 十二、驱动模板自动生成(CMake 脚本高级技巧)
CMake 可以自动生成驱动代码模板:
在 CMakeLists.txt 中加入:
set(DRIVER_NAME "custom")configure_file(${PROJECT_SOURCE_DIR}/template/driver_template.c.in${PROJECT_BINARY_DIR}/drivers/${DRIVER_NAME}_driver.c
)
模板文件 driver_template.c.in:
#include "driver.h"
#include <stdio.h>void driver_init() {printf("${DRIVER_NAME} driver initialized.\n");
}
执行 CMake 后,会自动生成:
drivers/custom_driver.c
其中 ${DRIVER_NAME} 会被替换成 “custom”。
🚀 十三、总结:你现在已经掌握
| 能力 | 内容 |
|---|---|
| 🧱 架构 | 多模块分层工程结构 |
| ⚙️ 编译控制 | 静态 / 动态库切换、条件驱动加载 |
| 💻 平台适配 | x86 与 ARM 双平台构建 |
| 🧠 宏调试 | message 与 trace 调试技巧 |
| 🔧 自动生成 | configure_file 模板机制 |
| 🪛 实战意义 | 嵌入式系统驱动层统一构建框架 |
是否希望我下一步带你进入 「CMake + GCC + 交叉编译联合调试(gdbserver + QEMU 实战)」
—— 这部分会讲如何 在 PC 上远程调试 ARM 目标程序?
