如果你手里有一块香橙派5这种基于RK3588的开发板,又心心念念想在它上面跑YOLOv5s目标检测,那大概率已经翻过不少教程了。翻来翻去你会发现,大量资料一上来都在讲“先交叉编译一个hello程序”。看着这一行hello world,很多人心里都会犯嘀咕:我不是要跑AI模型么?学这玩意儿干嘛?
这其实是整个嵌入式AI落地链条里最容易被低估的一环。RK3588是ARM64架构,跟我们日常用的PC是两套完全不同的指令集,你在电脑上编译出来的程序根本没法直接在板子上执行。想让YOLOv5s后续的C++推理代码在香橙派上跑起来,就必须先在PC上用交叉编译工具链生成ARM架构的可执行文件。这个“hello”练好了,后面编译模型推理demo、跑RKNN接口,全是同一套流程,只是换了个工程而已。
这篇文章就围绕这个第06期主题展开,把交叉编译hello这件事从头到脚拆开讲:为什么要这么做、工具链怎么装、常见错误怎么排查。我不玩虚的,尽量把每一步的“为什么”也讲清楚,你拿这篇文章当操作手册也好,当避坑指南也罢,都能少走不少弯路。
1. 为什么交叉编译是RK3588部署YOLOv5s的第一课
1.1 先梳理YOLOv5s部署全流程里交叉编译的位置
很多人以为在RK3588上跑YOLOv5s,就是把Python训练好的权重文件拷到板子上,然后pip install一堆包就行了。真这样做过一次就会知道,这条路在嵌入式设备上几乎走不通。RK3588虽然有6TOPS算力的NPU,但要让模型真正跑到NPU上,得先把PyTorch模型转换成RKNN格式,再通过C/C++的RKNN API写推理代码调用它。而这个推理程序本身,是在PC上编译好再放到板子上跑的。
这里就出现了一个关键矛盾:PC是x86_64架构,香橙派是aarch64架构,两者不能直接通用。所以整个部署流程里,凡是要生成可执行文件的操作,几乎都依赖于交叉编译。rknn_model_zoo里的各种C/C++例子、你自己写的yolov5s后处理代码,全都要先过交叉编译这一关,编出ARM版本才能在板端运行。
这一期做hello,本质上就是一次“工具链体检”。如果连一个hello程序都不能通过交叉编译跑到板上,那后面编译带一堆依赖的工程代码时,你会完全分不清问题是出在代码上、CMake配置上,还是工具链上。先拿hello把编译、传输、运行这条链路打通,后面所有工作都会顺很多。
1.2 为什么不直接在板子上用gcc编译
可能有人会说,RK3588好歹是8核处理器,直接ssh到板子上编译不就行了?实测下来,简单的程序确实能编,但一碰上真实项目就会很难受。
首先,RK3588的性能跟一台普通PC还是有差距的,尤其是I/O和散热。编译一个中等规模的C++工程,板子上可能要几分钟甚至十几分钟,期间CPU一直满负荷,功耗堆上来以后散热片都是烫手的。其次,板子上的Linux系统很多精简过的,gcc、make、各种依赖库的dev包未必齐全,临时装一套构建环境既麻烦又占空间。更要命的是,如果你后续要编译OpenCV、RKNN Toolkit依赖这类大体积库,板载编译基本等于自虐。
交叉编译的思路其实很好理解:把“编译”这个又重又慢的活留在PC上干,PC内存大、磁盘快、工具链想装什么装什么;编出来的ARM可执行文件再传到板子上“跑”。打个比方,你在一台工厂里排版印刷书籍,书印刷出来后运到各个书店去卖,而不是把整条印刷线搬到书店里。我们需要的只是印刷出来的“成品”,也就是能直接被ARM平台执行的文件。
2. 交叉编译原理与工具链选型
2.1 交叉编译的三板斧到底指什么
一套完整的交叉编译工具链,其实包含三大部分:编译器(GCC)、汇编器与链接器(binutils)、目标平台的C/C++运行库(典型的就是glibc)。编译器负责把C代码翻译成汇编,binutils里的as、ld负责把汇编变成机器指令并链接成可执行文件,glibc则是程序运行时跟系统交互的基础库。
一个新手最容易绕晕的点是:为什么在x86_64电脑上装一个“aarch64-linux-gnu-gcc”,编出来的程序就能发给ARM设备运行了?这里的关键在于,交叉编译指的是“生成的目标代码架构”和“运行编译工具的主机架构”不同。编译器程序本身运行在x86上,但通过内置的target配置,它最终生成的机器码是ARM64指令集。你不需要在家里摆一台ARM服务器来编译,只要工具链选对了,目标架构就对了。
这也引出另一个重要问题:目标板的运行库版本。交叉编译时链接用的是工具链自带的aarch64版glibc,如果这个库的版本比板端系统里的glibc还要新,程序跑到板子上就会报“GLIBC_xx not found”。所以挑工具链时,不光看能不能编译,还得留意版本匹配度。
2.2 三条路线获取aarch64工具链
在PC上拿一套能用的aarch64工具链,主流有这么三条路:
| 路线 | 典型来源 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 发行版软件源直接安装 | sudo apt install gcc-aarch64-linux-gnu | 安装简单、依赖自动处理 | 版本较新,可能比板端glibc高 | hello及中小型项目、学习入门 |
| 瑞芯微SDK自带工具链 | SDK内部prebuilts/gcc目录下的aarch64-buildroot-linux-gnu | 版本与官方板端系统匹配度好 | 需要先拉取完整SDK,体积大 | 正式项目、RKNN推理工程编译 |
| Buildroot自编译工具链 | 自己用Buildroot配置生成 | 版本完全可控,可定制 | 配置复杂、编译耗时长 | 定制rootfs、量产固件场景 |
新手阶段,我建议直接用系统源里的gcc-aarch64-linux-gnu。它安装很省事,装完就能用,对hello这种小工程完全够。等真正开始编译RKNN相关的C++项目了,再切到官方SDK自带的工具链,到时候你会发现很多官方demo的Makefile里都已经默认写好了SDK工具链的路径,你只需要保证路径存在就行。
2.3 动态编译与静态编译的选择差异
工具链装好后,编译时还有一个很重要的策略选择:动态编译还是静态编译。
默认情况下,gcc是动态编译的,生成的可执行文件很小,但它依赖目标板系统里的glibc动态库。比如一个hello,动态编译出来大概20KB左右,放到板子上能不能跑,取决于板端有没有对应版本的/lib/aarch64-linux-gnu/libc.so.6。大多数完整版Debian、Ubuntu系统都有,所以一般也能跑。但如果你在PC上用了特别新的工具链,板端系统又比较老,一运行就报GLIBC版本不足。
静态编译则是用-static参数,把程序依赖的库函数全部打包进可执行文件里。这样编出来的hello体积会有700KB到1MB,但好处是什么呢?它基本不再依赖板端的任何库,拷过去就能跑,对系统版本几乎免疫。做hello阶段,我特别建议用静态编译,这样可以把变量因素降到最低:只要工具链本身没问题,这个文件在任何ARM64 Linux上都能运行。等后面编译调用RKNN API的程序时再切回动态编译,因为RKNN的lib还需要链到板端的驱动库上。
3. 环境准备与工具链安装实战
3.1 编译主机的选择:Win下用WSL还是搞虚拟机
交叉编译第一步,是要有一台能装Linux工具链的主机。如果你本身就是用Linux桌面,那就省事了。如果主力机是Windows,有两个常见选择:WSL2和VMware虚拟机。
我个人推荐WSL2,理由很简单:启动快、资源占用低、跟Windows文件系统互操作方便。安装WSL2后装个Ubuntu 22.04,然后在里面装工具链,流程跟在Linux实体机上完全一样。VMware也不是不行,但每次启动要等完整开机流程,共享文件得配VMware Tools,交互起来没那么顺手。
需要注意的一个小点:WSL2里访问Windows盘文件走的是/mnt路径,跨文件系统读写性能比较差。所以我建议整个编译工程都放在WSL的Linux文件系统里,比如 ~/rk3588_work,不要放到 /mnt/d 下面去编译,否则大工程构建时会明显感觉到磁盘I/O拖慢。
3.2 安装工具链并做首轮快速验证
主机准备好后,安装工具链其实就两条命令的事:
sudo apt update sudo apt install -y build-essential gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu如果你后续要编译C++工程,顺手把C++交叉编译器和标准库也装上:
sudo apt install -y g++-aarch64-linux-gnu装完先别急着写hello,先确认编译器版本和默认目标架构:
aarch64-linux-gnu-gcc -v看到输出里有Target: aarch64-linux-gnu这一行,说明装对了。接着写一个最简的验证程序,并不需要真正“运行”,只要确认它生成了ARM64的机器码:
echo 'int main(){return 0;}' > /tmp/test.c aarch64-linux-gnu-gcc /tmp/test.c -o /tmp/test file /tmp/testfile命令会告诉你这个可执行文件的真实架构。如果输出是ELF 64-bit LSB executable, ARM aarch64,那说明工具链工作正常;如果显示的是x86-64,说明你大概率调用了本机gcc而不是交叉编译器。
3.3 三个必会的ELF查看工具
交叉编译后,验证产物是否正常,靠的就是这几个工具:
file:最直观,一条命令看架构、动态静态、调试信息等。aarch64-linux-gnu-readelf:交叉工具链自带的readelf,可查看ELF头、程序段、依赖的动态库。排查链接问题比file深入得多。aarch64-linux-gnu-objdump:反汇编用的,程序崩了或者想确认机器码指令时非常管用。
强调一个小习惯:平时在PC上看自己编的x86程序,用readelf、objdump是用本机的;现在编ARM程序,最好养成用带aarch64-linux-gnu-前缀的习惯。如果你用本机的ldd去查看一个ARM可执行文件,通常会报错或显示“not a dynamic executable”,这不是程序有问题,而是你用错了工具。想看ARM程序的动态库依赖,要用aarch64-linux-gnu-readelf -d或者直接看板子上的ldd。
4. 手把手把hello程序跑在香橙派上
4.1 编写源码与Makefile
先把源码建出来,比如在~/rk3588_work/hello目录下新建hello.c:
#include <stdio.h> int main(void) { printf("Hello from RK3588!\n"); return 0; }这段代码没有任何平台相关特性,每条C编译器都能编。但也正因为它写起来毫无难度,很多人会忽略Makefile的意义。
在嵌入式里,Makefile写得好不好,直接影响后面移植工程的效率。我给出一个兼顾扩展性的Makefile:
CROSS_COMPILE ?= aarch64-linux-gnu- CC := $(CROSS_COMPILE)gcc TARGET := hello SRCS := hello.c all: $(TARGET) $(TARGET): $(SRCS) $(CC) -o $@ $^ -static clean: rm -f $(TARGET)这里的关键就在第一行:CROSS_COMPILE ?= aarch64-linux-gnu-。?=的意思是,如果用户在命令行里没指定这个变量,就用后面的默认值。比如你后面切换到瑞芯微SDK自带的工具链时,可以直接这样:
make CROSS_COMPILE=../rk3588_sdk/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-不用改任何Makefile,工具链就换了。这种设计在瑞芯微官方的很多demo工程里大量存在,你现在把习惯养好,后面会非常受用。
4.2 编译、传输与板上运行
在hello目录下执行:
make编译完成后,先看一眼产物类型:
file hello应当输出类似hello: ELF 64-bit LSB executable, ARM aarch64, statically linked, not stripped的信息。看到statically linked就说明我们前面设置的-static参数生效了。
接下来把文件传到香橙派上。如果你板子已经连着局域网并且开了SSH,一条scp命令就行:
scp hello orangepi@192.168.x.x:/home/orangepi/如果没有网络,简单粗暴的办法是拷贝到U盘再插到板上。注意U盘挂载后通常默认不允许直接执行二进制文件,需要拷贝到本地再运行,比如:
sudo cp /media/usb0/hello /home/orangepi/ sudo chmod +x /home/orangepi/hello cd /home/orangepi && ./hello正常情况下你会看到终端打印出Hello from RK3588!。到这,整条交叉编译链路就算正式跑通了。
4.3 完整验证清单
运行成功后,我建议再花一分钟把整个链路自查一遍:
| 检查项 | 命令 | 预期结果 |
|---|---|---|
| 编译器是否交叉编译 | file hello | 显示ARM aarch64架构 |
| 是否静态链接 | file hello | 显示statically linked |
| 文件是否完整传输 | md5sum hello(两边对比) | 哈希值一致 |
| 板端是否可执行 | 直接./hello | 打印Hello信息 |
| 板端架构确认 | 板上执行uname -m | 输出aarch64 |
对比PC端和板端的md5值可以排除传输损坏的坑。很多奇怪问题,比如拷贝过程中文件被截断,或者U盘文件系统有毛病,都能用这招快速定位。
5. 常见问题与排查技巧实录
5.1 新手最容易踩的五个坑速查表
交叉编译hello这个看似简单的流程,实际操作中翻车的人真不少。我把这些年见过的典型报错整理成一个速查表:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
cannot execute binary file: Exec format error | 编译工具用错,生成了x86_64文件 | 检查Makefile里用的是不是aarch64-linux-gnu-gcc,并用file验证架构 |
version GLIBC_2.34 not found | 工具链自带glibc版本比板端系统高 | 改用静态编译;或换版本匹配的SDK工具链 |
/lib/ld-linux-aarch64.so.1: No such file or directory | 动态链接文件在精简系统上缺少动态加载器 | 静态编译,或者把板端glibc补齐 |
Permission denied | 文件没有执行权限 | chmod +x hello,注意U盘挂载目录通常不允许直接执行 |
串口连接时提示cannot receive hello packet | 串口参数、线序或USB转接板供电问题 | 检查波特率、数据位/停止位配置,换根线或换个USB口再试 |
最后一项看起来跟hello程序本身没关系,但实际调试设备时非常常见。很多人把程序跑在板子上,想通过串口看输出,结果上位机一直报收不到数据包,就以为是程序挂了。排查顺序永远是:先确认物理连接和串口参数,再怀疑程序逻辑。
5.2 从实战里总结的几个小技巧
这个hello项目虽然简单,但有几个习惯如果从现在就养成,能帮你躲掉后面很多麻烦。
第一个小技巧:把静态编译的hello文件当成“工具链健康标尺”。我自己的习惯是,每次新拿到一块RK3588/RK3568板子,先交叉编译一个静态hello,传到/root/下跑一遍。如果这个能跑,说明工具链和传输链路都没问题,再遇到别的程序跑不起来,我就只需要往代码和库依赖方向排查,省掉一大半无效调试时间。
第二个小技巧:Makefile里务必保留CROSS_COMPILE和CC这些变量分离的习惯。后面你用瑞芯微SDK里的rknn_model_zoo时,会发现官方大量工程的编译方式就是“设置工具链路径->make”,如果你自己写的工程早就习惯这种结构,迁移会非常丝滑。
第三个小技巧:别小看aarch64-linux-gnu-objdump -D hello这条命令。如果你的程序编译出来在板子上段错误了,先用PC上的反汇编看一眼指令,很多时候问题一眼就能定位到是寄存器操作还是跳转地址出了问题,比你在板子上疯狂加printf高效太多。
我个人在实际操作中的体会是:交叉编译hello这件事,本质上是把整个嵌入式开发流程中最容易出错的三个环节——架构、链接、传输——提前用最简化的方式演练了一遍。这个流程通了,后面跑YOLOv5s的C++部署代码就有底气了。而且这套流程不只适用香橙派RK3588,你手头换成其他ARM64板卡,甚至Windows下做Linux开发,思路都是一毛一样的。先把这个基本功练扎实,下一节我们可以聊RKNN模型转换和NPU推理了。