4. 构建工具CMake:CMake安装、基本语法、交叉编译配置
说到OTA升级包的构建,CMake是我绕不开的老伙计。说实话,早期我还在用纯Makefile写嵌入式项目,每次换平台都要重写一套编译规则,那叫一个痛苦。后来全面转向CMake,才算是把构建这件事理顺了。
CMake不是编译器,它是个构建系统生成器。你写一份CMakeLists.txt,它能给你生成Makefile、Ninja文件,甚至Visual Studio工程。对于嵌入式OTA这种多平台、多架构的场景,CMake几乎是标配。
4.1 CMake的安装
安装CMake其实很简单,但不同平台有点小区别。我个人习惯用包管理器装,干净利落。
| 平台 | 安装命令 | 备注 |
|---|---|---|
| Ubuntu/Debian | sudo apt install cmake |
版本可能偏旧,建议加PPA |
| macOS | brew install cmake |
Homebrew版本较新 |
| Windows | 下载安装包或choco install cmake |
记得勾选添加到PATH |
| 源码编译 | ./bootstrap && make && sudo make install |
适合定制化需求 |
cmake --version 确认版本。OTA项目建议用3.16以上版本,对工具链文件的支持更完善。
我曾经在Ubuntu 18.04上直接用apt装了个3.10版,结果交叉编译时死活找不到某个变量。折腾了半天,最后升级到3.20才解决。嗯,版本这事别将就。
4.2 CMake基本语法
CMake的语法其实不复杂,核心就几条命令。你想想看,它本质上就是个声明式的配置语言。
4.2.1 最小CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(ota_upgrade VERSION 1.0.0 LANGUAGES C CXX)
add_executable(ota_app main.c utils.c)
target_include_directories(ota_app PRIVATE include)
target_link_libraries(ota_app PRIVATE m)
这几行干了什么?
- cmake_minimum_required:声明最低版本,防止旧版特性缺失
- project:定义项目名、版本、语言
- add_executable:告诉CMake要生成一个可执行文件
- target_include_directories:指定头文件路径
- target_link_libraries:链接需要的库
关键点: 现代CMake推荐用target_*系列命令,而不是老式的include_directories和link_libraries。前者作用域更精确,不会污染其他目标。
4.2.2 变量与条件判断
写OTA升级包,经常要根据不同硬件平台做条件编译。CMake的变量系统这时候就派上用场了。
set(MCU_MODEL "STM32F407")
set(OTA_PARTITION_SIZE 0x100000)
if(MCU_MODEL STREQUAL "STM32F407")
add_definitions(-DSTM32F407)
set(FLASH_SIZE 0x200000)
elseif(MCU_MODEL STREQUAL "ESP32")
add_definitions(-DESP32)
set(FLASH_SIZE 0x400000)
else()
message(FATAL_ERROR "Unsupported MCU: ${MCU_MODEL}")
endif()
这里有个坑我踩过——set()命令默认设置的是局部变量。如果你想跨目录共享变量,得用set(VAR value CACHE STRING "description")把它变成缓存变量。
4.2.3 函数与宏
OTA项目中经常要重复创建多个组件的编译目标。写个函数封装一下,省事多了。
function(add_ota_component COMP_NAME)
add_library(${COMP_NAME} STATIC
src/${COMP_NAME}/${COMP_NAME}.c
src/${COMP_NAME}/ota_hal.c
)
target_include_directories(${COMP_NAME} PUBLIC
include/${COMP_NAME}
)
endfunction()
add_ota_component(flash_driver)
add_ota_component(crypto_engine)
add_ota_component(update_manager)
我个人习惯把这类通用函数放到 cmake/ 目录下的 .cmake 文件中,然后用 include() 引入。这样主CMakeLists.txt看起来清爽很多。
4.3 交叉编译配置
这才是本章的重头戏。做OTA升级包,你大概率是在x86的PC上编译,但目标板是ARM、RISC-V或者Xtensa架构。交叉编译就是干这个的。
4.3.1 工具链文件(Toolchain File)
CMake通过工具链文件来切换编译环境。说白了,就是告诉CMake:别用你本机的gcc,用这个交叉编译器。
# toolchain_arm_gcc.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(TOOLCHAIN_PREFIX arm-none-eabi-)
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}g++)
set(CMAKE_ASM_COMPILER ${TOOLCHAIN_PREFIX}gcc)
set(CMAKE_OBJCOPY ${TOOLCHAIN_PREFIX}objcopy)
set(CMAKE_SIZE ${TOOLCHAIN_PREFIX}size)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
CMAKE_FIND_ROOT_PATH_MODE_* 这四行非常关键。它们控制CMake在查找程序、库、头文件时,是去宿主系统找还是去目标系统找。设错了,你可能会链接到PC上的glibc,而不是目标板的newlib。
使用工具链文件编译:
mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain_arm_gcc.cmake
make
4.3.2 编译选项与链接脚本
嵌入式交叉编译,光指定编译器还不够。你还要告诉编译器目标CPU的架构、浮点单元、优化等级,以及链接时用哪个链接脚本。
# 在工具链文件中继续添加
set(MCU_FLAGS "-mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MCU_FLAGS} -Os -ffunction-sections -fdata-sections")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MCU_FLAGS} -Os -ffunction-sections -fdata-sections")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--gc-sections -T${CMAKE_SOURCE_DIR}/linker/stm32f407.ld")
我曾经接手过一个OTA项目,前任把链接脚本路径写死了,换了块Flash更大的芯片后,链接脚本没更新,结果升级包烧进去直接跑飞。嗯,链接脚本这事,一定要做成可配置的。
4.3.3 多架构构建
OTA项目经常要同时维护多个硬件版本。我的做法是用CMake的预设(Presets)来管理不同配置。
# CMakePresets.json
{
"version": 3,
"configurePresets": [
{
"name": "arm-debug",
"displayName": "ARM Debug",
"toolchainFile": "toolchain_arm_gcc.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"OTA_TARGET": "stm32f407"
}
},
{
"name": "esp32-release",
"displayName": "ESP32 Release",
"toolchainFile": "toolchain_xtensa_gcc.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"OTA_TARGET": "esp32"
}
}
]
}
用预设编译,命令简洁多了:
cmake --preset=arm-debug
cmake --build build/arm-debug
4.4 CMake构建流程总览
下面这张图是我自己总结的CMake交叉编译流程,每次带新人我都会画一遍。
整个流程其实就三步:配置、生成、编译。但每一步里都有不少细节,尤其是工具链文件的配置,稍微写错一个路径,编译出来的东西可能根本跑不起来。
4.5 避坑指南
最后分享几个我实际踩过的坑:
- 路径中的空格:CMake对路径中的空格处理很诡异。我曾经把工具链装到
Program Files (x86)下,结果各种报错。后来统一用C:/tools/这种无空格路径,世界清净了。 - 缓存污染:切换工具链时,一定要删掉
build/CMakeCache.txt或者直接删整个build目录。CMake会缓存第一次配置的结果,不清理的话,新工具链可能不会生效。 - 链接顺序:CMake的
target_link_libraries是有顺序依赖的。如果库A依赖库B,你得写成target_link_libraries(A PRIVATE B)。顺序反了,链接阶段会报未定义引用。
总结一下: CMake本身不复杂,但交叉编译的坑都在细节里。工具链文件写对、缓存清干净、链接顺序理清楚,基本就稳了。OTA升级包的构建环境,说白了就是靠CMake把这些碎片化的配置串起来,形成一套可复用的构建流水线。