第16章:编写工具链文件

工具链文件,说白了就是告诉CMake:「咱们用的编译器是谁、目标芯片长什么样、头文件和库文件去哪儿找」。这玩意儿在嵌入式开发里是绕不开的坎儿。我刚开始用CMake做嵌入式项目时,就因为这工具链文件没写好,折腾了整整两天才把程序烧进板子。

嗯,咱们今天就把这事儿彻底说清楚。

16.1 工具链文件的基本结构

工具链文件其实就是一个CMake脚本文件,通常命名为toolchain.cmakearm-none-eabi.cmake。它的核心任务就三个:指定编译器、指定目标架构、设置sysroot。

先看一个最基础的模板:

# 工具链文件名:arm-none-eabi-gcc.cmake

# 指定编译器
set(CMAKE_C_COMPILER   arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

# 指定目标架构
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 设置sysroot(交叉编译的根文件系统路径)
set(CMAKE_SYSROOT /usr/lib/arm-none-eabi)

# 告诉CMake这是交叉编译
set(CMAKE_CROSSCOMPILING TRUE)

这段代码看起来简单,但每个变量都有它的脾气。我一个个说。

16.2 指定编译器:别只写名字

很多人以为指定编译器就是写个名字完事。其实不然。我个人习惯用绝对路径,尤其是在团队协作时:

# 推荐:使用绝对路径
set(CMAKE_C_COMPILER /usr/bin/arm-none-eabi-gcc)

# 或者通过find_program动态查找
find_program(ARM_GCC arm-none-eabi-gcc)
if(NOT ARM_GCC)
    message(FATAL_ERROR "找不到arm-none-eabi-gcc编译器")
endif()
set(CMAKE_C_COMPILER ${ARM_GCC})

为什么要用绝对路径?我在项目中遇到过这种情况:同事的电脑上装了多个版本的GCC工具链,结果CMake找到了错误版本,编译出来的程序在板子上跑不起来。用绝对路径或find_program加检查,能避免这种低级错误。

注意:CMAKE_C_COMPILER必须在project()命令之前设置。否则CMake会先检测默认编译器,然后才读取你的设置——那就晚了。

16.3 指定目标架构:别让CMake猜

嵌入式世界里,ARM、RISC-V、Xtensa……每种架构都有自己的ABI和指令集。CMake需要知道这些信息才能正确配置编译选项。

我一般这样设置:

# 目标系统名称:Generic表示裸机或RTOS
set(CMAKE_SYSTEM_NAME Generic)

# 目标处理器架构
set(CMAKE_SYSTEM_PROCESSOR arm)

# 如果使用ARM Cortex-M系列,建议明确指定
set(CMAKE_SYSTEM_PROCESSOR cortex-m4)

这里有个坑:CMAKE_SYSTEM_NAME如果设置成Generic,CMake就不会去检测宿主机的系统特性。如果你设置成Linux,CMake会认为目标系统是Linux,然后去找Linux相关的库和头文件——这在裸机开发中会出大问题。

我曾经接手过一个项目,前任工程师把CMAKE_SYSTEM_NAME设成了Linux,结果编译出来的程序链接了glibc,而目标板子上跑的是FreeRTOS……嗯,那画面太美我不敢看。

16.4 设置sysroot:头文件和库的根目录

sysroot是交叉编译的核心概念。它告诉编译器:「所有系统头文件和库文件都从这个目录找,别去宿主机的/usr/include找。」

# 设置sysroot
set(CMAKE_SYSROOT /opt/gcc-arm-none-eabi-9-2020-q2-update/arm-none-eabi)

# 或者更精细地分别设置
set(CMAKE_FIND_ROOT_PATH /opt/gcc-arm-none-eabi-9-2020-q2-update/arm-none-eabi)
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:告诉CMake去哪里找目标系统的文件
  • MODE_PROGRAM NEVER:查找程序(如编译器)时不要限制在sysroot里
  • MODE_LIBRARY ONLY:查找库文件时只在sysroot里找
  • MODE_INCLUDE ONLY:查找头文件时只在sysroot里找

为什么要这样设置?你想想看,如果CMake在宿主机的/usr/lib里找到了libc.so,然后链接进去——那程序在目标板子上肯定跑不起来,因为架构都不一样。

16.5 常见坑点:我踩过的那些雷

做嵌入式CMake工具链文件,有几个坑我几乎每次都会遇到。分享出来,你们少走点弯路。

坑点1:CMAKE_C_COMPILER设置时机

前面提过,CMAKE_C_COMPILER必须在project()之前设置。但很多人会忘记在工具链文件里写project(),然后在主CMakeLists.txt里才调用project()——这没问题。但如果你在工具链文件里调用了project(),那就要小心了,因为project()会重置一些变量。

坑点2:编译器标志重复

我曾经在工具链文件里设置了-mcpu=cortex-m4,然后在CMakeLists.txt里又加了一遍。结果编译时出现了重复的-mcpu参数,虽然GCC能处理,但看着不舒服。更严重的是,如果两个参数冲突(比如一个设-mthumb,另一个设-marm),编译器会报错。

我的建议是:架构相关的标志放在工具链文件里,应用相关的标志放在CMakeLists.txt里。

坑点3:sysroot路径写错

这个坑我踩过两次。第一次是路径末尾多了一个斜杠,第二次是路径指向了宿主机的/usr目录。检查方法很简单:在工具链文件里加一行message(STATUS "sysroot: ${CMAKE_SYSROOT}"),然后看CMake输出对不对。

坑点4:忘记设置CMAKE_TRY_COMPILE_TARGET_TYPE

对于裸机开发,CMake在检测编译器时会尝试编译一个可执行文件。但裸机环境下没有main函数和启动文件,所以编译会失败。解决办法是:

set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

这样CMake只会尝试编译成静态库,不需要链接,也就不会报错了。

16.6 一个完整的工具链文件示例

说了这么多,咱们看一个实际项目中能用的完整示例。这是我给STM32F4系列写的一个工具链文件:

# STM32F4工具链文件
# 文件名:stm32f4-toolchain.cmake

# 编译器路径
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_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR cortex-m4)

