项目实战二:使用CMake交叉编译一个嵌入式Linux应用程序(ARM目标)

好了,前面我们聊了那么多CMake的理论和基础用法。说实话,光说不练假把式。今天咱们就来个真刀真枪的实战——用CMake交叉编译一个跑在ARM Linux上的应用程序。

这个项目我去年在做一个工业网关时正好用过。当时要在x86的开发机上编译出ARM架构的可执行文件,再传到板子上跑。嗯,这里面的坑,我踩过不少。今天一并分享给你。

1. 什么是交叉编译?

说白了,交叉编译就是在一种架构的机器上,编译出另一种架构上能跑的程序。

你想想看,你的开发机是x86_64的,但嵌入式板子是ARM Cortex-A系列的。直接在开发机上gcc编译出来的程序,ARM板子根本不认。怎么办?

答案就是:用交叉编译工具链。

核心概念:

  • 宿主机(Host):你正在用的开发机器,通常是x86_64
  • 目标机(Target):程序最终要运行的ARM板子
  • 交叉编译工具链:一套能在宿主机上生成目标机代码的工具集

2. 准备工作:获取交叉编译工具链

我个人习惯用Linaro提供的ARM交叉编译工具链。稳定,社区活跃,文档也全。

你可以从Linaro官网下载,或者直接用apt安装:

# 安装ARM交叉编译工具链(以Ubuntu为例)
sudo apt-get install gcc-arm-linux-gnueabihf
sudo apt-get install g++-arm-linux-gnueabihf

安装完后,检查一下:

arm-linux-gnueabihf-gcc --version

看到版本号,说明工具链就绪了。

小提示:如果你用的是ARM64(aarch64),那就装gcc-aarch64-linux-gnu。别搞混了,我刚开始就吃过这个亏。

3. 项目结构设计

我们先搭一个简单的项目。假设我们要写一个温湿度采集程序,从传感器读数据,然后打印出来。

项目目录结构如下:

arm_cross_compile_demo/
├── CMakeLists.txt          # 顶层CMake配置
├── main.c                  # 主程序
├── sensor/
│   ├── CMakeLists.txt      # 传感器模块的CMake配置
│   ├── sensor.h
│   └── sensor.c
└── build/                  # 构建目录(后面创建)

4. 编写CMake交叉编译配置文件

这里有个关键点:CMake本身并不知道你要交叉编译。你得告诉它:用哪套工具链,目标架构是什么。

我建议单独写一个工具链文件。这样项目干净,也方便切换不同的目标平台。

创建一个文件 arm_toolchain.cmake

# 指定目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# 指定查找路径(避免找到宿主机上的库)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
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)

为什么要设置这些FIND_ROOT_PATH_MODE?

我曾经在一个项目里没加这些,结果CMake在宿主机上找到了x86的libz库,链接进去后,ARM板子上跑起来直接段错误。排查了整整一下午……嗯,从那以后我再也不敢漏掉这几行了。

5. 编写顶层CMakeLists.txt

顶层文件负责整个项目的构建逻辑:

cmake_minimum_required(VERSION 3.10)
project(arm_sensor_demo C)

# 设置C标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 添加子目录
add_subdirectory(sensor)

# 生成可执行文件
add_executable(sensor_app main.c)

# 链接传感器库
target_link_libraries(sensor_app PRIVATE sensor_lib)

6. 编写传感器模块的CMakeLists.txt

sensor/CMakeLists.txt 中:

# 生成静态库
add_library(sensor_lib STATIC
    sensor.c
)

