banner
yono

yono

哈喽~欢迎光临
follow
github

CMAKE 扫盲

從何下手#

首先有這樣的教程,非常的 “從入門到入門”。👌

如果想要完整地了解一下 CMAKE 的機制和語法,可以跟著這個教程走完全程。但是教程有如下的問題

[!NOTE]

  1. ** 僅僅是學習和了解,無法用於生產環境。** 教程是基於學習環境,使用的 C++,以及默認的 C 編譯器 (在我的環境中是 clang),編譯出的也是僅可在桌面運行的 exe 。我們當然是使用 arm_gcc 進行編譯,或者至少要會指定其他的 gcc 工具,才可以將工程用於生產。
  2. ** 項目結構過分複雜,不利於初學理解。** 本來就不能用於生產了,整個軟件又太複雜,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 有關的如圖。

根文件夾

./cmake

./cmake/stm32cubemx

可執行主構建#

生成的主構建文件是根目錄下的 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 中導入

CMAKE 配置

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== 這個標籤屬性來代表兩個中都生效,示例進行了精細的控制。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。