Where to Start#
First, there is such a tutorial, which is very "beginner to beginner".👌
Cmake tutorial in Chinese, CMake教程中文翻译
If you want to fully understand the mechanisms and syntax of CMAKE, you can follow this tutorial all the way through. However, the tutorial has the following issues:
[!NOTE]
- It is only for learning and understanding, not suitable for production environments. The tutorial is based on a learning environment, using C++, and the default C compiler (in my environment, it is clang), and the compiled output is an exe that can only run on the desktop. We certainly need to use arm_gcc for compilation, or at least know how to specify other gcc tools to use the project in production.
- The project structure is overly complex, which is not conducive to beginners' understanding. Since it cannot be used in production, the entire software is too complex, with too many cmakelist files, which is not beginner-friendly.
So you can just look at Steps 1 to 3 of the tutorial.
The software tools involved need to be downloaded by yourself and the executable file paths added to the environment variables:
- Cmake
- MinGW64
- arm-none-eabi-gcc
Or use my packaged portable vscode environment from my file station.
Then use stm32cubemx to randomly generate a debuglink project, selecting CMAKE as the toolchain for generating the project. Then analyze the project it generates.
Download Software Tools#
It is recommended to install Cmake and MinGW64 using the MINGW64 branch environment of MSYS2, as manual installation may lead to dependency issues that make it difficult to proceed, or dependencies being scattered, which is not conducive to modularization.
After installing MSYS2, open mingw64.exe and run the following two commands:
pacman -S mingw-w64-x86_64-make
pacman -S mingw-w64-x86_64-cmake
After installation, add the path of the MSYS2 mingw64/bin folder to the system environment variable path.
arm-none-eabi-gcc needs to be downloaded from the official website, as the version downloaded in MSYS2 is incomplete and will miss the GDB tool.
Download from Arm GNU Toolchain Downloads – Arm Developer or from my file mirror arm-gcc.
After extracting or installing, make sure to add the path of the bin folder to the system environment variable path.
A Very Simple Project#
In stm32cubemx, randomly generate a simple cmake project, with a structure similar to the following. The parts related to cmake are shown in the figure.
Executable Main Build#
The generated main build file is CMakeLists.txt
in the root directory, which is also the CMakeLists.txt
called by commands like cmake ../
to generate the native build system.
# Specify the minimum required version of CMake as 3.22
cmake_minimum_required(VERSION 3.22)
#
# This file is the main build file called by cmake
# Users can freely modify this file as needed.
#
# Set compiler settings
set(CMAKE_C_STANDARD 11) # Set C standard to C11
set(CMAKE_C_STANDARD_REQUIRED ON) # Require the specified C standard
set(CMAKE_C_EXTENSIONS ON) # Enable compiler extensions
# Define build type
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Debug") # If CMAKE_BUILD_TYPE is not set, default to "Debug". This parameter can be specified when generating the native build system using a command like "cmake ../" with -DCMAKE_BUILD_TYPE=Release
endif()
# Set project name
set(CMAKE_PROJECT_NAME DebugBuild) # Set project name to DebugBuild
# Include toolchain file
include("cmake/gcc-arm-none-eabi.cmake")
# Enable compile command generation for indexing by other tools like clangd
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) # Generate compile_commands.json for IDE or tools to use
# Enable CMake support for ASM and C languages
enable_language(C ASM) # Enable support for C and assembly (ASM) languages
# Core project settings
project(${CMAKE_PROJECT_NAME}) # Define project using the previously set project name
message("Build type: " ${CMAKE_BUILD_TYPE}) # Output message for build type
# Create an executable target
add_executable(${CMAKE_PROJECT_NAME}) # Define an executable target using the project name
# Add subdirectory, which will automatically handle CMakeLists.txt files in subdirectories
add_subdirectory(cmake/stm32cubemx) # Add subdirectory, typically containing code generated by STM32CubeMX
# Link directory settings
target_link_directories(${CMAKE_PROJECT_NAME} PRIVATE
# Add user-defined library search paths
# e.g., "/path/to/libs"
)
# Add source files to the executable target
target_sources(${CMAKE_PROJECT_NAME} PRIVATE
# Add additional source files
# e.g., "src/main.c"
)
# Add include paths
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
# Add user-defined include paths
# e.g., "include"
)
# Add project symbols (macros)
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
# Add user-defined symbols
# e.g., "MY_MACRO=1"
)
# Add linked libraries
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx # Link to the stm32cubemx library, which actually exists as a project in the form of project(). The previous add_subdirectory(cmake/stm32cubemx) introduced this library named stm32cubemx, defined in the CMakeLists.txt file in that directory.
# Add user-defined libraries
# e.g., "mylib"
)
It is worth noting that this CMakeLists.txt does not directly include any source code but only defines some configurations. This should be maintained when modifying the CMakeLists.txt for the executable main build later.
It has
add_subdirectory(cmake/stm32cubemx)
......
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx
)
These two parts will indirectly include all the source files, as there is another CMakeLists.txt
in cmake/stm32cubemx
, which defines a project named stm32cubemx
and includes all the include paths and source files, using target_link_libraries for indirect inclusion, which will be detailed later.
Toolchain Specification#
In the executable main build, there is a line:
include("cmake/gcc-arm-none-eabi.cmake")
This line includes the gcc-arm-none-eabi.cmake
file in the ./cmake
folder. The include here is similar to C language, just performing text replacement, meaning all the contents of the gcc-arm-none-eabi.cmake file are replaced at this line, thus achieving the purpose of specifying the toolchain. The content of this file is as follows:
# Set system name and processor architecture
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm) # Set processor architecture to arm
# Force specify compiler
set(CMAKE_C_COMPILER_FORCED TRUE) # Force specify C compiler
set(CMAKE_CXX_COMPILER_FORCED TRUE) # Force specify C++ compiler
set(CMAKE_C_COMPILER_ID GNU) # Set C compiler ID to GNU
set(CMAKE_CXX_COMPILER_ID GNU) # Set C++ compiler ID to GNU
# Some default GCC settings, requiring arm-none-eabi-xx to be in the PATH environment variable
set(TOOLCHAIN_PREFIX arm-none-eabi-) # Set toolchain prefix to arm-none-eabi-
# Set paths and names for various tools
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc) # Set C compiler
set(CMAKE_ASM_COMPILER ${CMAKE_C_COMPILER}) # Set assembly compiler to use C compiler
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++) # Set C++ compiler
set(CMAKE_LINKER ${TOOLCHAIN_PREFIX}g++) # Set linker
set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy) # Set object copy tool
set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size) # Set size calculation tool
# Set suffix for generated executable files
set(CMAKE_EXECUTABLE_SUFFIX_ASM ".elf") # Set suffix for assembly executable files to .elf
set(CMAKE_EXECUTABLE_SUFFIX_C ".elf") # Set suffix for C executable files to .elf
set(CMAKE_EXECUTABLE_SUFFIX_CXX ".elf") # Set suffix for C++ executable files to .elf
# Set the target type for try compile to static library
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY) # Set the target type for try compile to static library
# MCU-specific compile flags
set(TARGET_FLAGS "-mcpu=cortex-m7 -mfpu=fpv5-d16 -mfloat-abi=hard ") # Set target platform specific compile flags
# Set C compile flags
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${TARGET_FLAGS}") # Add target platform specific compile flags to C compiler flags (based on existing flags)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic -fdata-sections -ffunction-sections") # Add more compiler flags
# Set different optimization levels based on build type
if(CMAKE_BUILD_TYPE MATCHES Debug)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0 -g3") # If Debug build type, set to O0 with no optimization and g3 for generating debug information
endif()
if(CMAKE_BUILD_TYPE MATCHES Release)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -g0") # If Release build type, set to O3 optimization and g0 with no debug information
endif()
# Set assembly compile flags
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MMD -MP") # Set assembly compile flags
# Set C++ compile flags
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS} -fno-rtti -fno-exceptions -fno-threadsafe-statics") # Add C++ compile flags, disabling RTTI, exceptions, and thread-safe static variables
# Set C linker flags
set(CMAKE_C_LINK_FLAGS "${TARGET_FLAGS}") # Add target platform specific compile flags to linker flags
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/STM32H743IITx_FLASH.ld\"") # Add linker script
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} --specs=nano.specs") # Use nano.specs configuration
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,-Map=${CMAKE_PROJECT_NAME}.map -Wl,--gc-sections") # Generate map file and remove unused sections
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lc -lm -Wl,--end-group") # Link C library and math library
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--print-memory-usage") # Print memory usage
# Set C++ linker flags
set(CMAKE_CXX_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -Wl,--start-group -lstdc++ -lsupc++ -Wl,--end-group") # Add C++ specific linker flags, linking standard C++ library
As you can see, it specifies the compilation toolchain and lists some compiler parameters here. For example, to add the parameter “-masm=auto” in the assembler, you can add it after:
set(CMAKE_ASM_FLAGS "${CMAKE_C_FLAGS} -x assembler-with-cpp -MMD -MP") # Set assembly compile flags
set(CMAKE_ASM_FLAGS "${CMAKE_ASM_FLAGS} -masm=auto") # Set assembler to automatically recognize assembly syntax
Here, ${CMAKE_ASM_FLAGS}
means to fill in all the previously set CMAKE_ASM_FLAGS
, achieving the effect of additional addition.
Source Code Inclusion#
In the first two related files, we specified the cmake generation configuration and toolchain parameter configuration, now we need to include the actual source code files.
In the executable main build, there is a line:
add_subdirectory(cmake/stm32cubemx)
This includes everything from ./cmake/stm32cubemx
, where only the CMakeLists.txt
is effective, and the other files do not have any actual effect and can be ignored. The content of this file is as follows:
# Set the minimum required version of CMake
cmake_minimum_required(VERSION 3.22)
# Define the project name as stm32cubemx
project(stm32cubemx)
# Add an INTERFACE library, INTERFACE libraries do not generate actual compilation products, but provide compilation options to targets that depend on them
add_library(stm32cubemx INTERFACE)
# Enable support for C and assembly languages
enable_language(C ASM)
# Add compile definitions for the stm32cubemx target
target_compile_definitions(stm32cubemx INTERFACE
USE_HAL_DRIVER # Define USE_HAL_DRIVER macro
STM32H743xx # Define STM32H743xx macro
$<$<CONFIG:Debug>:DEBUG> # If Debug configuration, define DEBUG macro. Here, $<CONFIG:Debug> can be confusing, as CONFIG corresponds to the CMAKE_BUILD_TYPE property
)
# Add include directories for the stm32cubemx target
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
)
# Add source files for the stm32cubemx target
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
)
# Add link directories for the stm32cubemx target
target_link_directories(stm32cubemx INTERFACE
)
# Add linked libraries for the stm32cubemx target
target_link_libraries(stm32cubemx INTERFACE
)
# Verify that the STM32CubeMX generated code is compatible with the C standard, and report an error if it is below C11
if(CMAKE_C_STANDARD LESS 11)
message(ERROR "Generated code requires C11 or higher") # Due to the HAL library having many overridable functions, C11 standard support is required
endif()
So here is where the actual source code files and include directories are introduced, with stm32cubemx
as the cmake project name. Do you remember the following part in the main build?
add_subdirectory(cmake/stm32cubemx)
......
target_link_libraries(${CMAKE_PROJECT_NAME}
stm32cubemx
)
The stm32cubemx
cmake project implements the inclusion of source code related to it.
vscode Configuration for CMake#
1. Configuration File#
Download the cmake configuration file from the following site and import it into vscode.
2. Modify User Configuration#
After importing the configuration file, many required plugins will be downloaded. You need to modify the user settings to add the paths of various toolchains to the appropriate settings.
Enable the configuration file imported in the previous step, and in the settings (User section) search for arm toolchain path
, which will have similar items.
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.
Click on Edit in settings.json
below this item, and edit this entry to change the path to your arm-none-eabi-gcc bin path.
"cortex-debug.armToolchainPath": "C:\\111_APPS\\arm-gnu-toolchain-13.2.Rel1-mingw-w64-i686-arm-none-eabi\\bin",
In the settings (User section) search for JLink GDBServer Path
, which will have similar items.
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.
Click on Edit in settings.json
below this item, and edit this entry to change the path to your JLinkGDBServerCL.exe bin path. Since the examples I provided usually involve writing JLink debugging tasks and utilizing JLink's RTT printing function, I use JLinkGDBServer. If you use OpenGDBServer, modify the corresponding GDBServerPath, but writing debugging tasks may be a bit challenging.
"cortex-debug.JLinkGDBServerPath": "C://111_APPS//SEGGER//JLink_V794f//JLinkGDBServerCL.exe",
3. Write vscode Tasks#
To debug CMAKE scripts and programs, you need to write debug tasks yourself. In the .workspace
file's "launch"
section, write as follows. If there is no "launch"
section, you can write it at the bottom.
"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"
}
]
},
}
]
}
In the "executable"
section, fill in the actual compilation output, and in the "device"
section, fill in the actual chip model. The "svdFile"
section should contain the actual location of the svd file (if not available, delete this entry).
Two tasks have been created: one for debugging the CMAKE generated script and another for the program. You can start the corresponding task in the vscode debug window.
Summary#
The cmake project generated by stm32cubemx is, in my opinion, very reasonable and easy to understand, divided into three clear parts.
- Executable Main Build: Defines various general configurations for the cmake project, such as C standard, whether to use C++, etc., which are unrelated to the source code, and introduces the other two parts.
- Specification of Compilation Toolchain: Defines which compiler to use, which varies depending on the target platform.
- Source Code Inclusion: This layer is similar to using other IDEs, where you only need to define the source code, include directories, and global defines.
The reason for wanting to switch to cmake is that the project is becoming larger and I want to modularize and manage various software functions. IDEs are still lacking in this aspect of separating software packages, while cmake allows each functional module to have its own CMakeLists, making it platform-independent except for hardware drivers.
Expansion#
My modbus protocol stack demonstrates an advanced usage of functional modules.
The CmakeList.txt for the functional library is as follows:
cmake_minimum_required(VERSION 3.22)
project(MODBUSX)
add_library(modbusx INTERFACE) # INTERFACE means this library itself will not be compiled, but will be used as a dependency by other targets, allowing MBx_user.h to be defined by itself and change the library behavior.
# Recursively find all source files
file(GLOB_RECURSE SRC ${CMAKE_CURRENT_LIST_DIR}/source/*.c)
# Non-recursive example
# 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()
And the main build calls it like this:
add_subdirectory(${CMAKE_CURRENT_LIST_DIR}/common) # Add subdirectory
# Set parameters to include and define
set(MY_INCLUDE_DIR ${CMAKE_CURRENT_LIST_DIR}/Example/win_test)
set(MY_DEFINITIONS MBX_INCLUDE_USER_DEFINE_FILE)
# Configure target and link properties
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)
# Add linker options
target_link_options(${target_name} PRIVATE
-Wl,-Map=${target_name}.map
-Wl,--gc-sections
)
endfunction()
# Configure each executable
configure_target(RTU_Mmain ${CMAKE_CURRENT_LIST_DIR}/Example/win_test/RTU_Mmain.c)
First, the sub-build is an INTERFACE library because it is a functional library that cannot run independently but needs to be called by actual application software.
The example main build demonstrates how to use the functional library, where both ==INTERFACE== and ==PRIVATE== properties are used for defines and include paths twice. The ==INTERFACE== tag here means it is effective in the sub-build but not in the main build. The ==PRIVATE== tag means it is only effective in the main build and not in the sub-build.
So in fact, it is effective in both CmakeLists.txt files, and you can use the ==PUBLIC== tag to indicate that it is effective in both. The example demonstrates fine control.