第16章:编写工具链文件
工具链文件,说白了就是告诉CMake:「咱们用的编译器是谁、目标芯片长什么样、头文件和库文件去哪儿找」。这玩意儿在嵌入式开发里是绕不开的坎儿。我刚开始用CMake做嵌入式项目时,就因为这工具链文件没写好,折腾了整整两天才把程序烧进板子。
嗯,咱们今天就把这事儿彻底说清楚。
16.1 工具链文件的基本结构
工具链文件其实就是一个CMake脚本文件,通常命名为toolchain.cmake或arm-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加检查,能避免这种低级错误。
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 知识体系图
下面这张图总结了工具链文件的核心要素和它们之间的关系:
16.8 使用工具链文件
写好了工具链文件,怎么用?很简单,在CMake命令行里指定:
cmake -DCMAKE_TOOLCHAIN_FILE=stm32f4-toolchain.cmake ..
或者在CMakeLists.txt里设置(不推荐,因为会降低灵活性):
set(CMAKE_TOOLCHAIN_FILE stm32f4-toolchain.cmake)
我个人习惯在命令行里指定,这样同一个项目可以针对不同目标芯片切换工具链文件,不用改代码。
cmake --preset=stm32f4就行了。
好了,工具链文件的内容就这些。记住一句话:工具链文件是CMake和嵌入式硬件之间的翻译官,写好了它,后面的编译工作就顺风顺水。写不好……嗯,你可能会像我一样,在调试器前坐一整天。
公众号:蓝海资料掘金营,微信deep3321