多架构构建:单一项目支持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构建,干净利落。

知识体系图

下面这张图展示了多架构构建的核心逻辑。从项目根目录出发,通过不同的工具链文件,生成不同架构的构建系统,最终产出对应的可执行文件或固件。

多架构构建核心逻辑 项目根目录 x86工具链文件 x86_64-linux-gnu.cmake ARM工具链文件 arm-none-eabi.cmake RISC-V工具链文件 riscv64-unknown-elf.cmake build-x86/ CMake生成Makefile build-arm/ CMake生成Makefile build-riscv/ CMake生成Makefile x86可执行文件 ARM固件 (.elf/.bin) RISC-V固件 (.elf/.bin) 条件编译:CMakeLists.txt 中根据架构设置宏和变量

避坑指南

最后分享几个我踩过的坑。

  • 工具链路径问题:我曾经把交叉编译器的路径写死在工具链文件里,结果换了一台电脑就编译不过了。后来我改用环境变量,比如 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把这件事做得相当漂亮。你只要把工具链文件写好,剩下的就是条件编译和构建命令的事了。嗯,希望这些经验能帮你少走弯路。


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