ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CMake 入门与嵌入式迁移:从环境配置到构建烧录全解析

CMake 入门与嵌入式迁移:从环境配置到构建烧录全解析 1. 从那一行红色报错说起cmake 到底是什么东西第一次在 Windows 的 PowerShell 里敲下cmake这四个字母回给你的大概率是这么一行红字cmake : 无法将“cmake”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。这句话看着像在骂人其实它只是很直白地告诉你系统在当前 PATH 里翻遍了所有目录没找到一个叫 cmake 的可执行程序。这个报错几乎是我见过的新手第一坑没有之一。它跟 CMake 本身难不难完全没关系纯粹是环境没配对。所以在讲怎么写 CMakeLists.txt 之前得先把“cmake 是个啥”说清楚不然很多人连自己在装什么都没搞明白。CMake 不是一个编译器它不负责把 .c 或者 .cpp 变成机器码。它干的事情更像一个工程文件翻译官你写一份跨平台的描述文件CMakeLists.txt告诉它“我这个项目有哪些源文件、依赖哪些库、要生成什么产物”然后它根据你当前的操作系统和你指定要用的编译器生成对应的底层构建文件。在 Windows 上它默认生成 Visual Studio 的 .sln/.vcxproj也可以生成 MinGW 的 Makefile在 Linux 上它生成 Makefile 或者 Ninja 的 build.ninja在 macOS 上它还能生成 Xcode 工程。所以 CMake 的定位是build system generator构建系统生成器不是构建系统本身。真正干活的是它生成出来的那套东西以及后面的 make、ninja、MSBuild。理解这一点非常关键因为它直接解释了后面几个高频问题为什么cmake .之后还要再make或者cmake --build .——因为第一步只是“生成”第二步才是“构建”。为什么同一个项目在 Windows 和 Linux 上都能编——因为描述文件是同一份生成出来的工程文件是两套。为什么新手总觉得它绕——因为多了一层间接多了一层就要多一次理解成本。我个人习惯把 CMake 类比成“装修图纸转换器”。你画的是一份标准图纸CMakeLists.txt转换器会根据施工队是江苏的还是广东的输出成他们各自看得懂的施工单Makefile / vcxproj。图纸不用改施工单随时可以重出。1.1 为什么它值得花时间学说句实在话如果只写单文件的练习代码直接gcc main.c -o main就够了根本用不着 CMake。但只要项目超过三个源文件、跨两个目录、还要链接第三方库手写编译命令就会迅速失控。这时候 CMake 的价值就出来了源文件列表改一处全平台同步生效换个编译器不用重写构建脚本接 CI 的时候流水线里三行命令就能跑起来。嵌入式这行尤其明显。以前用 Keil 或者 IAR 的时候工程文件是 IDE 私有的二进制格式git diff 出来全是乱码两个人同时改工程配置基本必然冲突。换成 CMake 之后工程配置就是纯文本谁改了什么一目了然。1.2 版本号这件事比你想的重要热词里出现了“ubuntu cmake 版本”说明不少人在这上面栽过。CMake 的版本差异不是小修小补cmake_minimum_required写 3.5 和写 3.20能用的命令、能生效的策略policy是两回事。Ubuntu 20.04 自带的 apt 源里是 3.1622.04 是 3.22而某些新库张口就要 3.20 以上。查版本很简单cmake --version输出第一行就是版本号。我一般建议在项目开头就把最低版本写清楚别写cmake_minimum_required(VERSION 2.8)这种远古写法那是十年前教程留下的遗毒现在写它只会让 CMake 走一堆兼容旧行为的策略分支反而更容易出怪问题。提示如果你在 Ubuntu 上用 apt 装完发现版本太老别急着到处找 PPA。最省事的做法是去 CMake 官网下载对应的 Linux x86_64 二进制 tar.gz解压到 /opt 下再软链接到 /usr/local/bin比折腾源干净得多。2. 安装与第一个能跑起来的项目先把环境弄干净再谈写代码。我见过太多人一上来就闷头写 CMakeLists.txt结果敲命令报错分不清是文件写错了还是环境没装好来回折腾一下午。2.1 Windows装完之后那三个必须确认的动作Windows 上最推荐的装法是官网下载 64 位 msi 安装包。注意是 x86_64 那个热词里“cmake wind10 64位”问的就是这个——别下成 32 位的虽然大多数情况也能跑但和你系统里的 64 位编译器混用时会出一些很迷的链接错误。安装过程中有两个勾选特别关键Add CMake to the system PATH这个决定了你能不能直接在命令行敲 cmake。选“为所有用户添加”还是“为当前用户添加”都行区别只在于写进的是系统 PATH 还是用户 PATH。安装路径尽量别带空格和中文。C:\Program Files\CMake是默认值问题不大但如果你手滑装到D:\我的工具\cmake这种路径后面配工具链的时候大概率要骂人。装完之后一定要重新开一个终端。已经打开的 PowerShell 不会自动刷新环境变量这就是为什么很多人明明装了、勾了 PATH还是看到那行“无法将 cmake 项识别为 cmdlet”。另外 VS Code 里的集成终端也一样装完 CMake 得把整个 VS Code 重启或者按 CtrlShiftP 执行一次重载窗口否则它继承的还是旧环境。验证三步走where.exe cmake cmake --version cmake --helpwhere.exe能列出所有叫 cmake 的路径如果这里有输出但cmake --version还是失败那基本就是 PATH 里有多个 cmake前面那个损坏或者被删了。如果确实没加进 PATH手动加也行打开“编辑系统环境变量”→ 环境变量 → 在用户变量里找到 Path → 编辑 → 新建一条指向 CMake 的 bin 目录比如C:\Program Files\CMake\bin。改完同样要重开终端。除了 msi还有几条路可以走各有适用场景安装方式命令适合谁wingetwinget install Kitware.CMakeWin10 1809 之后的系统一条命令搞定scoopscoop install cmake喜欢把工具装在用户目录、不污染系统的pippip install cmake已经有一套 Python 环境想顺手装的zip 免安装解压后手动加 PATH没有管理员权限的公司电脑注意pip 装出来的 cmake 在虚拟环境里是隔离的。如果你在项目 A 的 venv 里装的切到别的终端就找不到这点和 msi 全局安装完全不同别装完就失忆。2.2 Ubuntuapt 装完之后的第一件事Ubuntu 上简单sudo apt update sudo apt install cmake。但装完第一件事还是查版本。sudo apt update sudo apt install -y cmake build-essential cmake --versionbuild-essential会把 gcc、g、make 一次性带上不然 CMake 生成完 Makefile 你会发现 make 也没装。如果版本太老两种补救方式。一是官方预编译包这个最稳wget https://github.com/Kitware/CMake/releases/download/v3.29.6/cmake-3.29.6-linux-x86_64.tar.gz sudo tar -zxvf cmake-3.29.6-linux-x86_64.tar.gz -C /opt sudo ln -sf /opt/cmake-3.29.6-linux-x86_64/bin/cmake /usr/local/bin/cmake二是pip install cmake --upgrade好处是快坏处是它装在 Python 的 bin 目录里多用户环境下别人不一定能用。判断到底用的是哪个 cmake用which -a cmake能把路径全列出来。/usr/local/bin 一般排在 /usr/bin 前面所以软链接过去之后会优先命中新版。2.3 第一个最小工程从零到可执行建个目录两个文件先跑通再说。# CMakeLists.txt cmake_minimum_required(VERSION 3.15) project(hello LANGUAGES C) add_executable(hello main.c)/* main.c */ #include stdio.h int main(void) { printf(hello cmake\n); return 0; }然后执行cmake -S . -B build cmake --build build ./build/helloWindows 上最后一步换成.\build\Debug\hello.exe因为 VS 生成器默认是 Debug 配置会在 build 目录下再套一层配置名文件夹。这里出现的-S . -B build是 CMake 3.13 之后推荐的写法S 是 sourceB 是 build。老教程里的mkdir build cd build cmake ..效果一样只是多敲几行而且一不小心就在源码目录里拉一地文件。3. CMakeLists.txt 的骨架三行命令背后的逻辑能跑通一个文件之后往下就要面对真实项目了。真实项目的 CMakeLists.txt 通常分四块版本与工程声明、产物定义、依赖与链接、附加配置。搞清楚每一块的职责写起来就不会乱。3.1 cmake_minimum_required 到底是给谁看的这行不是写给人类看的注释它是写给 CMake 自己的。它告诉 CMake用这个版本的行为模式来解释我这份文件。CMake 有很多策略policy在不同版本间行为不一样比如 CMP0077 影响 option() 的处理方式CMP0069 影响 IPO 支持。写一个足够高的最低版本等于让 CMake 全部走新行为省得它一边跑一边给你发一堆开发者警告。那到底写多少合适我的经验纯自用的小工具写3.16就行这是 Ubuntu 20.04 的默认版本覆盖面足够。要用target_link_options3.13、FetchContent的完整功能3.14、cmake_path3.20就往上抬。别写VERSION 3.15...3.28这种范围写法除非你真的需要兼容旧行为新手用不上。3.2 project() 里不只有名字project(hello)是最简写法但它其实能带一堆参数project(mydemo VERSION 1.2.0 DESCRIPTION a small demo LANGUAGES C CXX ASM)几个值得注意的点LANGUAGES 一定要写。不写的话 CMake 默认启用 C 和 CXX会去探测 C 编译器。做纯 C 的嵌入式项目时会平白多一次编译器检查配置阶段变慢有时候还会因为找不到 C 编译器直接报错。VERSION 会生成宏比如MYDEMO_VERSION_MAJOR头文件里可以直接用做版本号输出的功能时很省事。project 之后CMake 会定义PROJECT_NAME、PROJECT_SOURCE_DIR这些变量后面写路径全靠它们。3.3 add_executable、target_include_directories、target_link_libraries 三件套这是现代 CMake 的核心思路一切围绕 target目标来配置而不是围绕目录。add_executable(demo src/main.c src/sensor.c) target_include_directories(demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) target_compile_options(demo PRIVATE -Wall -Wextra -O2) target_link_libraries(demo PRIVATE m)对比一下老写法# 不推荐 include_directories(include) add_executable(demo src/main.c src/sensor.c) link_libraries(m)两种写法在单目标项目里效果一样但项目一大就分道扬镳了。include_directories是目录级的加完之后这个目录以及所有子目录里定义的目标都受影响而且顺序敏感——加到add_executable之前还是之后作用范围不同。target_include_directories是目标级的只影响指定的那个 target谁需要谁加清晰可控。PRIVATE / PUBLIC / INTERFACE 这三个关键字也不是摆设关键字自己编译时用别人链接我时继承PRIVATE用不继承PUBLIC用继承INTERFACE不用继承举个例子我做一个sensor静态库它对外暴露的头文件里 include 了sensor_reg.h那sensor_reg.h所在目录就得用 PUBLIC 加因为用我这个库的人也得能找到它。而我只在 .c 内部用的sensor_debug.h就 PRIVATE别人不需要也不该看到。一开始记不住没关系先用 PRIVATE 跑起来等遇到“链接我的库时报找不到头文件”的时候再回来把它改成 PUBLIC这个错误会教你一辈子。4. build 目录、缓存和那堆让人心慌的文件第一次跑完 cmakebuild 目录里会冒出 CMakeCache.txt、CMakeFiles、cmake_install.cmake 一堆东西。很多人看着就怕觉得污染了源码。其实这些都是正常产物理解它们能帮你少走很多弯路。4.1 为什么强烈建议 out-of-source build所谓 out-of-source就是构建产物和源码分开放。cmake -S . -B build这种写法天然就是 out-of-source。好处有三个都很实在清理彻底。哪天构建乱了rm -rf build一把梭源码一根汗毛都不少。in-source 构建的话你得挨个分辨哪个 .o 是生成的、哪个 .h 是自己写的。多配置并存。build-debug和build-release两个目录各自独立切换配置就是切目录不需要重新配置。做性能对比测试时这个太方便了。git 干净。.gitignore 里一句build*/就够了。我踩过的一个坑是在同一个 build 目录里先配置了 MinGW 生成器又想切到 VS 生成器。这时候直接重跑 cmake 会报“生成器不匹配”。正确做法是把 build 目录整个删掉重来因为 CMakeCache.txt 里记着上一次的生成器和编译器路径改不动。4.2 CMakeCache.txt既是福也是祸CMakeCache.txt 存的是所有缓存变量编译器路径、生成器、各种-D传进来的开关。它的价值在于第二次配置时不用重新探测编译器速度快很多。但它也是最容易让人迷惑的东西。典型场景你第一次用了-DUSE_SSLOFF后来想改成 ON直接重跑 cmake 有时候不生效——因为缓存里那个变量的类型和值已经定死了。这时候有两个办法# 办法一显式强制覆盖 cmake -S . -B build -DUSE_SSLON # 办法二直接删掉缓存重配 rm -rf build cmake -S . -B build一般正常的-DXXXYYY第二次是能覆盖的覆盖不了的是那些被set(... CACHE ... FORCE)或者内部逻辑提前写死的。实在搞不清就删 build成本最低。4.3 换编译器和切 Debug/ReleaseWindows 上如果装了 Visual StudioCMake 默认就用 VS 生成器走的是多配置模式CMAKE_BUILD_TYPE在 VS 生成器下是无效的那个变量只有单配置生成器Makefile、Ninja才认。单配置生成器切配置cmake -S . -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j8多配置生成器VS、Xcode切配置cmake --build build --config Release这两个写法混用是高频错误。有人在 Ninja 下写--config Release发现压根没生效编出来的还是没优化的版本反过来在 VS 下写-DCMAKE_BUILD_TYPEReleaseCMake 连警告都不会给你静默忽略。记住一句话单配置用 CMAKE_BUILD_TYPE多配置用 --config。换编译器则通过CMAKE_C_COMPILER传cmake -S . -B build-mingw -G MinGW Makefiles \ -DCMAKE_C_COMPILERgcc \ -DCMAKE_CXX_COMPILERg这里有个铁律编译器一旦选定就不能在同一个 build 目录里换。想换就新开一个目录或者删掉缓存。5. 嵌入式视角CMake 和 Keil、J-Link、ESP-IDF 怎么打交道热词里好几个都指向嵌入式cmake jlink、cmake 可以代替 keil5 吗、如何将 keil 工程变成 cmake、还有那行include($ENV{IDF_PATH}/tools/cmake/project.cmake)。这块单独拎出来说因为场景和纯 PC 开发差别挺大。5.1 CMake 能代替 Keil5 吗边界在哪直接回答构建环节能替代调试和芯片支持包这块替代不了。Keil5 强在几件事一是 Arm 官方的设备支持包DFP点几下就把启动文件、链接脚本、寄存器头文件全配好了二是 μVision 里集成的调试器接上 ST-Link 或 J-Link 直接打断点看寄存器这套体验目前没有同等顺手的开源替代三是 Flash 算法各家芯片的下载算法都是现成的。CMake 能接管的是构建源文件组织、编译选项、依赖管理、生成 elf/hex/bin。它不管调试也不管芯片的寄存器定义。所以比较务实的做法是构建用 CMake调试用 Keil 或者 OpenOCD GDB。真要把整个流程搬出来就得自己准备这几样东西arm-none-eabi-gcc工具链芯片的启动文件startup_xxx.s链接脚本.ldCMSIS 头文件一份 toolchain file 告诉 CMake 用交叉编译器5.2 从 Keil 工程迁移到 CMake 的四步拆解这活儿我做过几次流程是固定的急不来。第一步把文件清单导出来。Keil 的 .uvprojx 本质是 XML用记事本打开能找到所有参与编译的源文件路径。把它们按目录分类整理启动文件和链接脚本单独标记出来。第二步写 toolchain file。这个文件只负责告诉 CMake“我要用交叉编译器”和具体项目无关可以复用。# arm-none-eabi.cmake set(CMAKE_SYSTEM_NAME Generic) set(CMAKE_SYSTEM_PROCESSOR cortex-m4) set(CMAKE_C_COMPILER arm-none-eabi-gcc) set(CMAKE_CXX_COMPILER arm-none-eabi-g) set(CMAKE_ASM_COMPILER arm-none-eabi-gcc) set(CMAKE_OBJCOPY arm-none-eabi-objcopy) set(CMAKE_SIZE arm-none-eabi-size) set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)最后那行STATIC_LIBRARY很关键。默认情况下 CMake 会尝试链接一个可执行文件来验证编译器但裸机环境没有默认的链接脚本一链接就报一堆undefined reference to _start。设成静态库就跳过链接验证了这个坑我第一次迁移时卡了大半天。第三步把编译选项搬过来。Keil 的 Options for Target 里那些勾对应 GCC 的参数大体是Keil 配置项GCC/CMake 对应Optimization Level -O2-O2C99 Mode-stdc99Define 宏target_compile_definitionsInclude Pathstarget_include_directoriesMisc Controlstarget_compile_options注意 Keil 的 ARMCC 和 GCC 有些内联汇编语法不同__asm那种写法在 GCC 下要改成__asm volatile一段段过。第四步加生成 hex/bin 和烧录的 target。add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O ihex $TARGET_FILE:${PROJECT_NAME} app.hex COMMAND ${CMAKE_OBJCOPY} -O binary $TARGET_FILE:${PROJECT_NAME} app.bin)$TARGET_FILE:...是生成器表达式能自动解析出完整产物路径比手写路径靠谱得多改配置也不会断。5.3 J-Link 接入加一个 flash target 就够我不太推荐把烧录写死在构建流程里那样每次编译都会触发下载调代码时很烦。更好的做法是单独定义一个 targetadd_custom_target(flash COMMAND JLinkExe -device STM32F407VG -if SWD -speed 4000 -autoconnect 1 -CommanderScript flash.jlink DEPENDS ${PROJECT_NAME} COMMENT download firmware via J-Link)flash.jlink 里写halt loadfile build/app.hex r go qc这样平时cmake --build build只编译要下载的时候再cmake --build build --target flash。Windows 下把 JLinkExe 换成 JLink.exe参数一样。提示JLinkExe 得在 PATH 里或者写全路径。公司电脑没管理员权限的话写全路径最省事别折腾环境变量。5.4 那行 include($ENV{IDF_PATH}/tools/cmake/project.cmake) 在干什么这是 ESP-IDF 项目的标准开头很多第一次看到的人完全懵为什么一上来就 include 一个环境变量拼出来的路径拆开看就三步。$ENV{IDF_PATH}是读环境变量 IDF_PATH也就是你 ESP-IDF 的安装目录。tools/cmake/project.cmake是这个目录下的一个脚本文件。include 就是把它加载进来。那这个脚本到底做了什么它重定义了 project() 这个命令的行为。正常情况下 project() 只管声明工程名但在 ESP-IDF 里它被扩展成了扫描 components 目录下所有组件的 CMakeLists.txt、构建 bootloader、生成分区表、把 FreeRTOS 和各种驱动一次性挂上。所以你才会看到 IDF 项目的 CMakeLists.txt 只有短短几行核心工作全被那行 include 带来的脚本接管了。顺带说一句idf.py本身就是一个包装了 CMake 的 Python 脚本。你敲idf.py build它内部就是先调 cmake 配置再调 cmake --build只是顺手处理了工具链路径、环境变量和 Python 依赖。理解这层关系之后很多“idf.py 和 cmake 到底谁管谁”的困惑就没了。6. 环境维护卸载、多版本共存和一些提速手法环境这东西装着装着就脏了。清理干净和当初装好一样重要尤其是磁盘紧张或者搞多版本测试的时候。6.1 Windows 卸载删完程序还得清 PATH正确顺序是控制面板 → 程序和功能 → 找到 CMake → 卸载。卸载完还有个尾巴——PATH 里那条C:\Program Files\CMake\bin不会被自动移除。这条残留看着无害但实际上很坑下次你换个方式重装 CMake比如用 scoop 装到用户目录系统里就有两条 PATH前面那条指向已经不存在的目录命令行可能还是找不到 cmake。所以卸载后记得回环境变量里把那条删掉。另外如果之前用过 MSI 安装包注册表里可能还有残留用系统的“应用和功能”卸干净一般就够了不用上第三方清理工具。6.2 Linux 多版本共存靠软链接切换Linux 上想同时留几个版本做兼容性测试思路很简单每个版本解压到 /opt 下一个独立目录只在前台维护一个软链接。# 两个版本都解压好之后 sudo ln -sf /opt/cmake-3.29.6-linux-x86_64/bin/cmake /usr/local/bin/cmake cmake --version # 想切回旧版本 sudo ln -sf /opt/cmake-3.16.9-linux-x86_64/bin/cmake /usr/local/bin/cmake如果连 make、ctest 这些配套命令也想跟着切就干脆把整个 bin 目录软链过去或者用 update-alternatives 管理。不过说实话大部分人只用到 cmake 一条命令单链一个文件最省事。卸载的话删掉 /opt 下的目录和 /usr/local/bin 里的那条软链接就完事了不会有别的残留这也是我喜欢手动装的原因——来去干净。6.3 几个能省时间的日常手法用 Ninja 代替 make。CMake 支持-G Ninja增量编译速度比 make 快不少尤其在文件多的项目上。前提是装了 ninjaUbuntu 上sudo apt install ninja-buildWindows 上 winget 或者 scoop 都能装。切过去就是cmake -S . -B build -G Ninja cmake --build build用 CMakePresets.json 固化配置。如果你每次都要敲一长串 -D 参数把它写进预设文件以后一条命令搞定。CMake 3.19 之后支持{ version: 3, configurePresets: [ { name: debug, generator: Ninja, binaryDir: ${sourceDir}/build/debug, cacheVariables: { CMAKE_BUILD_TYPE: Debug, CMAKE_EXPORT_COMPILE_COMMANDS: ON } } ] }然后cmake --preset debug、cmake --build --preset debug就行。打开 compile_commands.json。上面那个CMAKE_EXPORT_COMPILE_COMMANDS开关会在 build 目录里生成一份编译数据库。VS Code 配上 clangd 或者 C/C 插件之后跳转、补全、报错提示全都能准确识别到实际的编译参数比靠猜头文件路径配出来的体验好太多。做嵌入式交叉编译时不打开这个编辑器大概会给你的寄存器操作代码标满红。善用 --fresh。CMake 3.24 起提供了cmake --fresh等价于清空缓存重新配置但不用手动删目录。清理缓存这事儿从此有了一条正经命令。这一路从命令行报错、装环境、写最小工程到目录组织、缓存机制再到嵌入式迁移和烧录接入基本覆盖了入门阶段会撞上的所有典型问题。真正上手之后你会发现CMake 的语法其实不多难点全在“为什么这么设计”和“出错了往哪查”。我的经验是遇到怪问题先看三样CMakeCache.txt 里的变量、cmake --version 的版本号、以及 build 目录是不是该删了。这三样排查完八成的问题就有方向了。
返回列表