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. 安装必备工具

  1. 安装VS Code
  2. 安装VS Code插件,CMake Tools(用于支持 CMake 项目配置和生成)
  3. 安装 CMake,下载适合自己的版本;安装后,打开命令行输入cmake --version来确认是否安装成功,确保将CMake添加到系统路径(安装的时候勾选一下就可以啦)。

1.2. 配置CMake项目

  1. 创建C++项目结构,假设你有一个简单的C++项目,目录结构可能如下:
     my_project/
     ├── CMakeLists.txt
     ├── main.cpp
    
  2. 编辑 CMakeLists.txt 文件 在项目根目录创建 CMakeLists.txt 文件,内容可以简单如下:
     cmake_minimum_required(VERSION 3.10)
     project(MyApp)
    
     # 设置 C++ 标准
     set(CMAKE_CXX_STANDARD 17)
    
     # 添加源文件
     add_executable(MyApp main.cpp)
    
  3. 编辑 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步骤如下:

  1. 打开刚刚创建的C++项目文件夹。
  2. 生成构建文件,按Ctrl+Shift+P打开命令面板,输入CMake: Configure,选择自己需要的编译器
  3. 配置完成后,你可以开始构建项目:按Ctrl+Shift+P打开命令面板,输入CMake: Build,选择构建目标进行编译;或者直接选择点击状态左下角齿轮都可以。
  4. 这时CMake会在项目目录下创建一个build目录,并将编译的中间文件和最终的可执行文件放在那里。
  5. 如果需要调试,直接点击状态栏小虫子进行debug
  6. 如果需要清理构建目录、重新构建,可以使用鼠标右击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烧录固件

  1. 打开 J-Flash
  2. 创建新工程:
  3. 选择目标芯片(如 HC32F460PETB)或手动配置 Device、Flash 参数。
  4. 设置Interface(通常为 SWD 或 JTAG)。
  5. 加载固件文件:即我们生成的.hex文件
  6. 连接目标板(硬件连接):确保 J-Link 与目标板正确连接(SWDIO、SWCLK、GND、VCC)。
  7. 点击菜单栏Target → Connect,确认连接成功。
  8. 烧录固件:点击菜单栏Target → Production Programming,自动擦除并烧录。

3.3.2. 使用J-Link调试

命令行调试:

  1. 启动 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...
    
  2. 在另一终端启动 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调试:

  1. 安装Cortex-Debug插件
  2. 配置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"
             }
         ]
     }
    
  3. 配置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)
    
  4. 在运行调试中,直接选择Cortex Debug(JLink)进行调试,效果跟Keil Debug基本一致

常见问题&解决办法

CMake 可执行文件错误: “”。请检查以确保它已安装

这个问题出现在第二章 1.3节 配置VS Code过程中的第三个步骤的操作中发生

解决办法:

  1. 找到CMake Tool扩展,点击管理
  2. 在CMake Tool扩展的配置中,找到Cmake: Cmake Path
  3. 将之前安装好的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!

解决办法:

  1. 首先确保安装MinGW环境,MinGW环境中包含make工具;
  2. 确保环境变量包含”MinGW安装目录/bin”;
  3. 在MinGW安装目录中,找到bin目录,查看是否存在make.exe文件,如果没有则查找是否存在mingw32-make.exe(某些旧版MinGW的特殊命名,避免与系统其他make冲突),若存在mingw32-make.exe,则复制一份改名成make.exe。因为我们通常输入make而不是mingw32-make;
  4. 尝试执行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

解决办法:

  1. 在设置中搜索”CMake: Generator”,配置Unix Makefiles(适用于 GCC/MinGW)
  2. 或者直接编辑 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"
     },
    
  3. 然后重新配置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”,网上查也是各种原因

抱着试试的心态先把中文路径去除,编译通过。。

参考资料

  1. Milton. (2022). GCC Arm 12.2编译提示 LOAD segment with RWX permissions 警告. https://www.cnblogs.com/milton/p/16756523.html 

results matching ""

    No results matching ""