1. 为什么先折腾PC端模拟器——给RK3588打前站的三个理由
先说一下这篇的背景。我在做香橙派RK3588的YOLOv5s部署系列教程,这一篇是第04集,重点放在"PC端模拟器仿真跑通YOLOv5"。很多朋友拿到RK3588开发板,第一件事就是烧系统、连屏幕、开终端,恨不得当天就把YOLOv5的demo跑出来。结果往往是:系统烧写不顺利、网络配不通、SDK版本对不上、日志刷屏看不懂,折腾三天连Python环境都没装好,热情直接凉了一半。
我的做法相反——先在PC上用模拟器把整个流程"预演"一遍。RK3588是一块8核ARM64的SoC,集成了6TOPS算力的NPU,跑YOLOv5s这种轻量级检测网络完全没问题。但正因为它是ARM架构、跑的是Ubuntu根文件系统、还要跟NPU驱动打交道,前期环境调试的复杂度远高于一台普通x86电脑。如果你能先在模拟器里把Ubuntu 20.04环境、YOLOv5源码、依赖库、权重推理这一条链路全部跑通,后面上真机就会从容很多。
我先说说为什么这个"打前站"的思路值得做。
第一,模拟器里的环境是"可丢弃"的。你在PC上用QEMU模拟一块aarch64的机器,随时可以快照、回滚、重新创建。真机烧写一次系统至少十几分钟,还不一定一次成功;模拟器创建环境只需要一条命令,搞坏了再起一个就是。对于教程学习来说,这个试错成本差距是巨大的。
第二,仿真环境暴露的是"纯软件问题"。YOLOv5在RK3588上跑,涉及的问题分两类:一类是算法本身的问题,比如数据集格式、模型配置、推理参数、后处理逻辑;另一类是硬件平台的问题,比如NPU驱动、内存带宽、算子支持度。如果你直接在板子上调试,两类问题会混在一起,出了问题很难定位。先在模拟器里把第一类问题解决掉,上板之后只面对第二类问题,排查范围小了一半。
第三,性能不是模拟器的目的,正确性是。很多人一听模拟器就说"跑得慢有什么意义"。模拟器的意义根本不在于跑多快,而在于验证流程和逻辑是否正确。你的YOLOv5模型在模拟器里能正确识别出一张图片里的猫和狗,说明你的环境、代码、权重、推理链路是通的。换成真机,同样的代码大概率也能跑,只是速度更快、还能用NPU。
这一篇的另外一个价值在于:就算你没有RK3588板子,只是想学YOLOv5目标检测的知识,这套模拟器方案也能让你先接触ARM64环境下的部署流程,为以后的工作打好基础。
2. 模拟器环境搭建的完整过程:从Ubuntu 20.04根文件系统到QEMU启动
2.1 准备工具和系统镜像
我用的方案是QEMU系统级模拟,不是用户态模拟。两者的区别简单说:用户态模拟只模拟CPU指令层,你可以在x86的Linux上直接跑ARM64的二进制程序,但没法模拟整个操作系统和外设;系统级模拟则是一整台虚拟的ARM64机器,有虚拟硬盘、虚拟网卡、串口,可以像真机一样安装和运行完整的Ubuntu系统。对于我们要跑YOLOv5这种涉及Python、OpenCV、大量动态库的应用,系统级模拟是唯一靠谱的选择。
需要准备的材料有三样:
Ubuntu 20.04的aarch64根文件系统。我直接用的ubuntu-base-20.04-base-arm64.tar.gz,这是Ubuntu官方提供的ARM64基础系统包,没有桌面环境,只有最基础的命令行工具。跑YOLOv5我们不需要桌面,命令行就够了。
QEMU的aarch64固件,也就是EFI引导文件。用QEMU_EFI.fd这个文件,它负责模拟ARM64机器的固件启动阶段。
一个Linux内核镜像。我偷懒直接用了香橙派官方系统里提取出来的内核,但更通用的做法是用Ubuntu ARM64的内核包。这里要提醒一句:内核版本不用太新,稳定就好,因为模拟器里不需要板级的设备树驱动,通用内核反而更省心。
注意:在正式开始之前,先在PC上确认你的QEMU版本支持aarch64系统模拟。Ubuntu 20.04以上系统的软件源里,包名叫做qemu-system-arm,直接apt安装就能拿到。
2.2 QEMU系统仿真的启动参数
很多教程喜欢直接给一整条启动命令,但我不建议直接复制,而是要把每个参数吃透,否则出了问题你根本不知道从哪里排查。我整理一下我实际用的启动命令:
qemu-system-aarch64 \ -M virt \ -cpu cortex-a76 \ -smp 8 \ -m 8192 \ -bios QEMU_EFI.fd \ -kernel vmlinuz \ -initrd initrd.img \ -drive file=ubuntu_rootfs.img,if=none,id=hd0,format=raw \ -device virtio-blk-device,drive=hd0 \ -netdev user,id=net0,hostfwd=tcp::2222-:22 \ -device virtio-net-device,netdev=net0 \ -device virtio-gpu-pci \ -display none \ -append "root=/dev/vda rw console=ttyAMA0"逐项解释一下:
-M virt:使用QEMU的virt通用机器类型,这是ARM64模拟最常用的,兼容性最好,不需要关心具体板卡的差异。-cpu cortex-a76:模拟Cortex-A76核心。RK3588是四核A76加四核A55的架构,我在模拟器里统一用A76,性能更高一些,而且和真机的指令集是兼容的。实际上QEMU对ARMv8-A的支持已经很成熟,这个参数影响的是仿真执行效率,不是功能正确性。-smp 8:模拟8个CPU核心。这里有意思的是,QEMU的TCG多线程仿真在aarch64上已经支持得很好,8个核能明显加快PyTorch的CPU推理速度。-m 8192:分配8GB内存。YOLOv5s推理本身吃不了太多内存,但PyTorch和OpenCV的加载开销不小,而且Ubuntu 20.04基础系统运行也需要内存,我给到8GB是为了后面训练小数据集时不至于OOM。-netdev user:用QEMU的用户态网络,配合hostfwd把宿主机的2222端口映射到虚拟机里的22端口,这样就能通过SSH连接过去。-device virtio-gpu-pci:虽然-display none关闭了图形界面,但保留GPU设备是为了让系统能识别到显示设备,某些程序(比如OpenCV的GUI模块)在初始化时会检测显示环境,没有设备可能直接报错。-append:传给内核的启动参数,root=/dev/vda指根文件系统在第一个虚拟磁盘上,console=ttyAMA0把控制台输出重定向到串口。
2.3 网络配置和SSH登录
启动之后,如果看到内核日志刷屏、最后出现Ubuntu 20.04 LTS的登录提示符,说明系统已经跑起来了。
但模拟器里只有一个命令行终端,操作体验很差,所以我第一件事是配置SSH。在模拟器终端里执行:
apt update apt install -y openssh-server net-tools passwd root systemctl enable ssh systemctl restart ssh然后在宿主机上直接ssh root@localhost -p 2222,就能登录到模拟环境里了。这里有个实用技巧:我建议全程用SSH操作模拟器,不要直接在QEMU的串口终端里输命令。SSH的终端更流畅,支持复制粘贴,还能用VS Code的Remote SSH插件直接连上去写代码,效率完全不是同一个级别。
2.4 共享目录的搭建
模拟器里跑YOLOv5,总要把宿主机的图片、数据集、权重文件传进去。虽然可以用scp一个个拷,但我更喜欢用QEMU的virtio-9p共享目录功能,直接映射宿主机的一个文件夹到虚拟机里,读写共享,非常方便。
启动命令里加上这么一段:
-virtfs local,path=/home/me/yolo-share,mount_tag=hostshare,security_model=none,id=share0进入虚拟机后挂载:
mkdir -p /mnt/hostshare mount -t 9p -o trans=virtio hostshare /mnt/hostshare这样宿主机/home/me/yolo-share目录下的东西,在模拟器里直接就能看到。我把YOLOv5源码、测试图片、训练数据集全放在这个共享目录里,模拟器里只需要做执行和调试,文件管理还是交给宿主机,省去了来回拷贝的麻烦。
这一步完成之后,整个模拟环境就是一个"可以跑ARM64 Ubuntu的虚拟机器"了。下一步就是在这个环境里装YOLOv5的运行环境。
3. YOLOv5在模拟器里从安装到跑通的实操记录
3.1 搭建Python环境和PyTorch
Ubuntu 20.04自带的Python版本只有3.8,用来跑YOLOv5其实有点老(新版本YOLOv5要求Python 3.7以上即可,所以3.8是满足的)。我建议用一个干净的虚拟环境来安装,避免和系统Python包冲突。这是我做了很多次之后觉得最稳妥的方式:
apt install -y python3-pip python3-venv git wget python3 -m venv /opt/yolov5-env source /opt/yolov5-env/bin/activate激活虚拟环境之后,先安装PyTorch。这里要特别注意,模拟器是ARM64架构,而且没有GPU,所以只能装CPU版。在ARM64的Ubuntu上,PyTorch官方源是有对应轮子的,直接pip安装就行:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu装的时候如果速度很慢,可以把PyTorch的源换成国内镜像,但不要混合不同的源,否则容易拉取到不匹配的包。
提示:模拟器里千万不要尝试装CUDA版本的PyTorch,因为QEMU模拟出来的设备根本没有GPU计算能力。装CUDA版不仅浪费空间,还会在运行时报找不到驱动。
3.2 下载YOLOv5源码和权重
YOLOv5的源码在GitHub上,直接clone到共享目录里:
cd /mnt/hostshare git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt这里requirements.txt里有一堆依赖,最好让pip自动装。如果某个包编译失败(比如opencv-python-headless在ARM64源里比较少见),可以改用系统的apt包替代:
apt install -y python3-opencv这是一个很实用的思路:pip装不了的OpenCV版本,用apt装系统版往往更快更稳,YOLOv5需要用的核心API(imread、resize、cvtColor这些)在两个版本里都是通用的。
权重文件从官方仓库里下载yolov5s.pt:
wget https://github.com/ultralytics/yolov5/releases/download/v6.0/yolov5s.pt如果这个下载地址访问慢,可以从其他镜像站拉取。yolov5s.pt大概14MB,虽然不大,但在模拟器网络不好的时候也可能下载超时。我的做法是:在宿主机上下载好,放到共享目录里,模拟器直接读。
3.3 用图片跑第一次推理
环境搭建好之后,先用一张现成的图片测试能不能正常跑通推理。我用的是YOLOv5仓库自带的测试图片data/images/bus.jpg,这里我不直接介绍检测结果,先看一下最基础的推理命令:
cd /mnt/hostshare/yolov5 python detect.py --weights yolov5s.pt --source data/images/bus.jpg --conf-thres 0.4 --iou-thres 0.5 --project runs/detect跑起来之后,你会看到一堆日志,包括模型的层结构、参数量、FLOPs、类别数量等。如果一切正常,最后会生成一张标注好检测框的结果图,保存在runs/detect/exp/目录下。
第一次跑通的时间在模拟器里大概要1分多钟,因为在加载PyTorch和权重文件时需要把大量动态库映射到模拟内存中。这个速度看着慢,但你要知道,真实RK3588板子的CPU跑同样推理也就几百毫秒到一两秒,模拟器的主要目的是验证流程,不是看性能。
跑通一次之后,我建议做一个基础验证:换一张自己的图片试试。我放了一张有三瓶饮料的桌面照片进共享目录,YOLOv5默认的COCO权重能识别出bottle类别,如果识别结果和真实场景基本吻合,说明整个推理链路已经通了——从图片读取、预处理、模型推理、后处理到结果可视化,每一步都在正常工作。
3.4 用视频跑推理,顺便聊一下CPU模式的参数调优
图像测试通过之后,可以试试视频或者摄像头输入。但模拟器里的摄像头设备映射比较麻烦,我建议先用视频文件。我自己做测试时用的是一段30秒的车辆行驶视频,分辨率为1080p,命令如下:
python detect.py --weights yolov5s.pt --source test_car.mp4 --conf-thres 0.5 --device cpu视频推理比图片慢得多,这是正常的。因为每一帧都要做一次完整的前向推理,而模拟器里CPU没有硬件加速。这里有个实际经验:模拟器做视频推理的正确姿势是降低分辨率,比如把视频先缩放成640x480再喂给YOLOv5,速度会快很多,而且YOLOv5本身会把输入缩放到640x640,你送的视频分辨率太高反而浪费了缩放开销。
还有一个参数值得说,就是--conf-thres。这个阈值控制检测置信度:阈值设得低(比如0.1),模型会输出很多低置信度的框,漏检少但你得忍受大量误检;阈值设得高(比如0.8),只有高置信度检测框会保留,误检少但可能漏掉小目标。日常调试我习惯先用0.4看全局,再根据实际效果往上下调。
到这里,模拟器里的YOLOv5已经能稳定跑通了。但这只是"跑通了官方模型",下一步才是关键:我们最终要部署的是自己的数据集训练的模型,这在模拟器里怎么验证?答案和训练有关。我在模拟器里跑过一个2GB数据集的训练任务,虽然比真机慢不少,但训练流程完全能走通,loss曲线正常下降,最后产出的权重文件可以正常推理。这也验证了一件事:香橙派RK3588的部署链路在软件层面是完全可以脱离硬件来验证的。
4. 模拟器验证和真机RK3588部署的差距在哪里
这是大部分教程不会专门讲的部分,但我觉得恰恰是最重要的部分。很多人以为模拟器跑通了,上板子就能一样跑通,结果真机上一堆新的问题。核心原因在于模拟器帮你验证了"软件逻辑",但没有帮你验证"硬件对接"。
4.1 指令集兼容,但性能天差地别
模拟器里是ARMv8-A指令集,RK3588的CPU是ARMv8.2-A指令集,二者在基本指令层面完全兼容,所以在模拟器里编译和运行的程序可以直接拷贝到真机上执行。这是模拟器跑通流程能迁移到真机的前提。
但性能差距大到不能直接参考。我用同一个YOLOv5s模型在模拟器和真机上分别测试过:模拟器跑一张640x640图片,前后推理大概2到3秒(取决于宿主机的CPU性能);真机RK3588的CPU跑同样推理,实测大概0.3到0.5秒。如果NPU加速,可以做到0.05秒以下。也就是说,模拟器验证的是"能不能跑对",永远不要试图从模拟器里估算真机的实时性能。
4.2 NPU加速是模拟器里没有的隐藏技能
RK3588最有价值的地方是集成的NPU,可以跑RKNN格式的量化模型。这一步和模拟器里跑的完全是两个体系:
- 模拟器里:PyTorch标准模型,FP32精度,CPU推理。
- 真机上:RKNN模型,INT8量化,NPU推理。
所以我不建议上了真机还死磕PyTorch原版推理,那是浪费RK3588的能力。正确做法是:在模拟器里先把YOLOv5跑通验证模型正确性,然后转到工作PC(x86架构)上,使用RKNN Toolkit完成模型转换、量化、精度验证,最后把生成的RKNN模型部署到真机的RKNN运行时进行推理。
这个链条里,模拟器的角色定位就是"中间验证站":确保你在训练自己的数据集之后,模型推理效果没问题;这样后续在量化转换时遇到精度下降,你至少能区分问题出在网络还是出在量化。
4.3 模型量化与后处理的迁移要点
模拟器里跑的YOLOv5后处理是Python端的NMS(非极大值抑制),模型输出的是三个尺寸的特征图,每个特征图都有各自的anchor配置,后处理需要把这些特征图解码成检测框,再做NMS筛选。这部分代码在YOLOv5的models/yolo.py的Detect层里,导出ONNX时会把NMS之外的解码逻辑一起导出。
但到了RKNN上,后处理的写法和PyTorch里完全不同。RKNN的Python接口提供自带的post_process方法,而C接口则需要完全手写解码逻辑,包括:
- 从NPU输出读取三个特征图的原始数据,它们的排列方式是NCHW,但NPU输出的内存布局可能是特殊的;
- 解码时要把每个特征图的位置信息、目标置信度和类别置信度拆开;
- 如果模型做了INT8量化,输出数据不是直接的float,需要反量化公式恢复成置信度;
- 最后再做NMS,但RKNN输出的框坐标是相对于640x640输入尺寸的,需要映射回原始图像尺寸。
这些细节模拟器完全帮不上忙,必须拿到真机上调试。但反过来说,如果你在模拟器里已经理解了YOLOv5的后处理逻辑——每个特征图怎么解码、锚框怎么定义、置信度怎么计算——那就等于掌握了一套独立于硬件平台的"算法地基"。后续在RKNN上重写后处理,你是在"翻译"逻辑,而不是"猜"逻辑,难度完全不一样。
这也是我为什么坚持建议先在PC端模拟器仿真这一步:它不是可有可无的过渡,而是帮你把算法和硬件两个层面的问题分开解决,降低最终上板的整体难度。
5. 我在模拟器仿真阶段踩过的坑,附完整排查思路
5.1 坑一:启动后卡在"root filesystem"挂载失败
第一次启动QEMU时,系统一直卡在类似VFS: Unable to mount root fs on unknown-block(254,0)的地方。原因有几种可能性,我逐个排查:
- 根文件系统镜像格式不对。我最初直接把一个tar.gz文件当成磁盘镜像传给
-drive,那能启动才有鬼了,必须先制作一个raw格式的空镜像,再把tar包解压进去。 - 内核缺少对应的virtio驱动。最开始我用的内核没有编入
virtio_blk模块,导致系统识别不了/dev/vda作为根设备。
排查思路是这样的:先确认磁盘镜像是不是raw格式且分区正确,再看内核有没有virtio支持。如果两个都没问题,还可以在内核参数里临时加上rootdelay=5,给设备初始化多留一点时间,避免设备注册慢导致挂载失败。
5.2 坑二:模拟器里的Ubuntu磁盘空间不足
这个坑我在模拟器里踩过,后来发现网上不少人也遇到过。Ubuntu根文件系统默认镜像如果按1GB做的,那装完PyTorch、OpenCV一堆依赖后很快报警。我最初建的镜像总共才2GB,装到YOLOv5运行环境时,磁盘直接满掉。
排查思路是先看盘在哪:
df -h然后确认是系统盘满了还是共享目录满了。如果系统盘满了,最简单的办法是直接把镜像扩容。QEMU的raw镜像可以用truncate直接拉大,然后用parted扩容分区,最后用resize2fs扩展文件系统。我给模拟器创建镜像的时候就直接用了8GB,一次性到位的经验是:创建镜像时宁可大一点,也别抠容量。
5.3 坑三:YOLOv5推理时内存不足被OOM
跑了视频推理时,内存直接从6GB涨到接近分配上限,最后进程被系统杀掉。原因是视频推理时,YOLOv5会把每一帧的检测结果保留下来,在输出视频时做帧序列缓存,再加上中间的多帧tensor在内存里没有及时释放。
面对这种情况,我后来用两个方案解决:
- 增加内存分配,在QEMU参数里把
-m从4GB调到8GB。 - 降低检测帧的输入分辨率,把视频先缩放成640x480再输入,内存占用和输入分辨率几乎是线性关系,降分辨率的效果立竿见影。
这个坑虽然发生在模拟器里,但在真机上同样会遇到。在RK3588上做视频流检测时,必须限制帧率或者做跳帧处理,否则即使NPU算力够,吃满系统内存也是时间问题。
5.4 坑四:OpenCV装不上或者说依赖冲突
模拟器里pip install opencv-python极容易失败,因为ARM64的轮子版本鱼龙混杂,有些编译参数和Ubuntu 20.04的库不兼容。装好之后也可能在import cv2时报libGL.so.1: cannot open shared object file。看到这个报错的别慌,这是典型的有头OpenCV依赖libGL库,而服务器版系统里没有装图形库。
我的排查思路很简单:先用ldd看一下cv2依赖了哪些库,缺哪个就apt补哪个:
ldd /opt/yolov5-env/lib/python3.8/site-packages/cv2/*.so | grep "not found"通常缺的都是libGL.so.1或者libgthread-2.0.so.0,apt安装libgl1和libglib2.0-0就能解决。如果你不想折腾这些,直接apt install python3-opencv替代pip安装是最省事的。
5.5 用好QEMU快照功能,等于给模拟环境上保险
最后一个不是坑,是技巧。QEMU提供snapshot机制,可以随时把虚拟机当前的状态保存下来,之后随时恢复。这个功能对新手非常友好:
- 在QEMU启动参数中加上磁盘串口信息,然后用HMP(Human Monitor Protocol)的
savevm命令保存状态。 - 恢复时,在QEMU monitor里执行
loadvm,整个系统包括内存状态和磁盘状态一起还原。
你的模拟环境如果因为装了某个依赖搞坏了系统,一条命令就能回到装坏之前的干净状态。这比反复重建环境、重复安装依赖要省事太多。
我在做这套模拟器方案的过程中,QEMU快照这个功能帮我省了至少十几次重复搭建环境的操作。每次准备做一次"有风险的实验"前,我都先快照一下,实验做完不对就退回,完全不用心疼。
这一篇的内容写到这里已经足够完整了——从为什么先做模拟器仿真、环境怎么搭、YOLOv5怎么跑通,到模拟器和真机的差异、踩坑排查思路,全部串起来了。下一步就是拿着这份经验上真机烧写Ubuntu、配置RKNN环境了,那部分我会在系列后续的章节里接着写。