# 指定头文件路径
target_include_directories(sensor_lib PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

这里我用了静态库。为什么?嵌入式系统里,静态库可以减少运行时依赖,部署起来省心。动态库虽然省空间,但你要确保板子上有对应的.so文件,有时候挺麻烦的。

7. 编写源代码

sensor.h

#ifndef SENSOR_H
#define SENSOR_H

typedef struct {
    float temperature;
    float humidity;
} sensor_data_t;

sensor_data_t read_sensor(void);

#endif

sensor.c

#include "sensor.h"
#include <stdio.h>

sensor_data_t read_sensor(void) {
    sensor_data_t data;
    // 模拟读取传感器
    data.temperature = 25.6;
    data.humidity = 60.2;
    printf("Sensor read: temp=%.1f, hum=%.1f\n", data.temperature, data.humidity);
    return data;
}

main.c

#include <stdio.h>
#include <unistd.h>
#include "sensor.h"

int main() {
    printf("ARM Sensor Application Starting...\n");

    while(1) {
        sensor_data_t data = read_sensor();
        printf("Temperature: %.1f°C, Humidity: %.1f%%\n",
               data.temperature, data.humidity);
        sleep(2);
    }

    return 0;
}

8. 构建项目

现在开始编译。注意,我们要用之前写的工具链文件:

mkdir build
cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm_toolchain.cmake
make

如果一切顺利,你会看到 build 目录下生成了 sensor_app 可执行文件。

验证一下:

file sensor_app

输出应该类似:

sensor_app: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, not stripped

看到 ARM 字样,说明交叉编译成功了。

注意:如果file命令显示的是x86-64,说明你忘了指定工具链文件,CMake用了宿主机编译器。删掉build目录重新来一遍。

9. 核心流程可视化

为了让你更直观地理解整个交叉编译流程,我画了一张图:

CMake交叉编译ARM应用流程图 宿主机 (x86_64) Ubuntu + CMake 交叉编译工具链 arm-linux-gnueabihf-gcc ARM可执行文件 sensor_app (ELF ARM) 部署到ARM板 scp / nfs / u盘 目标机 (ARM) 运行 sensor_app 关键配置 CMAKE_TOOLCHAIN_FILE 常见问题 链接了宿主机库 → 段错误 验证方法 file命令查看ELF架构

10. 部署到ARM板子上运行

把编译好的 sensor_app 传到ARM板子上。我一般用scp:

scp sensor_app root@192.168.1.100:/root/

然后ssh到板子上,运行:

chmod +x /root/sensor_app
/root/sensor_app

如果一切正常,你会看到:

ARM Sensor Application Starting...
Sensor read: temp=25.6, hum=60.2
Temperature: 25.6°C, Humidity: 60.2%
Sensor read: temp=25.6, hum=60.2
Temperature: 25.6°C, Humidity: 60.2%
...

小技巧:如果板子上没有scp,可以用nfs挂载或者u盘拷贝。我个人习惯在开发阶段用nfs,改完代码直接make,板子上就能看到新程序,省去反复拷贝的麻烦。

11. 常见问题与避坑指南

这里总结几个我实际踩过的坑:

问题 现象 原因 解决方法
编译出来的程序在板子上跑不了 ./sensor_app: not found 动态链接器路径不对 检查工具链的sysroot,或者用静态编译
段错误(Segmentation fault) 程序启动就崩溃 链接了宿主机上的库 严格设置CMAKE_FIND_ROOT_PATH_MODE
编译时报找不到头文件 fatal error: xxx.h: No such file or directory 工具链的sysroot没配置对 检查CMAKE_FIND_ROOT_PATH是否指向正确的路径
file命令显示x86架构 明明用了交叉编译器,但输出是x86 CMake缓存了之前的编译器路径 删除build目录,重新cmake

我曾经在一个项目里,因为没清理build目录,反复编译了十几次,每次都是x86的。后来才发现CMake缓存了第一次的编译器路径。嗯,从那以后我每次换工具链都先rm -rf build。

12. 总结

这个实战项目,说白了就是三件事:

  • 准备工具链:装好arm-linux-gnueabihf-gcc
  • 写工具链文件:告诉CMake用哪套编译器、目标架构是什么
  • 构建并部署:cmake时指定-DCMAKE_TOOLCHAIN_FILE,然后make,最后传到板子上跑

掌握了这个流程,以后换任何ARM芯片——Cortex-A、Cortex-R、甚至RISC-V——你都能举一反三。无非是换一套工具链,改一下工具链文件里的名字而已。

好了,今天的实战就到这里。代码不多,但每一步都是实打实的经验。你可以在自己的开发机上搭一遍,遇到问题很正常,对照上面的表格排查就行。


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