项目实战二:使用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. 核心流程可视化
为了让你更直观地理解整个交叉编译流程,我画了一张图:
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——你都能举一反三。无非是换一套工具链,改一下工具链文件里的名字而已。
好了,今天的实战就到这里。代码不多,但每一步都是实打实的经验。你可以在自己的开发机上搭一遍,遇到问题很正常,对照上面的表格排查就行。