多架构构建:单一项目支持x86/ARM/RISC-V
做嵌入式开发的朋友,迟早会遇到一个头疼的问题:同一个项目,要跑在不同架构的芯片上。x86、ARM、RISC-V,这三兄弟各有各的脾气。我最早接触这个需求,是在一个物联网网关项目里。客户要求代码既能跑在x86的开发机上调试,又能部署到ARM的嵌入式板子上,后来还加了个RISC-V的备选方案。嗯,那会儿真是折腾得够呛。
说白了,CMake就是来解决这个问题的。它天生就是为多平台构建设计的。你想想看,如果每个架构都维护一套独立的Makefile,那项目规模一大,维护成本直接爆炸。CMake通过一套配置,就能生成不同架构的构建文件,这才是正道。
核心思路:工具链文件
CMake支持多架构构建的核心,就是工具链文件(Toolchain File)。这个文件告诉CMake:你要用什么编译器、什么链接器、什么编译选项。每个架构对应一个工具链文件,构建时指定一下就行。
我个人习惯把工具链文件放在项目根目录的 cmake/toolchains/ 文件夹下。这样结构清晰,找起来也方便。
关键点:工具链文件不是用来写业务逻辑的,它只负责定义编译器、链接器、架构标志等底层工具链信息。业务逻辑的配置,应该放在CMakeLists.txt里通过条件判断来处理。
实战:三个架构的工具链文件
我们先看x86的。x86最简单,因为开发机通常就是x86,直接用系统自带的gcc或clang就行。
# cmake/toolchains/x86_64-linux-gnu.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -m64")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m64")
ARM的就复杂一些。你需要指定交叉编译器。我记得第一次配ARM工具链时,忘了设置 CMAKE_FIND_ROOT_PATH,结果链接器一直找不到系统库,折腾了一下午。
# cmake/toolchains/arm-none-eabi.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++)
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)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mcpu=cortex-m4 -mthumb -mfloat-abi=hard -mfpu=fpv4-sp-d16")
RISC-V的配置和ARM类似,只是架构标志不同。
# cmake/toolchains/riscv64-unknown-elf.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR riscv64)
set(CMAKE_C_COMPILER riscv64-unknown-elf-gcc)
set(CMAKE_CXX_COMPILER riscv64-unknown-elf-g++)
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)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=rv64imafdc -mabi=lp64d")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=rv64imafdc -mabi=lp64d")
小技巧:如果你用的是ARM Cortex-M系列,建议把 -specs=nano.specs 和 -specs=nosys.specs 也加进去。这样可以减小固件体积,而且不需要完整的系统库。我在一个Flash只有64KB的项目里,靠这个省出了将近10KB的空间。
条件编译与配置
工具链文件搞定了,接下来就是代码层面的条件编译。CMake提供了两种方式:编译时宏定义和CMake变量。
先说编译时宏定义。在CMakeLists.txt里,你可以根据当前架构,定义不同的宏。
# CMakeLists.txt
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
add_definitions(-DPLATFORM_X86)
message(STATUS "Building for x86_64")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
add_definitions(-DPLATFORM_ARM)
message(STATUS "Building for ARM")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64")
add_definitions(-DPLATFORM_RISCV)
message(STATUS "Building for RISC-V")
else()
message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
然后在C代码里,你就可以用这些宏来写平台相关的代码。
/* main.c */
#include <stdio.h>
void platform_init(void) {
#if defined(PLATFORM_X86)
printf("Initializing x86 platform\n");
/* x86特有的初始化代码 */
#elif defined(PLATFORM_ARM)
printf("Initializing ARM platform\n");
/* ARM特有的初始化代码,比如配置NVIC */
#elif defined(PLATFORM_RISCV)
printf("Initializing RISC-V platform\n");
/* RISC-V特有的初始化代码,比如配置PLIC */
#else
#error "Unknown platform"
#endif
}
int main(void) {
platform_init();
printf("Hello from multi-arch project!\n");
return 0;
}
另一种方式是通过CMake变量。这种方式更灵活,适合控制编译选项、链接库等。
# CMakeLists.txt
if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64")
set(HAS_FPU TRUE)
set(LINKER_SCRIPT "")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm")
set(HAS_FPU TRUE)
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/linker/arm.ld")
elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64")
set(HAS_FPU FALSE)
set(LINKER_SCRIPT "${CMAKE_SOURCE_DIR}/linker/riscv.ld")
endif()
if(HAS_FPU)
add_definitions(-DHAS_FPU)
endif()
if(LINKER_SCRIPT)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -T ${LINKER_SCRIPT}")
endif()
注意:不要在一个CMakeLists.txt里写太多架构判断逻辑。我见过一个项目,CMakeLists.txt里密密麻麻全是if-else,读起来比C代码还累。建议把架构相关的配置抽到单独的文件里,比如 cmake/arch_config.cmake,然后用 include() 引入。
构建命令
配置好之后,构建就很简单了。你只需要在cmake命令里指定工具链文件。
# 构建x86版本
mkdir build-x86 && cd build-x86
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/x86_64-linux-gnu.cmake
make
# 构建ARM版本
mkdir build-arm && cd build-arm
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/arm-none-eabi.cmake
make
# 构建RISC-V版本
mkdir build-riscv && cd build-riscv
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/riscv64-unknown-elf.cmake
make
每个架构的构建产物都在各自的目录里,互不干扰。这就是CMake的out-of-source构建,干净利落。
知识体系图
下面这张图展示了多架构构建的核心逻辑。从项目根目录出发,通过不同的工具链文件,生成不同架构的构建系统,最终产出对应的可执行文件或固件。
避坑指南
最后分享几个我踩过的坑。
- 工具链路径问题:我曾经把交叉编译器的路径写死在工具链文件里,结果换了一台电脑就编译不过了。后来我改用环境变量,比如
set(CMAKE_C_COMPILER $ENV{ARM_GCC}),这样灵活多了。 - 系统库依赖:ARM和RISC-V的交叉编译,通常没有完整的系统库。如果你在代码里用了
printf,链接时可能会报错。解决方案是用-specs=nano.specs或者自己实现一个精简的_write函数。 - 浮点支持:不是所有ARM和RISC-V芯片都有硬件浮点单元。如果你在工具链里开了
-mfloat-abi=hard,但芯片不支持,程序会跑飞。建议在CMake里加一个选项,让用户显式指定是否启用FPU。 - 链接脚本:不同架构的链接脚本差异很大。不要把链接脚本放在代码目录里,而是放在独立的
linker/目录下,通过CMake变量指定路径。
多架构构建,说白了就是一套代码,多套配置。CMake把这件事做得相当漂亮。你只要把工具链文件写好,剩下的就是条件编译和构建命令的事了。嗯,希望这些经验能帮你少走弯路。