拿到香橙派5这台板子的第一周,我干了一件不怎么明智的事:直接在板子上去编译程序。结果就是8GB内存被吃到见底,风扇呼呼转,一个很简单的apt包等上十几分钟。说句实话,都上了RK3588这颗芯片,还在板端折腾编译工具链,等于放着A76大核的性能优势不用,非要在A55小核上玩耐力赛——正确的姿势应该是交叉编译。
这个系列走到第6篇,前面几篇已经把板子从开箱、烧录到系统跑通了,今天做一件特别小但特别关键的事:在PC上用交叉编译工具链编译一个hello world,然后把它通过scp传到香橙派5上跑起来。别小看这一步,这是整个RK3588+yolov5s部署之旅的第一块基石。工具链通不通、链接器路径对不对、动态链接库能不能对上,全在这个hello里暴露出来。后面你要交叉编译OpenCV、ncnn、RKNN Toolkit这些大工程,靠的都是今天验证过的那套编译器环境。
这篇就按我实际操作的完整过程来写,从为什么必须交叉编译,到工具链怎么拿、CMake怎么配、坑怎么避,一步步来。
1. 为什么在RK3588上跑yolov5s前,要先用hello world磨刀
1.1 板载编译的痛点:内存、功耗和时间成本
先说说为什么非要把交叉编译这件事单独拎出来讲。很多人在树莓派或者香橙派上做开发,习惯就是直接在板子上装gcc、装pip、然后原地编译。在ARMv7时代的树莓派上这么做还勉强能忍,但到了RK3588这个级别,你要是还走板载编译的老路,就会遇到几个特别现实的问题。
第一是内存压力。RK3588的丐版开发板通常是8GB内存,听起来不小,但你要是在板子上交叉编译一个带依赖的C++工程,比如先编OpenCV、再编ncnn、再编RKNN的runtime,编译器进程一多,8GB根本不够看,系统随时可能触发OOM,直接卡死或者杀掉编译进程。
第二是磁盘开销。开发板上跑的是精简版Ubuntu/Debian系统,eMMC或者TF卡的容量往往就32GB、64GB。而编译OpenCV这类大型库,中间产物动辄几个GB,装一套完整的编译工具链加依赖,磁盘空间很快就见底了。而PC上你随便划一个目录,几百GB空间随便造。
第三是时间成本。RK3588虽然是8核架构(4xA76 + 4xA55),但板载编译的时钟频率和散热条件都受限制,实际编译效率远不如一台普通的x86_64桌面CPU。我实测过,同样一份代码,PC上30秒编完,板子上可能要跑三四分钟。放到大型工程上,这个差距是灾难性的。
所以,交叉编译是RK3588上做AI部署项目的必经之路。你在PC上用aarch64目标的编译器,直接生成ARM64架构的程序,然后通过局域网传到板子上运行。整个过程行云流水,主机的高性能得到了利用,板子只管运行,不用承担编译的重活。
1.2 hello world在这个链条里扮演的角色
有人可能会问:交叉编译个hello world,有什么好讲的?网上搜一下,到处都是教程,不就是一条gcc命令吗?
但其实你如果把"手把手从零部署yolov5s到RK3588"作为一个目标,那这个hello world的意义就完全不一样了。它不仅仅是为了打印一行字符串,而是为了验证一整条"编译-链接-打包-传输-运行"链路:
- 交叉编译工具链是否可用、是否与目标平台架构匹配(x86_64编译出来的程序在AArch64上用不了,反过来也一样)。
- 动态链接器的路径配置是否正确。ARM64 Linux程序运行时,内核会按ELF文件头里记录的interpreter路径去寻找动态链接器,默认是
/lib/ld-linux-aarch64.so.1。如果这个路径在板子上不存在,程序会直接报"No such file or directory"。 - 从PC到板子的文件传输通道是否通畅。后面你编译出来的可执行文件、模型文件、动态库、打包的脚本,都要走这条路。
- 你的CMake交叉编译工程结构是否为后续大型项目做好了铺垫。
换句话说,hello是"最小可行性验证"。你在这一步花半小时把工具链调通,后面面对OpenCV、ncnn、RKNN这些重量级依赖时,只是往这个框架里填更多库而已;你如果连hello都没跑通,后面就算是把yolov5s的源码拿到手,编出来的程序也一定跑不起来,到时候排错范围就大到无从下手了。
所以在教程这个位置放一个交叉编译的hello,其实是用最小的代价逼出最大的确定性。
2. 准备工作:主机环境、板子网络与工具链的三种获取路线
2.1 主机侧需要提前装好的东西
做交叉编译,你需要的是一台Linux x86_64的电脑。我用的主力机是Ubuntu 22.04 LTS,如果你用的是Windows,建议装一个WSL2或者直接用虚拟机跑Ubuntu,后面所有命令都在这个Linux环境里执行。
在开始之前,先确认主机上有几个基础工具:
sudo apt update sudo apt install -y build-essential cmake git ssh net-toolsbuild-essential会带来gcc、g++、make这些基础编译工具链;cmake用于后续构建大型工程;ssh和net-tools分别用于远程连接和查看网络状态。
这里有个小提醒:最好先把主机的CMake版本确认一下,新版的RKNN、ncnn工具链对CMake有最低版本要求。我在做的过程中就吃过亏,Ubuntu 20.04自带的CMake是3.16,结果后面构建一个依赖库时报了最低3.20的错误,最后只能手动装了新版。你可以在动手之前执行:
cmake --version如果低于3.20,推荐去CMake官网下载安装包,别用apt装的老版本,省得后面返工。
2.2 板子侧:确认系统架构和网络连通
把香橙派5开机,进入系统后先确认两件事:
uname -m输出应该是aarch64,这就是ARM64架构的标志。如果看到别的输出,说明你当前系统跑的不是64位内核,需要重刷64位系统镜像。
再看一次IP地址,后面传输文件要用:
ip addr记下eth0或wlan0那段的IP,比如192.168.1.100。
然后回到PC上,测试一下到板子的连通性:
ping 192.168.1.100 -c 4能Ping通就说明二层网络没问题。接下来用SCP测试文件传输通道:
ssh orangepi@192.168.1.100默认密码在官方wiki里,一般是orangepi。登录成功后,建议加一条SSH Key信任关系,后面scp就不用每次输密码了。这个细节在后续频繁传文件时非常省事。
2.3 交叉编译工具链的三种来源,我推荐哪种
RK3588用的工具链是基于GCC的aarch64交叉编译版本,目标平台是ARM64 Linux。获取渠道大致有三条:
第一条:瑞芯微SDK自带的工具链。如果你之前下载过Rockchip官方的SDK(比如Linux SDK),里面一般会内置一套完整的prebuilts/gcc/linux-x86/aarch64交叉编译器。这套工具链经过瑞芯微自家的调校,兼容性不错,版本也比较稳定,适合从零开始就跟着SDK走的用户。
第二条:直接从网上下载Linaro版的工具链安装包。Linaro是ARM生态里的专业组织,发布的gcc-aarch64工具链在嵌入式圈子里用得非常广。我自己用的就是这条路线。解压完不需要安装,直接导出一个环境变量指向工具链目录就行。具体下载地址在Linaro官方发布页选择aarch64-linux-gnu的版本(带-linux-gnu的是面向Linux的),选择带有"aarch64-linux-gnu"前缀的压缩包即可。
第三条:Ubuntu发行版的软件仓库。执行sudo apt install gcc-aarch64-linux-gnu也能装上,但版本通常比上面两条路线老一些,而且仓库里的工具链有时候和瑞芯微提供的固件依赖库之间存在细微的ABI差异。如果只是简单的hello world,用仓库版问题不大;但后面积累到大型工程时,A/B选型上的差异会突然冒出来。
我实际采用的组合拳是:以Linaro工具链为主,把工具链解压到~/toolchains目录,写一个环境变量脚本,每次开终端时一句话加载。这样既能控制版本,也不会污染系统。
2.4 安装目录布局
我的PC上目录规划是这样的:
~/rk3588_tools/ ├── toolchain # 交叉编译工具链 │ ├── bin │ ├── aarch64-linux-gnu │ └── lib ├── my_yolo_workspace # 后续放yolov5s工程 └── hello_project # 今天的hello world工具链解压之后,bin目录下关键是这三个可执行文件:
aarch64-linux-gnu-gcc:C编译器aarch64-linux-gnu-g++:C++编译器aarch64-linux-gnu-objdump/aarch64-linux-gnu-readelf:检测工具,后面排查问题全靠它们
先在终端执行一次验证:
export PATH=$PATH:~/rk3588_tools/toolchain/bin aarch64-linux-gnu-gcc --version如果能看到版本号输出,说明工具链本体OK。
3. hello项目结构、CMakeLists与toolchain.cmake的落地写法
3.1 为什么这个项目必须用CMake,而不是一条gcc命令
很多初学者会想:一个hello world,直接写
aarch64-linux-gnu-gcc main.c -o hello一条命令搞定的事,干嘛非要上CMake?
这个判断逻辑放在单独做hello这个点上确实成立,但如果你把目光放到"RK3588部署yolov5s"这个更大目标上,就会发现,后续所有依赖库(ncnn、OpenCV、RKNN Toolkit、protobuf)的构建系统全部是CMake的生态。如果你现在只图省事用裸命令行跑通hello,那等于把"最小验证"这件事限定在了编译器的层面,而你需要的是整个工程构建链路的验证。
所以我建议从hello开始就直接用CMake组织工程,把交叉编译的工具链文件(toolchain.cmake)搭起来。后面每年要编译的大工程,只需要在这个toolchain.cmake基础上增加依赖库的搜索路径,就能无缝复用。
3.2 工程结构和main.cpp内容
先建目录和文件:
mkdir -p ~/rk3588_tools/hello_project cd ~/rk3588_tools/hello_project创建main.cpp:
#include <iostream> int main() { std::cout << "Hello, RK3588!" << std::endl; #ifdef __aarch64__ std::cout << "Architecture: AArch64" << std::endl; #else std::cout << "Architecture: unknown (not aarch64)" << std::endl; #endif std::cout << "Hello from cross-compile." << std::endl; return 0; }这里我故意加了__aarch64__宏判断,目的是让运行时能直观确认编译产物确实是ARM64版本。如果这个宏没有定义,说明你编译出来的是主机x86架构的程序,那样的话后面跑在板子上一定会报错。
3.3 toolchain.cmake的内容拆解
创建toolchain.cmake:
set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR aarch64) set(CROSS_COMPILE_PATH "$ENV{HOME}/rk3588_tools/toolchain/bin/aarch64-linux-gnu-") set(CMAKE_C_COMPILER ${CROSS_COMPILE_PATH}gcc) set(CMAKE_CXX_COMPILER ${CROSS_COMPILE_PATH}g++) set(CMAKE_STAGING_PREFIX "$ENV{HOME}/rk3588_tools/sysroot/staging") set(CMAKE_FIND_ROOT_PATH "$ENV{HOME}/rk3588_tools/sysroot") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)这几个配置项分别是什么意思,拆开来讲:
CMAKE_SYSTEM_NAME设置为Linux,告诉CMake目标系统不是Windows或其他平台;CMAKE_SYSTEM_PROCESSOR设置为aarch64,则明确目标CPU架构。
CMAKE_C_COMPILER和CMAKE_CXX_COMPILER指向了工具链bin目录下的交叉编译器。注意我用了前缀变量,后面如果在同一个工具链里还有aarch64-linux-gnu-ar、aarch64-linux-gnu-strip等工具,也可以在CMake里通过同样方式引用。
CMAKE_STAGING_PREFIX是一个很重要的概念。交叉编译的时候,你下载、编译的依赖库通常会安装到一个"暂存目录"——比如你自己编译的OpenCV安装到这里,将来板子上的程序再去这个目录里找头文件和库文件。这个目录不一定是板子的真实根目录,但后续大型工程的依赖管理全靠它。
CMAKE_FIND_ROOT_PATH是交叉编译时搜索头文件和库文件的最关键路径。如果你的main.cpp里还有#include <opencv2/opencv.hpp>,CMake就需要判断去哪里找这个头文件。设定了Root Path以后,CMake会在你给的这几个路径下寻找。
CMAKE_FIND_ROOT_PATH_MODE_PROGRAM设成NEVER,意思是CMake在查找程序(比如编译工具)时,不会把Root Path当成程序搜索路径去处理;MODE_LIBRARY和MODE_INCLUDE设成ONLY,则是告诉CMake在查找库文件和头文件时,只在这几个Root Path指定的范围内找,不要跑到主机系统的/usr/include和/usr/lib里去瞎逛。
为什么这么设?因为在交叉编译时,如果你不小心让CMake找到了主机的x86版本依赖库,链接阶段会出现格式不匹配的报错,而且特别难排查。把Root Path严格限定住,就是给这个依赖搜索加上了"铁笼子"。
3.4 CMakeLists.txt写法
创建CMakeLists.txt:
cmake_minimum_required(VERSION 3.16) project(hello_rk3588 CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_executable(hello main.cpp)对于hello来说,CMakeLists非常简单。核心就是add_executable(hello main.cpp)这句话,指定生成目标的名字为hello。
接下来在工程目录下新建一个build目录,作为构建目录:
mkdir build cd build然后执行CMake配置:
cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake注意,-DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake这个参数一定要在cmake ..里指定。如果把这一行漏了,CMake默认会在主机上用x86编译器,所有努力都白费。
再执行编译:
make -j$(nproc)如果你按上面的步骤操作,应该会在build目录下看到生成的hello文件。
4. 编译、传输与板端运行:从file命令到ldd的全链路验证
4.1 用file命令确认编译产物是ARM64格式
这是交叉编译后第一个必须要做的验证。在你自己的电脑上,执行:
file build/hello如果一切正常,输出末尾应该是类似这样的内容:
ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0...这里面的关键信息是:
ELF 64-bit LSB executable:它是一个64位Linux可执行文件。ARM aarch64:目标架构是ARM64,和开发板一致。dynamically linked:动态链接,意味着运行时光需要依赖系统里的动态链接器和动态库。interpreter /lib/ld-linux-aarch64.so.1:程序运行时,内核会加载这个动态链接器。这个路径在板子的根文件系统里必须真实存在。
如果file输出显示的是x86-64,别犹豫,一定是你命令写错了或者CMake配置没生效。这时候不要继续往下走,先把工具链路径和CMake配置检查一遍。
4.2 传送到RK3588板子
确认把构建结果准备好之后,切换到板子所在的网络环境(局域网),用scp发送:
scp build/hello orangepi@192.168.1.100:/tmp/hello传输完成后,登录板子:
ssh orangepi@192.168.1.100进入目录后,给文件加执行权限:
cd /tmp chmod +x hello ./hello如果你的整个链路是通的,你会在板子终端上看到:
Hello, RK3588! Architecture: AArch64 Hello from cross-compile.当“Architecture: AArch64”这一行出现在板子屏幕上时,你的交叉编译环境就已经完全打通了。
4.3 如果运行报错,用readelf和ldd快速定位
哪怕报错也别慌,用工具去拆解。
比如你报bash: ./hello: No such file or directory,其实文件是存在的,那就要怀疑动态链接器路径对不对。在PC上执行:
aarch64-linux-gnu-readelf -l build/hello | grep interpreter查看输出里的动态链接器路径。如果写的是/lib/ld-linux-aarch64.so.1,而板子的ls -l /lib/ld-linux-aarch64.so.1不存在,那就说明板子系统缺少ARM64的基础根文件系统组件,这通常出现在精简过的极简系统镜像上。
如果你想在PC上检查程序依赖了哪些动态库,可以用:
aarch64-linux-gnu-objdump -p build/hello | grep NEEDED输出会列出libstdc++.so.6、libc.so.6等等。这些库必须能在板子的/lib/aarch64-linux-gnu目录下找到。如果某条依赖缺失,把对应库从PC的工具链sysroot里拷贝到板子上,或者改用静态链接的方式重新编译。
5. 第一次踩坑实录:权限、链接器路径与架构不匹配的典型表现
5.1 壳子套错了:architecture mismatch
这是最典型的交叉编译新手坑,具体表现是:你在PC上编译出了可执行文件,也scp到了板子上,一运行就报:
-bash: ./hello: cannot execute binary file: Exec format error出现这条错误,99%的情况是编译出来的可执行文件不是你板子对应的架构。用file看一眼:
file hello如果显示的是ELF 64-bit LSB executable, x86-64,那就说明CMake根本没走交叉编译配置——你八成漏了-DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake,或者CMake缓存里残留了主机编译器信息。
遇到过这种情况后,建议删除build目录重来:
rm -rf build mkdir build && cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=../toolchain.cmake不要试图在一个已有缓存里强行切换编译器,太多老状态会导致CMake内部缓存混乱。删干净是最快的解决办法。
5.2 文件存在却报No such file or directory
这是一个让人摸不着头脑的报错。你明明用ls看到了文件,执行时却告诉你"No such file or directory"。这类问题的根因往往不在文件本身,而在"动态链接器"。
交叉编译产出的动态可执行文件,需要由内核根据ELF文件头里的interpreter字段去加载特定的动态链接器。ARM64版程序默认的链接器路径是/lib/ld-linux-aarch64.so.1。如果你的板子系统很精简,或者根文件系统被裁剪过,这个文件可能不存在,内核就找不到动态链接器,于是抛出一个误导性的"No such file or directory"。
排查方法很简单,在板子上执行:
ls -l /lib/ld-linux-aarch64.so.1如果确实没有,你可以从PC的工具链sysroot目录里找到这个文件,scp到板子对应路径下,或者用符号链接把它补上:
sudo ln -s /lib/aarch64-linux-gnu/ld-linux-aarch64.so.1 /lib/ld-linux-aarch64.so.1如果不想依赖板子上的动态库,还有一个选项:在编译时加-static参数,做全静态链接,这样程序不再依赖动态链接器,但二进制文件体积会大不少,而且后续yolo工程里的大库全静态链接并不现实,所以这里还是推荐把动态链接器路径理顺。
5.3 运行时提示缺少libstdc++等动态库
如果你发现hello已经成功开始执行,却在某一行输出后中断,或者直接弹出类似于:
error while loading shared libraries: libstdc++.so.6: cannot open shared object file: No such file or directory说明程序运行依赖的C++标准库在板子上没找到。一般板上的Ubuntu系统会自带的,但如果用极简镜像就可能缺。解决办法有几个:
- 在板子上安装对应软件包:
sudo apt update && sudo apt install -y libstdc++6,同时确认系统基础库完整。 - 从工具链的sysroot目录手动拷贝对应
libstdc++.so.6到/lib/aarch64-linux-gnu/。 - 在编译时配置
CMAKE_EXE_LINKER_FLAGS强制静态链接libstdc++和高版本libgcc,比如-static-libgcc -static-libstdc++,但整个工程里OpenCV、ncnn这类库还是动态链接为主,这种方法只能救急。
5.4 板子上没有联网的尴尬
RK3588的开发板如果处于离线环境——比如内网隔离、没插网线、Wi-Fi没配起来——那连scp传输这一关都过不去。这种时候我也会用U盘拷贝,把编译好的可执行文件或整个依赖目录拷到U盘,再插到板子的USB口上。虽然麻烦,但对比在板子上现场编译,还是省时省力得多。在离线场景下,建议在PC上提前把后面要依赖的所有库交叉编译安装到sysroot,然后把整个sysroot打包复制过去,一次搞定。
6. 从hello到yolov5s:这套工具链接下来怎么用
6.1 hello验证了哪些能力
当hello在香橙派5上成功打印出“AArch64”时,你手里拥有的是一套经过真实验证的交叉编译基础设施,其中包括:
- 可用的aarch64交叉编译工具链,已经过hello的编译和运行双重测试。
- 一套可复用的CMake工具链文件,明确指定了编译器、sysroot、暂存路径,后续NCNN、OpenCV、RKNN这些库的构建都能基于它扩展。
- 一套已经跑通的PC→板卡文件传输链路,后面模型文件、动态库、脚本都会走这条路。
- 一套排错方法论:遇到二进制跑不起来,先
file、再readelf、再确认链接器路径,这个顺序在后续更复杂的工程里能帮你快速收敛问题。
6.2 下一步:交叉编译依赖库时,sysroot和staging怎么演变
hello项目里没有第三方依赖,所以sysroot还只是个空壳。等你要把yolov5s的推理程序跑起来,你会需要:
- OpenCV:图像预处理、可视化都依赖它。交叉编译OpenCV时,要把FFmpeg、JPEG、PNG等依赖也一起交叉编译,并安装到同一个staging目录。
- ncnn:如果你想在RK3588上用ncnn框架跑yolov5s,ncnn本身就提供了一套比较完善的CMake交叉编译支持,你只要把它的
toolchain.cmake替换成我们自己的配置,或者通过-DCMAKE_TOOLCHAIN_FILE指向同一个文件即可。 - RKNN Toolkit:如果走瑞芯微NPU路线,还需要编译librknnrt.so,并把它放到板子上;PC端还需要跑模型转换工具。这里的交叉编译思路和hello完全一样,只是要同时管理PC端和板端两套产物。
- protobuf:如果你用RKNN去解析模型配置,经常需要交叉编译protobuf,然后把它安装到staging目录。
在这些工程里尽量保持同一个staging目录,比如~/rk3588_tools/sysroot/staging。这样后续你写程序头文件搜索路径时,只需要告诉CMake去staging里找即可。依赖库统一、版本可控,后续踩的坑会少非常多。
6.3 板子上调试yolov5s程序时的经验
交叉编译好的yolo推理程序传到板子上,运行时如果发现某些库找不到,别再回PC上瞎试。推荐先ldd hello(如果你交叉编译了一个带运行场景的demo,可以直接对demo执行ldd)看看缺哪些动态库。注意,板子系统里未必有完整的aarch64版ldd工具,没有就在PC上用交叉编译工具链里的aarch64-linux-gnu-objdump代替。
在PC上确认依赖、手动拷库、上板复测,这个循环在我跑yolo的整个过程中重复了无数遍。每次跑通一个库,就把它安装到staging目录并顺手拷贝到板子上,确保两边保持一致。这个过程本身也是交叉编译能力的一次次升级。
6.4 给后续工程预留的坑位
现在的hello_project目录其实已经是一个模板了。后续新开一个工程,直接复制这个目录,替换main.cpp的代码,然后在CMakeLists里增加你需要的target和依赖库,就能立刻进入状态。我习惯的做法是建一个通用的cmake/目录,专门放公共的toolchain.cmake,所有工程通过路径引用它,避免每工程拷贝一份配置,将来改依赖路径时只要改一处就行。
有一件事建议提前做:把工具链的路径写进一个env.sh脚本里,每次新开终端source env.sh就能加载PATH,而不是靠记忆力敲全路径。交叉编译这种东西,路径一旦错,后面全错。
hello只是起手式,但它定义了后面所有复杂度的最小前提。我确信把这段交叉编译链路彻底理顺之后,你在RK3588上交叉编译yolov5s工程时会感觉非常顺滑——因为那时的你已经知道:工具链选对了,一切才有意义。