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交叉编译流程,每次带新人我都会画一遍。

CMake交叉编译构建流程 CMakeLists.txt Toolchain File CMake Presets cmake -S . -B build --preset=arm-debug 生成 Makefile / Ninja 构建文件 cmake --build build/arm-debug ota_app.elf / .bin / .hex 输入文件 配置命令 生成阶段 编译阶段 输出产物

整个流程其实就三步:配置、生成、编译。但每一步里都有不少细节,尤其是工具链文件的配置,稍微写错一个路径,编译出来的东西可能根本跑不起来。

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把这些碎片化的配置串起来,形成一套可复用的构建流水线。