CMake应用
前言
当我们完成代码时,我们的代码是如何工作的?
源代码到可执行文件的过程可以分为:预处理 -> 编译 -> 汇编 -> 链接。
- 预处理(Preprocessing):在这个阶段,预处理器会处理所有以#开头的指令(如#include、#define、#if等)。例如,#include指令会将头文件的内容插入到源文件中,#define会将宏展开。
- 编译(Compilation): 预处理后的源代码会被送入编译器。编译器将其转化为汇编语言代码,或者在某些系统中直接转化为中间代码。编译器会进行语法检查、语义分析、优化等工作。在这个阶段,编译器生成一个目标文件(通常以.o或.obj为扩展名)。
- 汇编(Assembly): 编译器生成的汇编语言代码会被汇编器转化为机器代码。这个过程会生成一个目标文件(.o 或 .obj 文件),这个文件是机器代码的二进制格式。
- 链接(Linking): 链接器负责将一个或多个目标文件和相关的库(如标准库或其他第三方库)链接成一个单一的可执行文件。在这个阶段,链接器会解决符号(如函数、变量等)的引用,并将它们正确地映射到内存地址。如果程序引用了标准库或其他外部库,链接器也会将这些库的代码加到最终的可执行文件中。
在这个过程中,g++(GNU编译器)等工具通常会在后台自动执行这些步骤。
所以CMake是什么?可以解决什么问题?
CMake是个一个开源的跨平台自动化建构系统,用来管理软件建置的程序,并不依赖于某特定编译器,并可支持多层目录、多个应用程序与多个函数库。
无论是在 Linux、Windows还是macOS,开发者只需要维护一套CMakeLists.txt配置文件,而CMake会根据平台的不同生成适合的构建脚本(例如 Makefile、Visual Studio项目文件等)。这使得跨平台开发变得非常简单。
CMake通过使用简单的配置文件 CMakeLists.txt,自动生成不同平台的构建文件(如Makefile、Ninja 构建文件、Visual Studio工程文件等),简化了项目的编译和构建过程。
CMake本身不是构建工具,而是生成构建系统的工具,它生成的构建系统可以使用不同的编译器和工具链。
1. 在Windows平台上使用CMake与VS Code编译C++源码
1.1. 安装必备工具
- 安装VS Code
- 安装VS Code插件,CMake Tools(用于支持 CMake 项目配置和生成)
- 安装 CMake,下载适合自己的版本;安装后,打开命令行输入
cmake --version来确认是否安装成功,确保将CMake添加到系统路径(安装的时候勾选一下就可以啦)。
1.2. 配置CMake项目
- 创建C++项目结构,假设你有一个简单的C++项目,目录结构可能如下:
my_project/ ├── CMakeLists.txt ├── main.cpp - 编辑 CMakeLists.txt 文件 在项目根目录创建 CMakeLists.txt 文件,内容可以简单如下:
cmake_minimum_required(VERSION 3.10) project(MyApp) # 设置 C++ 标准 set(CMAKE_CXX_STANDARD 17) # 添加源文件 add_executable(MyApp main.cpp) - 编辑 main.cpp 文件 在项目根目录创建 main.cpp 文件,内容可以简单如下:
#include <iostream> int main() { std::cout << "Hello, CMake and VS Code!" << std::endl; return 0; }
1.3. 配置VS Code
配置VS Code步骤如下:
- 打开刚刚创建的C++项目文件夹。
- 生成构建文件,按Ctrl+Shift+P打开命令面板,输入CMake: Configure,选择自己需要的编译器
- 配置完成后,你可以开始构建项目:按Ctrl+Shift+P打开命令面板,输入CMake: Build,选择构建目标进行编译;或者直接选择点击状态左下角齿轮都可以。
- 这时CMake会在项目目录下创建一个build目录,并将编译的中间文件和最终的可执行文件放在那里。
- 如果需要调试,直接点击状态栏小虫子进行debug
- 如果需要清理构建目录、重新构建,可以使用鼠标右击CMakeLists.txt进行快速操作
2. CMakeLists.txt文件结构及语法
参考菜鸟Cmake教程-Cmake语法,根据需要往上堆就可以了
cmake_minimum_required(VERSION 3.10)
project(MyApp)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
# 添加头文件搜索路径
include_directories(include)
# 查找Boost库,如果Boost没有找到,CMake会报错并停止构建。
# 如果找到了Boost,它会设置一些CMake变量,例如Boost_INCLUDE_DIRS和Boost_LIBRARIES,这些可以用来配置包含目录和链接库。
find_package(Boost REQUIRED)
# 添加源文件
set(SOURCES src/main.cpp src/foo.cpp)
# 生成可执行文件
add_executable(MyApp ${SOURCES})
# 链接Boost库
target_link_libraries(MyApp Boost::Boost)
target_link_libraries(MyApp /path/to/your/libs/mylib.dll)
# 安装规则
install(TARGETS MyApp DESTINATION bin)
3. 使用CMake编译单片机C程序
使用CMake编译单片机程序其核心思想就是通过CMake的构建系统生成适合单片机开发的编译环境(Makefile文件),最终调用交叉编译工具链(gcc)生成可烧录的固件,通常是.bin,.hex格式文件。当我们的烧录文件成功生成后,再借助烧录工具(JLink)对程序进行烧录和调试。
3.1. 环境准备
- 旧版arm-none-eabi-gcc、新版arm-none-eabi-gcc,单片机交叉编译工具链,主要用于ARM Cortex-M,如果选择解压安装记得配置环境变量;
- VS Code,代码编辑器;
- CMake,自动化建构系统;
- J-Link,单片机代码烧录和debug工具;
- MinGW,是一个用于 Windows 平台的开发工具集,提供了一套 GNU 工具链(如 GCC、G++、GDB、MAKE);
3.2. 配置CMakeLists.txt
我的工程目录结构:
.
├── gcc
│ ├── build
│ │ └── output
│ └── CMakeLists.txt
├── source
│ ├── app
│ ├── bsp
│ ├── chip
│ ├── common
│ ├── driver
│ └── ...
# 1. 设置系统类型和处理器架构(交叉编译相关),置顶(否则出现--major-image-version问题)
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 2. 设置交叉编译工具链路径和编译器,编译器的配置要在project之前!
set(TOOLCHAIN_PATH "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi")
set(CMAKE_C_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc.exe")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-g++.exe")
set(CMAKE_ASM_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc.exe")
set(CMAKE_OBJCOPY "${TOOLCHAIN_PATH}/bin/arm-none-eabi-objcopy.exe")
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# 3. 指定CMake最低版本和项目名称
cmake_minimum_required(VERSION 3.15)
project(NewProject LANGUAGES C ASM) # 支持C语言和汇编语言
# 4. 设置编译选项和标准
set(CMAKE_C_STANDARD 99)
add_definitions(-DHC32F460)
# 5. MCU相关编译和链接参数, -g
set(MCU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_C_FLAGS "${MCU_FLAGS} -ffunction-sections -fdata-sections")
set(CMAKE_ASM_FLAGS "${MCU_FLAGS} -x assembler-with-cpp")
set(CMAKE_EXE_LINKER_FLAGS
"${MCU_FLAGS} -g\
-Wl,--gc-sections\
-T${CMAKE_SOURCE_DIR}/../source/chip/hc32f4xx/linker/HC32F460xE.ld\
-Wl,-Map=${PROJECT_NAME}.map"
)
# -Wl,-v\ # 编译时输出详细信息
# -Wl,--verbose\
# 6. 设置输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/output)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/output)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/build/output)
message(STATUS "CMAKE_RUNTIME_OUTPUT_DIRECTORY : ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
# 7. 设置源码和头文件目录
set(SRC_DIR
${CMAKE_SOURCE_DIR}/../source
${CMAKE_SOURCE_DIR}/../source/app
${CMAKE_SOURCE_DIR}/../source/bsp
${CMAKE_SOURCE_DIR}/../source/chip/core
${CMAKE_SOURCE_DIR}/../source/chip/hc32f4xx
${CMAKE_SOURCE_DIR}/../source/common
${CMAKE_SOURCE_DIR}/../source/driver/hc32_ll_driver
)
set(INC_DIR ${SRC_DIR})
include_directories(${INC_DIR})
# 8. 查找所有C源文件
set(SOURCES "")
foreach(DIR ${SRC_DIR})
file(GLOB_RECURSE DIR_SOURCES ${DIR}/*.c)
list(APPEND SOURCES ${DIR_SOURCES})
endforeach()
# 9. 添加启动文件,注意这里gccD的启动文件和Keil有所区别
set(STARTUP_FILE "${CMAKE_SOURCE_DIR}/../source/chip/hc32f4xx/start/gcc/startup_hc32f460.S")
# 10. 添加可执行文件目标
add_executable(${PROJECT_NAME}.elf
${SOURCES}
${STARTUP_FILE}
)
# 11. 设置目标输出属性
set_target_properties(${PROJECT_NAME}.elf PROPERTIES
SUFFIX ".elf"
OUTPUT_NAME "${PROJECT_NAME}"
)
# 12. 生成hex和bin文件
add_custom_command(TARGET ${PROJECT_NAME}.elf POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "=== ELF EXISTS? ==="
COMMAND ${CMAKE_COMMAND} -E echo "OUTDIR=${CMAKE_RUNTIME_OUTPUT_DIRECTORY}"
COMMAND ${CMAKE_COMMAND} -E echo "$<TARGET_FILE:${PROJECT_NAME}.elf>"
COMMAND ${CMAKE_OBJCOPY} -O ihex $<TARGET_FILE:${PROJECT_NAME}.elf> ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}.hex
COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}.elf> ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${PROJECT_NAME}.bin
COMMENT "Generating hex and binary files"
)
# 13. 安装目标(可选),将生成的固件文件(如firmware.elf)复制到指定目录(通常是系统标准路径或自定义路径),便于后续统一管理或自动化脚本调用
install(TARGETS ${PROJECT_NAME}.elf
DESTINATION ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}
)
# 14. 文档生成目标(可选),定义一个名为docs的虚拟目标,用于触发文档生成
add_custom_target(docs
COMMAND make -C ${CMAKE_SOURCE_DIR}/docs html
COMMENT "Building documentation"
)
注意
- 上述中
startup.S和.ld文件,通常可以从官方例程中获取。 - 核心思路,通过编写的CMakeList自动构建代码工程的Makefile文件,执行make命令生成
.elf文件,再将.elf文件转换成.bin,.hex文件。 - 编译命令
cmake -G "Unix Makefiles" -S . -B build # -G "Unix Makefiles":指定生成器,这里选择“Unix Makefiles”,表示生成适用于Unix风格的Makefile文件。这里如果没有强调“Unix Makefiles”,如果Windows上,CMake默认通常会选择 Visual Studio的生成器(如 "Visual Studio 17 2022"),而不是Makefile。 # -S .:指定源码目录为当前目录(.)。 # -B build:指定构建输出目录为build文件夹。 cmake --build build # cmake --build:调用CMake的构建功能。 # build:指定要构建的目录(即上一步生成Makefile的目录)。
3.3. J-Link烧录与调试
到目前位置我们已经成功的生成了烧录文件,接下来就是将文件烧录到单片机当中!
这时候我们通常会使用J-Link,J-Link是Segger公司推出的专业嵌入式开发调试工具,支持ARM Cortex、RISC-V等多种内核的微控制器。
访问Segger官网下载J-Link软件包,软件包主要包含:
- J-Link GDB Server(用于调试)
- J-Flash(用于烧录)
- J-Link Commander(命令行工具)
- 其他一些驱动与调试插件
3.3.1. 使用J-Link烧录固件
- 打开 J-Flash
- 创建新工程:
- 选择目标芯片(如 HC32F460PETB)或手动配置 Device、Flash 参数。
- 设置Interface(通常为 SWD 或 JTAG)。
- 加载固件文件:即我们生成的.hex文件
- 连接目标板(硬件连接):确保 J-Link 与目标板正确连接(SWDIO、SWCLK、GND、VCC)。
- 点击菜单栏Target → Connect,确认连接成功。
- 烧录固件:点击菜单栏Target → Production Programming,自动擦除并烧录。
3.3.2. 使用J-Link调试
命令行调试:
- 启动 GDB Server:
JLinkGDBServer -device HC32F460xE -if SWD -port 2331如果成功的话,会显示如下信息: Connecting to J-Link... J-Link is connected. Firmware: J-Link V9 compiled May 7 2021 16:26:12 Hardware: V9.60 S/N: 69653600 Feature(s): RDI, GDB, FlashDL, FlashBP, JFlash Checking target voltage... Target voltage: 3.30 V Listening on TCP/IP port 2331 Connecting to target... Halting core... Connected to target Waiting for GDB connection... - 在另一终端启动 GDB(如 arm-none-eabi-gdb):
arm-none-eabi-gdb NewProject.elf (gdb) target remote localhost:2331 (gdb) load # 烧录固件 (gdb) monitor reset # 复位 (gdb) b main # 设置断点 (gdb) c # 继续执行
VS Code + Cortex-Debug调试:
- 安装Cortex-Debug插件
- 配置launch.json
{ "version": "0.2.0", "configurations": [ { "name": "Cortex Debug (JLink)", "type": "cortex-debug", "request": "launch", "servertype": "jlink", "device": "HC32F460xE", // 按实际芯片型号填写 "interface": "swd", "cwd": "${workspaceRoot}/gcc/build/output", "executable": "NewProject.elf", "runToEntryPoint": "main", "preLaunchTask": "build", "gdbPath": "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi/bin/arm-none-eabi-gdb.exe" } ] } - 配置arm-toolchain.cmake
# VSCode CMake Tools默认会用主机编译器测试能否编译主机程序,但你指定的是交叉编译器(arm-none-eabi-gcc),它不能编译和运行Windows下的测试程序。 # 使用Toolchain文件解决 set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi/bin/arm-none-eabi-gcc.exe") set(CMAKE_CXX_COMPILER "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi/bin/arm-none-eabi-g++.exe") set(CMAKE_ASM_COMPILER "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi/bin/arm-none-eabi-gcc.exe") set(CMAKE_OBJCOPY "E:/Program Files/arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi/bin/arm-none-eabi-objcopy.exe") set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) - 在运行调试中,直接选择Cortex Debug(JLink)进行调试,效果跟Keil Debug基本一致
常见问题&解决办法
CMake 可执行文件错误: “”。请检查以确保它已安装
这个问题出现在第二章 1.3节 配置VS Code过程中的第三个步骤的操作中发生
解决办法:
- 找到CMake Tool扩展,点击管理
- 在CMake Tool扩展的配置中,找到Cmake: Cmake Path
- 将之前安装好的Cmake.exe的路径填入即可
无法执行make指令,或找不到Unix Makefiles
make : 无法将“make”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ make -v
+ ~~~~
+ CategoryInfo : ObjectNotFound: (make:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
CMake Error: CMake was unable to find a build program corresponding to "Unix Makefiles". CMAKE_MAKE_PROGRAM is not set. You probably need to select a different build tool.
CMake Error: CMAKE_C_COMPILER not set, after EnableLanguage
CMake Error: CMAKE_ASM_COMPILER not set, after EnableLanguage
-- Configuring incomplete, errors occurred!
解决办法:
- 首先确保安装MinGW环境,MinGW环境中包含make工具;
- 确保环境变量包含”MinGW安装目录/bin”;
- 在MinGW安装目录中,找到
bin目录,查看是否存在make.exe文件,如果没有则查找是否存在mingw32-make.exe(某些旧版MinGW的特殊命名,避免与系统其他make冲突),若存在mingw32-make.exe,则复制一份改名成make.exe。因为我们通常输入make而不是mingw32-make; - 尝试执行
make -v,弹出版本信息问题解决。
在复现的过程中发现执行构建命令的过程中置顶的编译器不对,并非arm-none-eabi-gcc,而是MinGW中的gcc
PS D:\Users\GAI\Desktop\WorkSpace\NewProject\gcc> cmake -G "Unix Makefiles" -S . -B build
-- The C compiler identification is GNU 8.1.0
-- The ASM compiler identification is GNU
-- Found assembler: D:/MinGW64/bin/gcc.exe
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - failed
-- Check for working C compiler: D:/MinGW64/bin/gcc.exe
-- Check for working C compiler: D:/MinGW64/bin/gcc.exe - works
-- Detecting C compile features
-- Detecting C compile features - done
-- CMAKE_RUNTIME_OUTPUT_DIRECTORY : D:/Users/GAI/Desktop/WorkSpace/NewProject/gcc/build/output
-- Configuring done (20.2s)
-- Generating done (0.2s)
-- Build files have been written to: D:/Users/GAI/Desktop/WorkSpace/NewProject/gcc/build
可以看到编译时使用了“D:/MinGW64/bin/gcc.exe”,并非我CMakeList指定的arm-none-eabi-gcc
解决办法:必须在project之前设置编译器!
# 2. 设置交叉编译工具链路径和编译器,必须在project之前设置编译器!
set(TOOLCHAIN_PATH "D:/Program Files (x86)/Arm GNU Toolchain arm-none-eabi/13.2 Rel1")
set(CMAKE_C_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc.exe")
set(CMAKE_CXX_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-g++.exe")
set(CMAKE_ASM_COMPILER "${TOOLCHAIN_PATH}/bin/arm-none-eabi-gcc.exe")
set(CMAKE_OBJCOPY "${TOOLCHAIN_PATH}/bin/arm-none-eabi-objcopy.exe")
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # 禁止CMake测试编译器(必须设置)
# 3. 指定 CMake 最低版本和项目名称
cmake_minimum_required(VERSION 3.15)
project(NewProject LANGUAGES C ASM) # 支持C语言和汇编语言
警告 ld.exe: warning: output/NewProject.elf has a LOAD segment with RWX permissions
解决办法: 考虑使用 –no-warn-rwx-segments 屏蔽,参考1
在尝试使用VScode去驱动cmake编译时,出现 Running ‘nmake’ ‘-?’ failed with: no such file or directory报错
这个错误是因为 CMake 尝试使用 nmake(微软的构建工具)作为生成器,但你的系统找不到 nmake
解决办法:
- 在设置中搜索”CMake: Generator”,配置
Unix Makefiles(适用于 GCC/MinGW) - 或者直接编辑 settings.json(Ctrl + Shift + P → Preferences: Open WorkSpace Settings(JSON))
"cmake.generator": "Unix Makefiles", "cmake.sourceDirectory": "${workspaceFolder}/gcc", "cmake.configureSettings": { "CMAKE_TOOLCHAIN_FILE": "${workspaceFolder}/gcc/arm-toolchain.cmake" }, - 然后重新配置cmake,按(Ctrl + Shift + P → CMake: Delete Cache and Reconfigure)清除缓存并重新生成。
在使用CMake编译时代码时,CPU被拉满
打开任务管理器发现编译的时候,Microsoft PC Manager Service进程占了很多CPU,把CPU拉满了,本来公司电脑性能就不行:(
据说是什么安全防护扫描文件的进程,咱也不知道。禁用就完事了。
解决办法: win+R -> services.msc -> Microsoft PC Manager Service -> 禁用
CMake编译报错 could not load cache
起因是改个临时程序,在桌面创建文件夹复制了一份工程,代码修改完成后打算用CMake编译,但是CMake报错
开始时报错”Error: C:/Users/YYJ/Desktop/新建文件夹 (6)/EvaporativeCooling/gcc/build is not a directory”,以为是没有创建build文件夹
遂创建文件夹,报错”could not load cache”,网上查也是各种原因
抱着试试的心态先把中文路径去除,编译通过。。
参考资料
-
Milton. (2022). GCC Arm 12.2编译提示 LOAD segment with RWX permissions 警告. https://www.cnblogs.com/milton/p/16756523.html ↩