1. 交叉编译是RK3588开发绕不开的第一关1.1 为什么非要交叉编译不可做香橙派RK3588开发尤其是想跑yolov5s这类目标检测模型的朋友迟早都会撞上“交叉编译”这四个字。我为什么把交叉编译hello这个看起来极其简单的例子放在这个系列教程里单独占一篇因为这是整个RK3588开发链路里最基础、也最容易劝退新人的一道坎。先说一个很多人踩过的坑。不少人第一次拿到香橙派5之类RK3588板卡第一反应是“这不就是一台小电脑嘛”于是直接SSH连上去在板卡上写代码、编代码。如果只是编译个hello或者小脚本确实能跑板卡上的Ubuntu系统装个gcc就行。但是一旦你开始碰yolov5s部署、OpenCV、rknn-toolkit2、ncnn这类稍微重一点的工程在板卡上直接编译的体验会变得非常痛苦。原因很简单。第一板卡内存和CPU虽然有优势但满负荷编译yolo相关依赖库的时候风扇狂转、系统卡顿、磁盘空间被吃光这是所有RK3588用户都经历过的噩梦。第二板卡上装的是Ubuntu系统你安装的编译器版本、依赖库版本可能和你的PC开发环境不一致版本一漂移就很容易出现“PC上跑得好好的板卡上一编译就报错”。第三也是最核心的原因真正做嵌入式部署的工业流程里根本没有“在板卡上现场编译”这个选项产品要量产程序必须在PC端交叉编译成目标平台的二进制文件再统一烧录或分发。交叉编译的出现就是为了解决这个问题。我的PC是x86_64架构香橙派的RK3588芯片是arm64架构两边指令集完全不同。在PC上用普通gcc编译出来的程序直接扔到板卡上是跑不起来的。交叉编译就相当于一个翻译官我在x86的PC上用一套专门生成arm64指令的工具链把源码编译成RK3588能直接执行的二进制文件然后传过去就能跑。1.2 搞清楚你的硬件和系统是哪种组合在动手之前先花点时间确认自己手里的东西是什么。香橙派5系列用的RK3588芯片是瑞芯微的旗舰级SoCCPU部分是4核Cortex-A76加4核Cortex-A55的big.LITTLE架构整体算力在单板计算机里属于第一梯队。这块芯片的架构是arm64所以你在PC上交叉编译的目标平台就是aarch64-linux-gnu。系统方面这个系列教程默认用的是香橙派官方Ubuntu系统我这边用的是Ubuntu 20.04版本的arm64镜像。之所以强调系统版本有一个很实际的原因不同版本的系统自带的glibc版本不同直接影响后面动态编译的程序能不能在板卡上跑。比如你在PC上交叉编译时如果工具链默认指向的glibc版本比板卡系统的glibc版本还新编译出来的程序在板卡上就可能因为找不到GLIBC_2.34这类符号而报错。这一点后面细说。宿主机就是你用来开发和编译的那台PC我建议用Ubuntu 22.04或者20.04如果你是Windows用户也可以用WSL2装Ubuntu。说句实在话做RK3588开发Windows原生环境非常别扭大量工具链和脚本都是面向Linux的与其跟各种模拟器较劲不如老老实实装一个Ubuntu虚拟机或者WSL2后面能省无数的事。2. 环境准备安装aarch64交叉编译工具链2.1 宿主机环境怎么选这里我说一下我自己的环境给大家一个参考。我主力开发机是一台普通的x86_64 PC装的是Ubuntu 22.04 LTS。为什么选22.04而不是20.04因为Ubuntu 22.04的软件源里交叉编译工具链的版本比较新用起来省心。但要注意你在PC上编译出的程序最终要在板卡的Ubuntu 20.04上运行所以我会在编译部分教大家怎么解决glibc版本的兼容问题。如果你是Windows用户请你认真考虑装一个WSL2。WSL2里的Ubuntu环境对RK3588开发来说基本可以当作一个原生Linux来用。我在WSL2里做交叉编译也验证过除了USB直连设备这类场景有点麻烦之外交叉编译本身没有任何问题。2.2 安装工具链交叉编译工具链在Ubuntu的软件源里是现成的不需要去源码编译工具链除非你有极端特殊的需求。装起来非常简单打开终端sudo apt update sudo apt install gcc-aarch64-linux-gnu g-aarch64-linux-gnu如果你是Debian系的老版本系统包名可能会有一点区别但Ubuntu 20.04和22.04上就是这两个包。顺便多说一句gcc是C语言的编译器g是C的我们后面部署yolov5s相关的推理框架时C工程是躲不开的所以这一步就直接把两个都装上。安装完成之后验证一下aarch64-linux-gnu-gcc --version如果能看到类似aarch64-linux-gnu-gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0的输出恭喜你工具链已经就位。看到这里不要急着往下走先想一个问题为什么包名是aarch64-linux-gnu-gcc而不是arm-linux-gnueabihf-gccaarch64是64位ARM架构的官方名字后面的linux-gnu指的是目标平台的操作系统是Linux且C库用的是glibcGNU C Library。与之相对应的还有arm-linux-gnueabihf那是给32位ARM用的。RK3588是64位ARM芯片所以我们必须用aarch64开头的这一套。如果你用错了工具链编译的时候可能不会立刻报错但编译出来的程序放到板卡上会直接告诉你“无法执行”因为指令集根本对不上。2.3 确认工具链和目标平台的关系工具链装好之后我建议大家做一个很简单的检查能帮你理解交叉编译的本质。在PC上执行file aarch64-linux-gnu-gcc正常情况下你会看到这是一个x86-64架构的ELF可执行文件。对你没看错工具链本身是x86的因为它要在你的PC上运行。但它生成的代码却是arm64的。我们再用它随便编译一个空文件试试echo int main(){return 0;} test.c aarch64-linux-gnu-gcc -o test test.c file test这时候你看到的输出就会变成ELF 64-bit LSB executable, ARM aarch64。同一个编译过程编译器自己跑在x86上产出的程序却是给arm64用的这就是交叉编译最直观的解释。这个检查建议每个人都做一遍确认自己的工具链真的能用避免后面几章排查问题的时候怀疑到工具链头上。3. 写一个“不简单”的hello程序3.1 不只是printf让程序带上RK3588的味道既然是hello程序我们当然要从最基础的开始。但我想稍微加一点信息量让这个hello不至于无聊。我们让程序在打印hello的同时输出目标板卡的CPU信息和核心数这样后面部署到香橙派上运行的时候一眼就能确认“这个程序确实跑在RK3588上”而不是跑在一个模拟器或者什么奇怪的环境里。创建一个工程目录我这边命名为hello_rk3588mkdir hello_rk3588 cd hello_rk3588在里面创建源码文件hello_rk3588.c#include stdio.h #include unistd.h #include sys/sysinfo.h int main(void) { printf(Hello, RK3588!\n); printf(This binary was built by cross compiler.\n); printf(Number of processors: %d\n, get_nprocs()); FILE *fp fopen(/proc/cpuinfo, r); if (fp) { char buf[256]; while (fgets(buf, sizeof(buf), fp)) { printf(%s, buf); } fclose(fp); } return 0; }这个源码干了两件事打印基础问候语然后把板卡的/proc/cpuinfo整个打印出来。/proc/cpuinfo是Linux内核暴露的CPU信息接口里面会出现RK3588字样运行的时候就能看到。这里用get_nprocs()拿到的是逻辑核心数RK3588在Linux下通常显示8核看看对不对。为什么不直接写一个最最简单的printf因为后面我们要部署yolov5s那才是一个正经的工程需要读取模型文件、解析图像数据、调用NPU接口。你现在写的hello如果只会在终端打一行字它的信息量太小。加上CPU信息输出你就能学会“程序如何访问Linux系统信息”这一点在调试板卡环境的时候非常有用。3.2 编译命令逐参数拆解代码写好了下面就是关键的一步用交叉编译器编译aarch64-linux-gnu-gcc -o hello_rk3588 hello_rk3588.c我们来逐参数拆解一下这条命令。aarch64-linux-gnu-gcc是交叉编译器前面已经确认过。-o hello_rk3588指定输出文件名默认情况下如果不写-ogcc会生成一个叫a.out的可执行文件那名字太没辨识度了我们好习惯要养成显式指定输出名。最后面的hello_rk3588.c是源码文件。这条命令的完整语义是用面向arm64 Linux的交叉编译器把源码hello_rk3588.c编译链接成名为hello_rk3588的可执行文件。编译完之后还是用file看一眼file hello_rk3588你会看到输出是hello_rk3588: 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, with debug_info, not stripped里面有几个关键信息值得注意。ARM aarch64说明架构对了。dynamically linked说明这是一个动态链接程序它依赖系统里的共享库。interpreter /lib/ld-linux-aarch64.so.1说明这个程序运行时需要一个aarch64版本的动态链接器。这几个信息后面排查问题的时候会反复用到。3.3 静态编译和动态编译怎么选编译hello的时候还有一个值得聊深一点的话题动态编译还是静态编译。上面那条命令默认是动态编译程序依赖的系统库在板卡的Ubuntu镜像里都自带所以不需要额外处理。但交叉编译工程的场景一变就麻烦。想象一下这个情况你的板卡系统是Ubuntu 20.04glibc版本是2.31但你的PC工具链是Ubuntu 22.04带的glibc版本是2.35。你用这套工具链动态编译出来的程序到了板卡上系统找不到2.35版本的glibc符号直接报version GLIBC_2.34 not found。这是非常经典的生产事故。如果你改用静态编译aarch64-linux-gnu-gcc -static -o hello_rk3588_static hello_rk3588.c那么编译器会把所有依赖的库都打包进这个可执行文件里。file一下看输出你会发现statically linked文件体积也会变得比动态编译大很多一个hello可能都有几百KB甚至上MB。静态编译的好处是摆脱了板卡系统库版本的影响拷过去就能跑。坏处是文件体积大、内存占用多而且如果链接第三方库比如后续要用的opencv和rknn静态编译会遇到更多的兼容性问题。我个人的建议是前期做hello、做环境验证的时候用动态编译就可以因为目标板卡是完整的Ubuntu系统不缺基础库。等后面交叉编译yolov5s推理程序、需要链接opencv和rknn的库时优先做好库的交叉编译和版本匹配动态链接就好。如果实在遇到版本冲突再用静态编译作为兜底方案。4. 用Makefile把编译过程固化下来4.1 为什么工程化要趁早很多人在学习阶段会忽略一个习惯编译过程全靠手敲命令。以前我调研过一些新手朋友的做法编译一次hello用一条命令还行但到了yolov5s这种动辄几十个源文件、依赖一堆第三方库的工程里手敲命令完全不可行而且极易出错。你根本无法保证每次输入的命令参数一致更别说要去修改某一个编译选项。我在这个系列教程里坚持一个原则从最小例子开始就上Makefile。把编译过程写进Makefile相当于把你的操作步骤固化成了文档这个文档是机器可执行的。你以后再也不用背编译命令只需要make一下搞定。4.2 一个最小可用的交叉编译Makefile在hello_rk3588目录下创建Makefile内容如下CC : aarch64-linux-gnu-gcc TARGET : hello_rk3588 SRCS : hello_rk3588.c all: $(TARGET) $(TARGET): $(SRCS) $(CC) -o $ $^ clean: rm -f $(TARGET) .PHONY: all clean这个Makefile里最关键的一行是CC : aarch64-linux-gnu-gcc它把编译器变量固定下来以后想切换到板卡原生gcc或者换成别的编译器只需要改这一行。$(TARGET): $(SRCS)表示hello_rk3588这个目标依赖hello_rk3588.c如果源码有更新执行make就会重新编译。$是目标名的简写$^是全部依赖文件的简写所以整条编译命令展开后就是aarch64-linux-gnu-gcc -o hello_rk3588 hello_rk3588.c和手动编译完全一致。然后执行make clean make你会看到终端输出aarch64-linux-gnu-gcc -o hello_rk3588 hello_rk3588.c然后当前目录多了hello_rk3588这个可执行文件。至此你的hello工程已经完成了工程化改造。4.3 多文件工程的交叉编译思路接下来的几篇教程里我们会慢慢进入yolov5s部署的世界工程会从单个.c文件膨胀到很多个文件还会有第三方库的目录。所以我现在就把Makefile的扩展思路讲清楚。假设你的工程有了src/main.c、src/utils.c还有头文件目录inc和第三方库目录lib你的Makefile会长这样CC : aarch64-linux-gnu-gcc TARGET : demo_yolo SRCDIR : src INCDIR : inc LIBDIR : lib CFLAGS : -I$(INCDIR) -O2 -Wall LDFLAGS : -L$(LIBDIR) -lm SRCS : $(wildcard $(SRCDIR)/*.c) OBJS : $(SRCS:.c.o) all: $(TARGET) $(TARGET): $(OBJS) $(CC) -o $ $^ $(LDFLAGS) $(OBJDIR)/%.o: %.c $(CC) $(CFLAGS) -c -o $ $ clean: rm -rf $(OBJS) $(TARGET) .PHONY: all clean这里多说一句wildcard函数的用途它自动把src目录下所有的.c文件都列出来你不用手动一个个写源文件名。$(SRCS:.c.o)是把源文件列表里的.c后缀替换成.o表示每个源文件对应一个编译出的目标文件。两步合起来就实现了“多文件自动编译”。这个Makefile你现在不一定要完全照抄但你心里要有这个数交叉编译的工程化本质上就是把“编译器、头文件目录、库目录、源文件”这些信息整理清楚。yolov5s部署时会引入opencv、rknn runtime库到时候你只需要在LDFLAGS里加上对应的-lopencv_world -lrknnrt在INCDIR里加上它们的头文件路径套路完全一致。5. 部署到香橙派并跑起来5.1 文件传输的几种方式对比交叉编译出来的hello_rk3588现在还在你的PC上。下一步是把它传到香橙派上。这里我对比一下常用的几种方式。传输方式优点缺点适用场景scp/rsync最简单一行命令走网络需要板卡和PC在同一局域网日常开发最推荐U盘拷贝不需要网络每次都要插拔U盘麻烦板卡没联网或网络不可用adb push通过USB数据线直连板卡侧要开启adb服务Windows主机时比较方便Samba共享像访问本地磁盘一样需要搭建共享服务性能一般频繁传大量文件时用我最常用的就是scp因为它的使用成本几乎是零。PC上执行scp hello_rk3588 orangepi192.168.1.100:/home/orangepi/把192.168.1.100换成你香橙派的实际IP地址orangepi是板卡上的用户名如果你是首次使用后面跟上密码。命令执行完可执行文件就到了板卡的家目录下。5.2 板端运行与验证接下来SSH登录到香橙派ssh orangepi192.168.1.100进入家目录先给可执行文件加上执行权限chmod x hello_rk3588然后直接运行./hello_rk3588如果一切正常你会在终端看到以Hello, RK3588!开头的输出然后是一大串/proc/cpuinfo的信息。CPU信息里你会看到类似model name : ARMv8 Processor rev v4 (v8l)或者Hardware : Rockchip RK3588这样的标识这时候你就能确认这个程序确实在RK3588上运行了。这里有个细节值得多说两句。chmod x这一步很多新手会忘记。因为不设置执行权限内核会拒绝运行这个文件报Permission denied。还有一点如果你的scp命令用的是root用户文件的所有者是root普通用户运行通常会因为权限问题失败建议在板卡端用sudo mv把文件放到/usr/local/bin一类目录或者直接在普通用户目录下运行。5.3 跑不起来时的排查路线如果运行报错最常见的是两个。第一个是./hello_rk3588: No such file or directory。这个报错非常具有迷惑性因为文件明明就在当前目录。其实这是因为动态链接器缺失。程序在内核里被加载时内核尝试执行它指定的interpreter/lib/ld-linux-aarch64.so.1如果你的板卡系统里没有这个文件内核直接拒绝加载并且提示“找不到文件”。解决方法是先file hello_rk3588确认编译出来的确实是arm64再确认板卡系统是arm64uname -m应该输出aarch64。如果架构没问题还报错大概率是板卡系统太精简缺库需要用ldd hello_rk3588看看动态库依赖情况然后手动补齐缺失的库。第二个常见报错是bash: ./hello_rk3588: cannot execute binary file: Exec format error。这个报错的意思很直白你试图运行的二进制文件CPU架构和当前系统不匹配。最典型的原因是你在PC上用普通gcc编译了一个x86的hello然后传到板卡上运行。遇到这个问题第一反应就是回去用aarch64-linux-gnu-gcc重新编译。排查路线的核心逻辑说穿了就是一句话确认三个东西的架构一致——编译器生成的目标架构、可执行文件的ELF架构、板卡CPU的架构。三个一致才能跑起来。6. 从hello到yolov5s交叉编译的真正价值6.1 RK3588的推理栈需要什么我知道你可能会想交叉编译一个hello和我的yolov5s部署有什么关系这里我把逻辑串起来讲清楚。yolov5s模型要在RK3588上跑起来通常有两条路线。第一条是直接用瑞芯微的NPU加速流程是先把训练好的yolov5s模型转换成rknn格式然后用RKNN Runtime在板卡上推理。第二条是纯CPU或GPU路线用ncnn、onnxruntime这类推理框架跑性能通常不如NPU路线。无论哪条路线你都会面临同一个问题推理框架和运行时库必须在板卡上存在。技术成熟的做法是你在PC上交叉编译好整个推理程序包括RKNN Runtime的链接然后把可执行文件和模型文件一同传到板卡。所以交叉编译hello的整套思路——安装工具链、编写Makefile、处理库依赖、传文件、板端运行排查——和后面交叉编译yolov5s推理程序是完全一致的只是源码复杂度和库依赖数量变了。我们可以直接对比一下这个演进关系步骤本教程helloyolov5s推理程序工具链aarch64-linux-gnu-gcc同一个工具链源码一个简单的c文件推理框架源码后处理代码依赖库无或系统基础库opencv、rknn runtime等传输方式scpscp模型文件权重文件板端运行直接执行需要模型文件和库路径设置表格放到这里就很清楚了。你现在学会的每一步都不是白学的。6.2 交叉编译思想在yolo工程中的复用具体到交叉编译yolov5s程序时有几个坑是现在就可以提前留意的。第一个是库的架构必须一致。你必须在PC上交叉编译出arm64版本的opencv、rknn runtime或者在板卡上直接用apt安装arm64版本的库然后在板卡上编译推理程序。我在实操中最常用的一种模式是在PC上把推理程序交叉编译成arm64可执行文件但opencv这类大库直接在板卡上通过apt安装现成的arm64包然后在Makefile里指定头文件目录和库目录指向板卡上的路径。这样省去了交叉编译opencv的漫长过程。第二个是模型文件本身没有架构问题。rknn格式的模型文件本质上是一个字节流的数据文件不存在x86和arm64的区别。所以你在PC上做模型转换模型文件直接拷贝到板卡即可使用。但推理程序本身有架构问题这就是为什么必须靠交叉编译或者板卡本地编译来生成可执行文件。第三个是rpath问题。当你交叉编译一个链接了libopencv_world.so的推理程序时动态链接器在运行时需要能找到这个库。如果库放在一个非标准路径比如/home/orangepi/libs程序运行时会报找不到库。解决方法是编译时在Makefile里加-Wl,-rpath,/home/orangepi/libs把库搜索路径直接写进二进制文件里。这个技巧在hello阶段可以先不操作但你要记在脑子里因为部署yolo时它一定会找上门来。7. 常见问题速查表我把自己实际操作中遇到过的、以及身边朋友踩过的问题整理成一张速查表方便你排查。现象可能原因解决思路aarch64-linux-gnu-gcc: command not found工具链没装好或不在PATH中重新执行sudo apt install gcc-aarch64-linux-gnu并用--version验证file显示x86-64而不是ARM aarch64你用了普通gcc编译了确认Makefile里CC已经设置为aarch64-linux-gnu-gcc板卡上运行报No such file or directory动态链接器不存在或架构不匹配file确认架构、uname -m确认板卡ldd查看依赖库板卡上运行报Exec format error二进制和CPU指令集不匹配用aarch64工具链重新编译板卡上运行报GLIBC_2.xx not foundPC工具链的glibc版本比板卡新换老版本工具链或改用静态编译Permission denied文件没有执行权限chmod x hello_rk3588编译时报找不到头文件缺依赖库的开发包安装对应库的头文件包或检查CFLAGS中的-I路径这里面最值得单独拎出来说的是glibc版本问题。用Ubuntu 22.04的PC交叉编译程序时默认的glibc版本是2.35而香橙派Ubuntu 20.04镜像里的glibc是2.31这时动态链接的hello虽然简单不一定会出问题因为只用到了基础系统调用但一旦程序里用到了高版本glibc才有的API板卡上就会直接崩。我见过不止一个朋友栽在这里排查了半天以为是架构问题实际上就是版本不兼容。稳妥的做法有两个一是把PC工具链换成较老版本比如在Ubuntu 22.04上手动安装gcc-10-aarch64-linux-gnu二是在板卡上用ldd --version查看glibc版本然后确保PC工具链的glibc不高于这个版本。8. 我的几点实操体会最后分享一些我在实际开发中的体会算是给这个基础章节收个尾。第一养成交叉编译习惯之后你会发现整个开发节奏变快了。在PC上改代码、编译出arm64可执行文件、scp过去、远程运行整个过程一气呵成比在板卡上本地编译省下大量时间。尤其是编译稍微大一点的工程PC交叉编译可能只要几十秒板卡上可能要几分钟这个差距会随着工程规模的增大变得非常明显。第二file命令绝对是嵌入式开发者的好朋友。每次编译完、传输前、运行报错时第一件事就是file一下确认架构。我见过太多人报错之后第一反应是怀疑系统、怀疑环境最后兜兜转转才发现是架构字母的问题。多花一秒钟运行file能帮你省下半小时的排查时间。第三这个hello程序其实是后续所有部署工作的“最小可行性模型”。你把这个流程玩透后面无论是交叉编译一个带opencv的图像处理demo还是链接RKNN Runtime跑yolov5s核心骨架都不会变。在你真正开始做RKNN模型转换之前我强烈建议你先把本章的hello部署流程重复三遍直到你不需要看教程就能完成全流程。下一部分我们就会开始逐步进入yolov5s模型部署的正题包括模型格式转换、NPU推理接口调用这些核心内容到那一步你就会发现本篇文章所做的所有铺垫都是值得的。