23、工具链文件:交叉编译入门。如何编写Toolchain.cmake,为ARM、RISC-V等嵌入式平台构建代码。
说实话,我第一次接触交叉编译时,心里是有点发怵的。那时候我刚从桌面端开发转到嵌入式,习惯了在 x86 上写代码、点一下编译就跑起来。结果一上来就要给 ARM 板子编译固件,连个 hello world 都跑不通。后来我才明白,核心问题就出在——CMake 不知道你的目标平台是谁。
嗯,今天我们就来聊聊这个。说白了,Toolchain.cmake 就是告诉 CMake:「嘿,别用你本机的编译器,用这个交叉编译器,目标平台是 ARM 或 RISC-V。」
什么是交叉编译?为什么需要它?
交叉编译,就是在你的 PC(x86 架构)上,编译出能在 ARM、RISC-V 等嵌入式平台上运行的二进制文件。你想想看,嵌入式设备通常资源有限,跑个编译器都费劲,更别说跑整个构建系统了。所以我们在强大的开发机上完成编译,再把产物丢到目标板上运行。
我遇到过不少新手,直接在树莓派上编译代码,等得花儿都谢了。其实完全没必要——交叉编译才是正道。
Toolchain.cmake 的核心职责
一个标准的 Toolchain.cmake 文件,主要做三件事:
- 指定目标系统:告诉 CMake 目标平台的名字、处理器架构、操作系统等。
- 指定交叉编译器:设置 C 编译器、C++ 编译器、链接器、汇编器等的路径。
- 指定 sysroot:告诉编译器去哪里找目标平台的头文件和库文件。
说白了,就是给 CMake 一个「翻译官」,让它知道怎么跟嵌入式平台打交道。
核心原则:Toolchain.cmake 必须在第一次运行 cmake 配置时通过 -DCMAKE_TOOLCHAIN_FILE 指定,之后不能再更改。否则 CMake 会报错,或者产生不一致的构建。
实战:为 ARM Cortex-M4 编写 Toolchain.cmake
假设我们用的是 ARM GCC 工具链(arm-none-eabi-gcc),目标芯片是 STM32F4 系列。下面是一个完整的例子:
# arm-gcc-toolchain.cmake
# 目标系统
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 指定编译器
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
# 指定 sysroot(可选,但强烈建议)
set(CMAKE_SYSROOT /opt/arm-none-eabi/arm-none-eabi)
# 查找工具链中的其他程序
set(CMAKE_FIND_ROOT_PATH /opt/arm-none-eabi)
# 搜索策略:只在 sysroot 中查找库和头文件
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)
# 设置目标架构和浮点选项(针对 Cortex-M4F)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard")
# 链接脚本(可选,但嵌入式项目常用)
set(LINKER_SCRIPT ${CMAKE_SOURCE_DIR}/stm32f4_flash.ld)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${LINKER_SCRIPT}")
这段代码里,有几个关键点我要强调一下:
CMAKE_SYSTEM_NAME设为Generic,因为裸机(bare-metal)没有操作系统。如果是 Linux 嵌入式,就设为Linux。CMAKE_FIND_ROOT_PATH_MODE_*这几个变量,控制 CMake 去哪里找头文件和库。我习惯把PROGRAM设为NEVER,避免 CMake 在 sysroot 里找本机工具。- 浮点选项要根据芯片来。Cortex-M4F 支持硬件浮点,但 M3 就不支持,别搞混了。
小技巧:如果你不确定编译器路径,可以在终端里先跑一下 which arm-none-eabi-gcc,确认工具链已经安装并加入了 PATH。我曾经因为路径写错,折腾了半小时才发现是工具链没装全。
实战:为 RISC-V 64 位 Linux 编写 Toolchain.cmake
RISC-V 这两年越来越火。假设我们用 riscv64-unknown-linux-gnu-gcc,目标是一个运行 Linux 的 RISC-V 板子:
# riscv64-linux-toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR riscv64)
set(CMAKE_C_COMPILER riscv64-unknown-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER riscv64-unknown-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/riscv64/sysroot)
set(CMAKE_FIND_ROOT_PATH /opt/riscv64)
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)
# RISC-V 架构选项
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=rv64gc -mabi=lp64d")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=rv64gc -mabi=lp64d")
这里 CMAKE_SYSTEM_NAME 是 Linux,因为目标板跑的是 Linux 内核。架构选项 rv64gc 表示 64 位 RISC-V,带通用扩展(G)和压缩指令(C)。lp64d 是 ABI,表示长指针 64 位,支持双精度浮点。
如何使用 Toolchain.cmake
写好了文件,怎么用呢?很简单,在配置项目时加上 -DCMAKE_TOOLCHAIN_FILE:
mkdir build-arm && cd build-arm
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-gcc-toolchain.cmake
make
或者用 CMake Presets 来管理,更优雅:
{
"version": 3,
"configurePresets": [
{
"name": "arm-debug",
"displayName": "ARM Debug",
"toolchainFile": "${sourceDir}/arm-gcc-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug"
}
},
{
"name": "riscv-release",
"displayName": "RISC-V Release",
"toolchainFile": "${sourceDir}/riscv64-linux-toolchain.cmake",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release"
}
}
]
}
我个人非常推荐用 Presets,尤其是项目要支持多个平台时。你只需要写一次配置,团队成员直接 cmake --preset arm-debug 就能用,省心不少。
避坑指南:我踩过的几个坑
嗯,这里分享几个我实际遇到过的问题,希望能帮你少走弯路:
- 忘记设置 CMAKE_SYSROOT:CMake 会去本机的
/usr/include找头文件,结果编译出一堆 x86 的符号,链接时各种 undefined reference。我曾经因为这个浪费了一整个下午。 - CMAKE_FIND_ROOT_PATH_MODE_PROGRAM 设成 ONLY:CMake 找不到本机的
cmake、make等工具,直接报错。记住,程序搜索模式要设成NEVER。 - 浮点 ABI 不匹配:ARM 的
mfloat-abi=soft和hard混用,链接时一堆undefined reference to __aeabi_dadd。解决方案是统一用hard,前提是你的芯片支持硬件浮点。 - 链接脚本路径写死:我一开始把链接脚本路径写成了绝对路径,结果换了个电脑就找不到文件了。后来改用
${CMAKE_SOURCE_DIR}相对路径,问题解决。
重要提醒:Toolchain.cmake 中不要使用 include() 或 add_subdirectory() 来引入其他 CMake 文件。这会导致 CMake 在解析工具链时执行额外的逻辑,容易产生不可预料的副作用。保持工具链文件简洁、专注。
知识体系:Toolchain.cmake 的核心逻辑
下面这张图,帮你理清 Toolchain.cmake 在整个构建流程中的位置和作用:
从这张图可以看得很清楚:Toolchain.cmake 在 CMake 配置阶段被读取,它决定了编译器、sysroot 和搜索策略。这三个要素共同作用,最终在构建阶段生成目标平台的可执行文件。任何一个环节出错,都会导致交叉编译失败。
总结一下
Toolchain.cmake 是交叉编译的「钥匙」。你只需要写一次,就能让 CMake 为 ARM、RISC-V 等嵌入式平台生成正确的构建系统。核心要点就三个:
- 正确设置
CMAKE_SYSTEM_NAME和CMAKE_SYSTEM_PROCESSOR - 指定交叉编译器路径和 sysroot
- 配置搜索策略,避免 CMake 误用本机库
我个人建议,把 Toolchain.cmake 放在项目根目录的 cmake/ 文件夹下,用 CMake Presets 来管理不同平台的配置。这样团队协作时,每个人都能一键切换目标平台,省时省力。
嗯,今天就聊到这里。下次你给嵌入式板子编译代码时,记得先写好 Toolchain.cmake——它会让你的交叉编译之路顺畅很多。