# sysroot
set(CMAKE_SYSROOT /opt/gcc-arm-none-eabi-9-2020-q2-update/arm-none-eabi)
set(CMAKE_FIND_ROOT_PATH ${CMAKE_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_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)

# 架构相关编译选项
set(COMMON_FLAGS "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_C_FLAGS_INIT ${COMMON_FLAGS})
set(CMAKE_CXX_FLAGS_INIT ${COMMON_FLAGS})
set(CMAKE_ASM_FLAGS_INIT ${COMMON_FLAGS})

# 链接选项
set(CMAKE_EXE_LINKER_FLAGS_INIT "-mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16 -specs=nano.specs -specs=nosys.specs")

# 交叉编译标志
set(CMAKE_CROSSCOMPILING TRUE)

这个文件我用了好几年,基本没出过问题。你根据自己的芯片型号和工具链路径改一下就能用。

16.7 知识体系图

下面这张图总结了工具链文件的核心要素和它们之间的关系:

工具链文件核心要素 工具链文件 指定编译器 指定目标架构 设置sysroot CMAKE_C_COMPILER CMAKE_CXX_COMPILER CMAKE_ASM_COMPILER CMAKE_SYSTEM_NAME CMAKE_SYSTEM_PROCESSOR 架构编译选项(-mcpu等) CMAKE_SYSROOT CMAKE_FIND_ROOT_PATH 查找模式(MODE_xxx) 常见坑点:设置时机 | 标志重复 | sysroot路径 | 裸机编译检测

16.8 使用工具链文件

写好了工具链文件,怎么用?很简单,在CMake命令行里指定:

cmake -DCMAKE_TOOLCHAIN_FILE=stm32f4-toolchain.cmake ..

或者在CMakeLists.txt里设置(不推荐,因为会降低灵活性):

set(CMAKE_TOOLCHAIN_FILE stm32f4-toolchain.cmake)

我个人习惯在命令行里指定,这样同一个项目可以针对不同目标芯片切换工具链文件,不用改代码。

小技巧:如果你经常切换工具链,可以写一个shell脚本或者CMake预设文件(CMakePresets.json),把常用的工具链配置保存起来。这样每次只需要执行cmake --preset=stm32f4就行了。

好了,工具链文件的内容就这些。记住一句话:工具链文件是CMake和嵌入式硬件之间的翻译官,写好了它,后面的编译工作就顺风顺水。写不好……嗯,你可能会像我一样,在调试器前坐一整天。


公众号:蓝海资料掘金营,微信deep3321