CMAKE 扫盲
从何下手
首先有这样的教程,非常的“从入门到入门”。👌
如果想要完整地了解一下 CMAKE 的机制和语法,可以跟着这个教程走完全程。但是教程有如下的问题
Note
- 仅仅是学习和了解,无法用于生产环境。教程是基于学习环境,使用的 C++,以及默认的 C 编译器(在我的环境中是 clang ),编译出的也是仅可在桌面运行的 exe 。我们当然是使用 arm_gcc 进行编译,或者至少要会指定其他的 gcc 工具,才可以将工程用于生产。
- 项目结构过分复杂,不利于初学理解。本来就不能用于生产了,整个软件又太复杂,cmakelist 文件过多,对初学者不友好。
所以教程可以先只看 Step1~3 就好了。
其中涉及到的软件工具,需要自行下载并且把可执行文件路径添加到环境变量
- Cmake
- MinGW64
- arm-none-eabi-gcc
或者使用我的文件站中打包好的 便携 vscode 环境
然后用stm32cubemx随便生成一个 debuglink 的项目,生成项目的工具链选择 CMAKE。然后分析一下他生成的这个项目。
下载软件工具
Cmake 和 MinGW64 建议使用 MSYS2 的 MINGW64 分支环境安装,手动安装会有依赖问题导致难以进行,或者依赖分散不利于模块化。
安装 MSYS2 后打开其中的 mingw64.exe ,打如下两条指令
pacman -S mingw-w64-x86_64-make
pacman -S mingw-w64-x86_64-cmake
安装完成后将 MSYS2 的 mingw64/bin 文件夹路径添加进系统环境变量的 path
arm-none-eabi-gcc 需要在官网下载,MSYS2 中下载的不完全,会缺失 GDB 工具。
在 Arm GNU Toolchain Downloads – Arm Developer 官网下载或者是我的文件镜像 arm-gcc。
解压 or 安装后,确认将其中的 bin 文件夹路径添加进系统环境变量的 path。
一个最简单的工程
在 stm32cubemx 中随便生成一个简单的 cmake 工程,大致结构如下。与 cmake 有关的如图。
可执行主构建
生成的主构建文件是根目录下的 CMakeLists.txt
这也是我们使用cmake ../
类似的指令生成原生构建系统所调用的CMakeLists.txt
# 指定CMake的最低版本要求为3.22
cmake_minimum_required(VERSION 3.22)
#
# 该文件是cmake调用的主构建文件
# 用户可以根据需要自由修改此文件。
#
# 设置编译器设置部分
set(CMAKE_C_STANDARD 11) # 设置C标准为C11
set(CMAKE_C_STANDARD_REQUIRED ON) # 要求使用指定的C标准
set(CMAKE_C_EXTENSIONS ON) # 启用编译器扩展
# 定义构建类型
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug") # 如果未设置CMAKE_BUILD_TYPE,则默认设置为"Debug"。该参数可以在使用类似"cmake ../"生成原生构建系统时添加-DCMAKE_BUILD_TYPE=Release指定
endif()
# 设置项目名称
set(CMAKE_PROJECT_NAME DebugBuild) # 设置项目名称为DebugBuild
# 包含工具链文件
include("cmake/gcc-arm-none-eabi.cmake")
# 启用编译命令生成,以便于其他工具进行索引例如clangd
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) # 生成compile_commands.json,以便IDE或工具使用
# 启用CMake对ASM和C语言的支持
enable_language(C ASM) # 启用C和汇编(ASM)语言支持
# 核心项目设置
project(${CMAKE_PROJECT_NAME}) # 定义项目,使用之前设置的项目名称
message("Build type: " ${CMAKE_BUILD_TYPE}) # 消息输出构建类型
# 创建一个可执行对象
add_executable(${CMAKE_PROJECT_NAME}) # 定义一个可执行目标,使用项目名称
# 添加子目录部分,这会自动处理子目录中的CMakeLists.txt文件
add_subdirectory(cmake/stm32cubemx) # 添加子目录,通常包含STM32CubeMX生成的代码
# 链接目录设置
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
# 添加用户定义的库搜索路径
# e.g., "/path/to/libs"
)
# 向可执行目标添加源文件
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# 添加额外的源文件
# e.g., "src/main.c"
)
# 添加包含路径
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
# 添加用户定义的包含路径
# e.g., "include"
)
# 添加项目符号(宏)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
# 添加用户定义的符号
# e.g., "MY_MACRO=1"
)
# 添加链接库
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx # 链接stm32cubemx库 实际上也是以project()项目的形式存在,此前的add_subdirectory(cmake/stm32cubemx)引入了这名为stm32cubemx的库,该目录下的CMakeLists.txt文件中定义了这个库
# 添加用户定义的库
# e.g., "mylib"
)
值得注意的是,这个 CMakeLists.txt 没有直接地引入任何源码,而是仅定义了一些配置。这在后续修改可执行主构建的 CMakeLists.txt 时应当保持。
他有
add_subdirectory(cmake/stm32cubemx)
......
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx
)
这两个部分会使用间接方式引入所有的源码文件,在cmake/stm32cubemx
中另有一个CMakeLists.txt
,其中定义了名为stm32cubemx
的工程并且引入了所有的包含路径和源码文件,使用target_link_libraries的方式进行间接引入,后续会详细介绍该文件。
工具链指定
可执行主构建中有这样一句话
include("cmake/gcc-arm-none-eabi.cmake")
这句话将./cmake
文件夹下的gcc-arm-none-eabi.cmake
文件引入了,这里的 include 和 C 语言一样,只是进行文本替换,也就是将 gcc-arm-none-eabi.cmake 文件的所有内容都替换到了这一行,从而达到指定工具链的目的。这个文件内容如下。
# 设置系统名称和处理器架构
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm) # 设置处理器架构为arm
# 强制指定编译器
set(CMAKE_C_COMPILER_FORCED TRUE) # 强制指定C编译器
set(CMAKE_CXX_COMPILER_FORCED TRUE) # 强制指定C++编译器
set(CMAKE_C_COMPILER_ID GNU) # 设置C编译器ID为GNU
set(CMAKE_CXX_COMPILER_ID GNU) # 设置C++编译器ID为GNU
# 一些默认的GCC设置,要求arm-none-eabi-xx必须在PATH环境变量中
set(TOOLCHAIN_PREFIX arm-none-eabi-) # 设置工具链前缀为arm-none-eabi-
# 设置各个工具的路径和名称
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) # 设置C编译器
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) # 设置汇编编译器,使用C编译器
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) # 设置C++编译器
set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}g++) # 设置链接器
set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) # 设置对象复制工具
set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) # 设置大小计算工具
# 设置生成的可执行文件的后缀
set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf") # 设置汇编可执行文件后缀为.elf
set(CMAKE_EXECUTABLE_SUFFIX_C ".elf") # 设置C可执行文件后缀为.elf
set(CMAKE_EXECUTABLE_SUFFIX_CXX ".elf") # 设置C++可执行文件后缀为.elf
# 设置尝试编译的目标类型为静态库
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # 设置尝试编译的目标类型为静态库
# MCU-specific 编译标志
set(TARGET_FLAGS "-mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard ") # 设置目标平台的特定编译标志
# 设置C编译标志
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS}") # 添加目标平台特定的编译标志到C编译器标志(基于原有标志添加)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic -fdata-sections -ffunction-sections") # 添加更多编译器标志
# 根据构建类型设置不同的优化级别
if(CMAKE_BUILD_TYPE MATCHES Debug)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g3") # 如果是Debug构建类型,设置为O0无优化并g3生成调试信息
endif()
if(CMAKE_BUILD_TYPE MATCHES Release)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -g0") # 如果是Release构建类型,设置为O3优化并g0不生成调试信息
endif()
# 设置汇编编译标志
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MMD -MP") # 设置汇编编译标志
# 设置C++编译标志
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe-statics") # 添加C++编译标志,禁用RTTI、异常和线程安全静态变量
# 设置C链接器标志
set(CMAKE_C_LINK_FLAGS "${TARGET_FLAGS}") # 添加目标平台特定的编译标志到链接器标志
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32H743IITx_FLASH.ld\"") # 添加链接脚本
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --specs=nano.specs") # 使用nano.specs配置
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections") # 生成映射文件并移除未使用的部分
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lc -lm -Wl,--end-group") # 链接C库和数学库
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--print-memory-usage") # 打印内存使用情况s's
# 设置C++链接器标志
set(CMAKE_CXX_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lstdc++ -lsupc++ -Wl,--end-group") # 添加C++特定的链接标志,链接标准C++库
可以看到,就是制定了编译工具链,并且将一些编译器参数在这里一一指定,例如在汇编器中加入传参“ -masm=auto”,就可以在
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MMD -MP") # 设置汇编编译标志
这一句后加入
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -masm=auto") # 设置汇编器自动识别汇编语法
其中的${CMAKE_ASM_FLAGS}
意味着将此前设定的CMAKE_ASM_FLAGS
通通填在前面,实现额外添加的效果。
工程源码引入
前两个相关文件中,我们指定了 cmake 生成配置,工具链参数配置,现在要引入事实上的源码文件。
可执行主构建中有这样一句话
add_subdirectory(cmake/stm32cubemx)
将./cmake/stm32cubemx
全部引入,其中事实上有效的只有CMakeLists.txt
,其他文件都没有实际生效,不用理会。这个文件内容如下。
# 设置CMake的最低版本要求
cmake_minimum_required(VERSION 3.22)
# 定义项目名称为stm32cubemx
project(stm32cubemx)
# 添加一个INTERFACE库,INTERFACE库不生成实际编译产物,只提供编译选项给依赖它的目标
add_library(stm32cubemx INTERFACE)
# 启用C和汇编语言支持
enable_language(C ASM)
# 为stm32cubemx目标添加编译定义
target_compile_definitions(stm32cubemx INTERFACE
USE_HAL_DRIVER # 定义USE_HAL_DRIVER宏
STM32H743xx # 定义STM32H743xx宏
$<$<CONFIG:Debug>:DEBUG> # 如果是Debug配置,定义DEBUG宏。这里的$<CONFIG:Debug>比较迷惑,实际上CONFIG对应的就是CMAKE_BUILD_TYPE属性
)
# 为stm32cubemx目标添加包含目录
target_include_directories(stm32cubemx INTERFACE
../../Core/Inc
../../Drivers/STM32H7xx_HAL_Driver/Inc
../../Drivers/STM32H7xx_HAL_Driver/Inc/Legacy
../../Drivers/CMSIS/Device/ST/STM32H7xx/Include
../../Drivers/CMSIS/Include
)
# 为stm32cubemx目标添加源文件
target_sources(stm32cubemx INTERFACE
../../Core/Src/main.c
../../Core/Src/gpio.c
../../Core/Src/stm32h7xx_it.c
../../Core/Src/stm32h7xx_hal_msp.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_cortex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_rcc.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_rcc_ex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_flash.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_flash_ex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_gpio.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_hsem.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_dma.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_dma_ex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_mdma.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_pwr.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_pwr_ex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_i2c.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_i2c_ex.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_exti.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_tim.c
../../Drivers/STM32H7xx_HAL_Driver/Src/stm32h7xx_hal_tim_ex.c
../../Core/Src/system_stm32h7xx.c
../../Core/Src/sysmem.c
../../Core/Src/syscalls.c
../../startup_stm32h743xx.s
)
# 为stm32cubemx目标添加链接目录
target_link_directories(stm32cubemx INTERFACE
)
# 为stm32cubemx目标添加链接库
target_link_libraries(stm32cubemx INTERFACE
)
# 验证STM32CubeMX生成的代码是否兼容C标准,如果低于C11则报错
if(CMAKE_C_STANDARD LESS 11)
message(ERROR "Generated code requires C11 or higher") # 由于hal库有诸多可覆盖定义的函数,必须C11标准支持
endif()
那么好,这里就是引入工程实际源码文件和包含目录的地方了,以stm32cubemx
为 cmake 项目名。还记得主构建中有以下的部分吗?
add_subdirectory(cmake/stm32cubemx)
......
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx
)
stm32cubemx
这个 cmake 项目中实现与源码相关的引入。
vscode 配置 cmake
1.配置文件
在以下站点下载 cmake 配置文件,并在 vscode 中导入
2.修改用户配置
导入配置文件后会下载诸多所需的插件,要修改用户设置,将各个工具链的路径添加到合适的设置项。
启用上一步导入的配置文件,在设置中 (用户分栏) 搜索 arm toolchain path
有类似如下的项。
Cortex-debug: Arm Toolchain Path
Path to the GCC Arm Toolchain (standard prefix is "arm-none-eabi" - can be set through the armToolchainPrefix setting) to use. If not set the tools must be on the system path. Do not include the executable file name in this path.
点击此项下方的 在 settings.json 中编辑
,编辑这个条目类似如下,将其中的路径改为自己 arm-none-eabi-gcc 的 bin 路径。
"cortex-debug.armToolchainPath": "C:\\111_APPS\\arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi\\bin",
在设置中 (用户分栏) 搜索 JLink GDBServer Path
有类似如下的项。
Cortex-debug: JLink GDBServer Path
Path to the JLink GDB Server. If not set then JLinkGDBServer (JLinkGDBServerCL.exe on Windows) must be on the system path.
点击此项下方的 在 settings.json 中编辑
,编辑这个条目类似如下,将其中的路径改为自己 JLinkGDBServerCL.exe 的 bin 路径。由于我给出的例子通常都是编写的 JLink 调试任务,并且利用 JLink 的 RTT 打印功能,所以使用 JLinkGDBServer ,如果使用 OpenGDBServer ,自行修改对应的 GDBServerPath,只是恐怕编写调试任务有些困难。
"cortex-debug.JLinkGDBServerPath": "C://111_APPS//SEGGER//JLink_V794f//JLinkGDBServerCL.exe",
3.编写 vscode 任务
需要进行 CMAKE 脚本以及程序的调试,需要自行编写 debug 任务,在 .workspace 文件的 "launch"
部分编写如下,如果没有 "launch"
部分可以写在最下方。
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "CMake: Script debugging",
"type": "cmake",
"request": "launch",
"cmakeDebugType": "configure"
},
{
"cwd": "${workspaceRoot}",
"executable": "./build/H7_GCC_BASE.elf",
"name": "Debug with JLink",
"request": "launch",
"type": "cortex-debug",
"device": "STM32H743II",
// "runToEntryPoint": "Reset_Handler",
"runToEntryPoint": "main",
"showDevDebugOutput": "none",
"servertype": "jlink",
"interface": "swd",
"svdFile": "../../src/5_PhysicalChip/CPU/STM32H743.svd",
"liveWatch": {
"enabled": true,
"samplesPerSecond": 4
},
"rttConfig": {
"enabled": true,
"address": "auto",
"decoders": [
{
"label": "",
"port": 0,
"type": "console"
}
]
},
}
]
}
其中 "executable"
部分填写为实际的编译产出,"device"
填写为真正的芯片型号,"svdFile"
部分填写真正的 svd 文件所在(如果没有,删除这个条目)。
共创建了两个任务,分别调试 CMAKE 生成脚本,以及程序,在 vscode 调试窗选择对应的任务开始即可。
总结
stm32cubemx 生成的 cmake 项目,个人觉得非常合理非常易懂,分为三个部分清晰明了。
- 可执行主构建:定义 cmake 项目的各种通用配置,比如 C 标准、是否使用 C 艹等等编译器、源码无关的事情,并引入另外两个部分。
- 编译工具链的指定:定义使用什么编译器,依据目标平台不同而不同
- 源码的引入:这一层就类似使用其他 IDE 了,仅需要将 源码、包含目录、全局 define 一一定义
之所以想要换成 cmake,是因为工程越来越庞大,而且想要将各个软件功能都模块化抽离管理,IDE 在分软件包这方面还是差一些,cmake 每个功能模块独立自己的 CmakeList 就好,除了硬件驱动都可以抽象化使其平台无关。
拓展
我的 modbus 协议栈展示了一种功能模块的先进用法。
功能库的 CmakeList.txt 如下
cmake_minimum_required(VERSION 3.22)
project(MODBUSX)
add_library(modbusx INTERFACE) # INTERFACE意味着这个库本身并不会被编译,而是作为依赖被其他目标使用,以便于MBx_user.h可以自己定义并且对库行为进行变更
# 递归查找所有源码文件
file(GLOB_RECURSE SRC ${CMAKE_CURRENT_LIST_DIR}/source/*.c)
# 非递归的案例
# file(GLOB SRC ${CMAKE_CURRENT_LIST_DIR}/source/*.c)
target_include_directories(modbusx INTERFACE
${CMAKE_CURRENT_LIST_DIR}/include
${CMAKE_CURRENT_LIST_DIR}/../port/generic/inc
)
target_sources(modbusx INTERFACE
${SRC}
)
if(CMAKE_C_STANDARD LESS 11)
message(ERROR "Generated code requires C11 or higher")
endif()
而主构建中使用这样的方法调用
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/common) # 添加子目录
# 设置要包含和定义的参数
set(MY_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/Example/win_test)
set(MY_DEFINITIONS MBX_INCLUDE_USER_DEFINE_FILE)
# 配置目标和链接属性
function(configure_target target_name source_file)
add_executable(${target_name} ${source_file})
target_compile_definitions(${target_name} INTERFACE ${MY_DEFINITIONS})
target_compile_definitions(${target_name} PRIVATE ${MY_DEFINITIONS})
target_include_directories(${target_name} INTERFACE ${MY_INCLUDE_DIR})
target_include_directories(${target_name} PRIVATE ${MY_INCLUDE_DIR})
target_link_libraries(${target_name} PRIVATE modbusx)
# 添加链接器选项
target_link_options(${target_name} PRIVATE
-Wl,-Map=${target_name}.map
-Wl,--gc-sections
)
endfunction()
# 配置每个可执行文件
configure_target(RTU_Mmain ${CMAKE_CURRENT_LIST_DIR}/Example/win_test/RTU_Mmain.c)
首先是子构建是 INTERFACE 库,因为是一个功能库,不可能独立运行,而是需要实际的应用软件调用这个功能库。
示例主构建就是展示如何使用功能库,这里分别使用 INTERFACE 以及 PRIVATE 属性进行 define 和 include path 了两次。 其中 INTERFACE 标签在这里代表在子构建中生效,而在这个主构建中不生效。 而 PRIVATE 标签在这里代表只在主构建中生效,而在子构建中不生效。
所以事实上是在两个 CmakeList.txt 中都生效了,可以使用 PUBLIC 这个标签属性来代表两个中都生效,示例进行了精细的控制。