把工位上的检测主机换成树莓派5之前,我一直以为这是件很轻松的事。开发机上YOLOv5模型跑得好好的,精度也够,搬到ARM板上无非是换个环境跑推理而已。结果从开机到稳定运行,前后折腾了一个多月,硬生生卡在了六件事上:系统选型、模型迁移、推理性能、供电散热、产线通信、长期运行可靠性。
这篇不是正经教程,就是一次“树莓派5进车间”的项目复盘。我会把每个卡点怎么出现、怎么排查、最后怎么解决的都写清楚。打算把自训练的YOLOv5模型部署到树莓派5边缘设备上的朋友,尤其是要在工业现场跑长期任务的,这篇文章里的坑你大概率会踩到。
1. 系统底座:Ubuntu 装是装上了,可驱动世界塌了半边
1.1 为什么一上来就选 Ubuntu 而不是官方系统
开发环境是Ubuntu,云端训练也是Ubuntu,所以给树莓派5装系统时,第一反应就是装Ubuntu。当时树莓派5安装Ubuntu确实很顺,官方Imager里直接选Ubuntu 24.04 LTS镜像,写入SD卡,开机就能进系统。桌面能用,网口能通,那一刻我还觉得这波稳了。
稳了大概半天,问题开始往外冒。
树莓派5的官方系统Raspberry Pi OS Bookworm,和Ubuntu在硬件支持上差得不是一点半点。Bookworm里出厂就打磨好的东西,在Ubuntu下经常要自己动手,甚至动手都没用。比如GPIO,树莓派5的引脚控制方式改了,很多老的RPi.GPIO代码在Ubuntu上跑不起来,要用libgpiod那一套。再比如硬件视频编码,树叶派5的H.264编码器在Ubuntu下的V4L2接口行为跟官方系统不一致,做视频录像审计的时候就卡住了。还有GPU算力,树莓派5的VideoCore VII在官方系统里对Vulkan支持是现成的,Ubuntu上得自己折腾驱动。
卡到第三天,我做了一个后来被证明极其正确的决定:宿主机换成Raspberry Pi OS Bookworm,再用Docker跑一个Ubuntu 24.04容器作为开发环境。所有依赖装在容器里,宿主机负责摄像头、GPIO、串口这些硬件资源,通过--device /dev/video0:/dev/video0把设备映射进容器。这样既保留了Ubuntu的软件生态,又不丢官方系统的硬件支持。
一句话,系统底座的正确打开方式不是二选一,而是“官方系统兜底,容器凑生态”。
1.2 ARM架构下的依赖地狱
树莓派5安装Ubuntu之后,不管是直接跑还是容器跑,装深度学习那套依赖都会遇到一个现实:很多pip包没有aarch64平台的预编译wheel,需要现场源码编译。YOLOv5系列涉及PyTorch、OpenCV、NumPy、pandas,如果版本选得太新,光编译就能让你等上几个小时。
我的实际做法是锁版本。PyTorch用官方在Linux aarch64有预编译wheel的版本,ONNX Runtime同样找aarch64的发布版,OpenCV用apt装系统的发行版而不是pip死磕。另外给pip配了piwheels源,虽然piwheels主要面向树莓派官方系统,但纯ARM架构的预编译包能省不少编译时间。
这期间最大的教训是:别在树莓派上追求“最新”。开发机上最新版能跑,板子上不一定有对应包;板子上有对应包,不一定能编过;就算编过了,下次环境重装又要再来一遍。车间设备要的是可复现,不是新潮。
2. 自训 YOLOv5 搬上 ARM:没死在模型,死在了依赖上
2.1 导出链路:pt 到 onnx 只是表面顺利
我的模型是自己在工业数据集上从头训练的YOLOv5s,类别有划痕、脏污和异物,总共三类。在开发机上验证没问题后,第一步导出ONNX:
python export.py --weights best.pt --include onnx --opset 12这个命令在PC上几秒钟就跑完了,拷到树莓派上也顺利加载,但后处理开始出幺蛾子。YOLOv5的ONNX输出不止一个头,原始输出是一大串reshape后的数组,需要自己解析成三个尺度的feature map,再自己写NMS。
当时图省事,试过带端到端NMS的导出方式,结果ONNX Runtime在aarch64平台上对那个自定义NMS算子的支持不完善,推理报错。折腾一下午之后,我老老实实写了一个纯Python的后处理函数:解析输出、按置信度过滤、NMS去重。
这块的教训是:边缘部署的后处理代码,越简单越通用越好。别迷信框架自带的端到端方案,尤其在ARM平台上,算子兼容性永远是隐形的坑。
2.2 推理库选型与性能基线
树莓派5上推理库我测了三条路:PyTorch直接推理、ONNX Runtime CPU推理、OpenCV DNN推理。
PyTorch最重,但兼容性最好,缺点是启动慢、内存占用高、推理帧率低。OpenCV DNN快,但YOLOv5的某些层支持不完整,后处理更麻烦。最后选定ONNX Runtime,原因很简单,aarch64有官方wheel,CPU优化做得比PyTorch直接跑好,部署时还能动态量化。
实测基线(树莓派5 8GB,默认散热,yolov5s,输入640x640,FP32):
- PyTorch直接推理:2.1 FPS
- ONNX Runtime CPU推理:4.3 FPS
- 加上图像预处理和后处理NMS:端到端约3.5 FPS
这个速度对静态检测勉强够用,但产线上稍微快一点,传送带每秒移动30厘米,就会出现漏检。测完这个基线,我才意识到后面专门要给性能开一个专题。
2.3 量化那条路,我半路放弃了
ONNX Runtime支持动态量化,FP32模型压到INT8,体积小一半多,CPU推理速度能再快一倍。可测试下来,小目标漏检率明显上升,尤其是划痕这类低对比度缺陷。车间检测最怕的不是慢,是漏。漏一次就是残次品流到客户手里。
所以最终方案很简单粗暴:保留FP32精度,牺牲部分帧率,用输入尺寸和管线架构把速度找回来。这个取舍,越到后面越觉得值。
3. 性能不够:不是算力的问题,是取舍的问题
3.1 别对树莓派5的 GPU 抱幻想
树莓派5刚到手时,我也研究过用GPU加速推理。VideoCore VII确实支持Vulkan,网上也有人成功用NCNN在树莓派5上跑YOLOv5。但我实测下来,这条路在工业场景里不现实。
第一,树莓派5的GPU算力有限,跑YOLOv5s这类模型,收益远不如预期。第二,Vulkan/NCNN生态的部署链路不成熟,量化和后处理都要自己调,风险大。第三,GPU推理的驱动依赖和系统绑定很深,换个环境可能就废了。
我的结论是:树莓派5的GPU不是给你跑深度学习用的,把CPU优化做足,已经能覆盖很多场景。
3.2 当场能做的三件优化
第一个优化是换轻量模型。YOLOv5s换成YOLOv5n,参数少了近一半,精度降低一点,但推理帧率直接翻倍。如果是检测明显的工件或大缺陷,YOLOv5n完全够用。
第二个优化是降输入分辨率。640降到416,推理时间减少大概50%。但这里有个前提:你的缺陷尺寸不能太小。我拿实际数据集测过,320分辨率下一个小划痕可能只有几个像素,直接漏检。所以我的建议是输入尺寸要根据最小目标像素数来定,先算理论边界,再动手降。
第三个优化才是核心:把多线程/多进程管线和推理解耦。我的架构分三个进程:相机采集进程、推理进程、结果上传进程。进程之间用队列通信,而不是在同一个进程里串行执行。这样采集和推理可以重叠,端到端吞吐量从3.5 FPS提到6 FPS左右。
代码层面,ONNX Runtime的线程配置也很讲究:
import onnxruntime as ort sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = 4 sess_options.inter_op_num_threads = 1 sess = ort.InferenceSession("best.onnx", sess_options, providers=["CPUExecutionProvider"])同时设环境变量OMP_NUM_THREADS=4,别让线程调度打架。
3.3 真正解决性能问题的,是 PCIe 外接 NPU
CPU优化做得再狠,yolov5s在树莓派5上也就是个位数的FPS。产线节拍一紧,我最终上了树莓派5最有价值的外设接口:PCIe。
树莓派5提供PCIe Gen3 x1,接一块M.2 HAT转接板,就能插NVMe SSD或NPU加速卡。我用的是Hailo-8加速卡,标称算力26 TOPS,YOLOv5模型走它的工具链做量化编译后,推理帧率直接跳到50 FPS以上,而且功耗远低于CPU满载。
部署自训练的YOLOv5模型这件事,最后的生产方案变成了:树莓派5仅做IO调度和协议处理,模型推理全部交给NPU,CPU负载保持在20%以下,温度稳定,帧率富余。说实话,如果早两周把PCIe这条路想明白,能少踩很多坑。
4. 车间第一道物理关:供电、散热与接线隔离
4.1 5V/5A 不是够不够的事,是电压稳不稳的事
树莓派5的供电协议比4代挑剔得多。官方要求5V/5A,也就是27W左右的USB-C PD输入。我一开始用一个标称5V/3A的电源,短时间跑没问题,一旦推理负载上去,USB相机偶尔掉线,NVMe盘还可能直接消失。
查dmesg,满屏的bcm2835-power电压欠压警告。树莓派检测到输入电压低于阈值,会主动限制USB外设的电流,这就是外设掉线的根因。车间供电环境比办公室恶劣,隔壁就是电机和变频器,电压波动是常态。
解决办法是给树莓派单独配了一路导轨电源,DC 5V输出,容量按5A以上选,不跟其他设备共用一个开关电源。如果是布点位置不方便接插头,可以用PoE HAT,一根网线同时解决供电和网络,注意别同时插USB电源和PoE,电源冲突会出问题。
4.2 散热不解决,推理性能自动打七折
树莓派5满负载时CPU温度升得非常快,一旦到85℃就开始降频,推理帧率从4 FPS掉到2.5 FPS,这是硬件自己认怂,不做任何提示。车间环境夏天轻松到40℃+,装风扇是必然的。
我用的是铝制散热片加4020风扇,温度控制到65℃以下。别忘了留维护口:车间粉尘大,风扇半年就堵,到时候吹出来的风全是热的,散热效果大打折扣。正常维护周期我定在三个月清一次灰。
高温还会加速SD卡损耗。这也是我后来把系统迁移到NVMe SSD上的重要原因。树莓派5通过M.2 HAT接了一块NVMe硬盘,系统、模型、日志全放NVMe,SD卡只保留引导分区。速度提升了,耐用性也提升了,SD卡再也不怕写坏了。
4.3 24V 与 3.3V 之间,隔着一层光耦
树莓派的GPIO是3.3V电平,车间的接近开关、继电器输出大多是24V电平,直接对接非常危险。我第一次接感应开关时,因为没注意NPN/PNP类型,烧了一块GPIO扩展板,几十块钱倒是小事,耽误两天调试才是大事。
正确的做法是中间加光耦隔离模块,24V信号转3.3V信号再接树莓派。输出方向,树莓派3.3V信号也要通过固态继电器或光耦去驱动24V负载,绝对不能直接驱动PLC的外部继电器。车间线缆用带磁环的USB线,尽量走独立线槽,跟电机动力线保持距离,不然干扰会导致视频流掉帧。
5. 和产线设备"对话",比训练模型更磨人
5.1 模型输出不等于产线事件
卡在这一步时,模型精度已经不是瓶颈了。我花了三天时间把检测模型的准确率从87%调到92%,结果跟PLC联调了一天,发现对方根本不关心你的框画得准不准,只关心三件事:现在是好件还是坏件,坏件是什么类型,坏件位置在哪。
所以协议设计必须往前放,放在模型部署之前。我用的是一组Modbus寄存器,树莓派作为Modbus TCP客户端,周期性写入PLC的保持寄存器区:
- 寄存器1:状态,0空闲、1检测中、2良品、3不良品
- 寄存器2:缺陷类型编号,1划痕、2脏污、3异物
- 寄存器3-4:缺陷中心X坐标
- 寄存器5-6:缺陷中心Y坐标
就这么点东西,写代码只花了一个小时,但定义协议和跟PLC工程师对参数对齐,用了大半天。车间设备的对接从来不是技术难题,而是沟通和约定谁来做状态机的问题。
5.2 Modbus TCP 和 RS485,两条腿都要会走
老一点的生产设备没有网口,只有RS485串口,跑Modbus RTU协议。树莓派5的串口设置也有个坑:默认的UART可能被系统console占用,需要在/boot/firmware/config.txt里关掉console接管:
enable_uart=1 dtoverlay=uart0串口外接RS485电平转换芯片,比如MAX3485,再接设备。接线时A/B别接反,终端电阻要不要加看距离。Modbus RTU和Modbus TCP的字节序、帧格式都不同,我这里就吃过亏:TCP模式下坐标乱跳,排查半天发现是PLC和树莓派对寄存器的高低字节解释不一致。总线协议这种东西,最好写一个单独的小工具先连通,再跑业务逻辑。
5.3 触发时序:谁给谁发信号,这是个状态机问题
最开始我打算让树莓派同时负责触发信号采集和视觉检测:接近开关直接接GPIO,触发后立刻拍照推理。听起来顺理成章,实际跑起来各种乱。
原因是Python不是实时系统,从GPIO电平变化到后端推理线程响应,延迟不可控。产线节拍50毫秒级时,这种延迟会导致漏拍。
最终架构是把触发逻辑从树莓派上剥出去,改由PLC统一负责时序:PLC收到接近开关信号后,通过Modbus寄存器通知树莓派“开始拍一张”,树莓派推理完把结果写回寄存器,PLC读到结果后执行分拣动作。树莓派只做视觉,不碰实时控制。隔离职责之后,系统稳定多了。
5.4 检测数据必须留档
车间设备最怕扯皮。产品到底有没有经过检测,检测结果是什么,必须能回溯。我每天生成一个CSV日志,记录时间戳、结果、坐标、置信度,图片按小时归档到NVMe本地盘,定期拷贝到公司共享存储。
有一次客户投诉,说某批产品有缺陷流出。查系统日志,发现那个批次确实有一帧漏检,原因是我们设置的置信度阈值偏高,模型对某类缺陷输出只有0.49,被过滤掉了。没有数据留档,这种问题会变成“供应商与客户”之间无休止的争论。有了留档,问题三分钟定位。
6. 七天不间断:服务自愈、看门狗与远程维护
6.1 SD卡怕断电,那就把系统做成“断电也不怕”
车间意外断电很常见。树莓派如果直接跑在SD卡上,断电几次,ext4文件系统损坏,起不来的概率非常高。我一开始用的树莓派5安装Ubuntu方案,SD卡被我拔电测试拔坏过两张。
后来把系统迁移到NVMe SSD,SD卡只做引导分区,问题基本消失。进一步增强:根文件系统以只读模式挂载,临时文件和运行状态放到tmpfs内存盘,程序更新时再重新挂载为读写。这样就算被一把拉断电,文件系统也是干净的,开机几秒钟恢复工作。
日志方面用了log2ram方案,把/var/log放到内存里,定期同步到磁盘,减少写操作。知道的人会觉得这是洁癖,但在工业现场,多扛一次断电,就少一次出勤维护。
6.2 systemd 服务自启 + 看门狗
推理程序不能只做python main.py就完事,必须变成系统服务:
[Service] ExecStart=/usr/bin/python3 /opt/vis_app/main.py Restart=always RestartSec=5 WatchdogSec=10Restart=always保证程序崩溃自动拉起,WatchdogSec=10配合systemd的看门狗机制,程序定期sd_notify心跳喂狗,超过10秒没喂,systemd强制重启服务。
系统层面也挂看门狗。树莓派5没有独立外部看门狗芯片,但可以用Linux内核的softdog模块,加载后出现/dev/watchdog设备,再在/etc/systemd/system.conf里设置RuntimeWatchdogSec=30,内核定期写狗。系统一旦假死,硬件重启。
程序内部还要有业务看门狗:连续5秒没拿到新的检测结果,自动记录错误并重启推理服务。这个逻辑比系统看门狗更重要,因为系统没死,但业务卡死了,产线上照样是废品。
6.3 远程维护:老老实实走局域网
车间工位不可能放显示器键盘。我的维护方式很简单:树莓派固定IP,同一车间网络内SSH登录;需要看图形界面就用VNC。但不要依赖什么花哨的远程穿透方案,车间网络安全要求通常不允许设备随便外联外部网络。
我的做法是给树莓派只分配内网IP,所有对外通信只有Modbus TCP到PLC的端口。维护时通过公司已有的远程办公系统接入内网再SSH。另外定期把/etc和/opt/vis_app打包备份到共享存储,系统挂了直接重新烧录,半小时恢复工位。机械设备修理讲究工装,这种“烧录备份”就是软件工装。
最后说个真实体会:树莓派5进车间这件事,如果只是实验工位、节拍松、检测目标又大,那它完全能胜任;如果要对齐严格产线节拍,一定把供电、散热、通信、可靠性当性能一样对待。我卡了一个多月,绝大多数时间不是在调模型,而是在解决这些“不性感”的现场问题。现在这套方案已经在车间连续跑了两个多月,一次死机都没出现过。折腾的人是我,希望后来者少折腾几天。