树莓派+OpenPLC:基于YOLOv5n与Modbus的边缘AI工业控制方案
1. 项目缘起:当边缘AI遇上工业控制
最近在做一个挺有意思的自动化项目,客户需要在仓库的特定区域实现人员闯入检测,一旦检测到未经授权的人员,就要联动现场的PLC(可编程逻辑控制器)去控制声光报警器、关闭安全门,甚至暂停AGV小车。传统的方案要么是依赖昂贵的工业视觉相机加专用工控机,要么就是用网络摄像机把视频流送到云端服务器分析,前者成本太高,后者又担心网络延迟和隐私问题。
琢磨了一下,手头正好有闲置的树莓派和配套的AI摄像头模块,一个想法就冒出来了:能不能用树莓派直接在边缘端跑人员检测模型,然后把检测结果通过工业上最通用的Modbus协议,实时发送给现场的OpenPLC(一个开源的软PLC)呢?这样,整个系统就变成了一个低成本、高响应速度、且不依赖外部网络的独立边缘智能控制单元。
这个组合听起来有点跨界——一边是玩嵌入式AI和Python的树莓派,另一边是搞梯形图和工业通讯的PLC。但实际跑通后发现,这种“AI感知+工业执行”的架构,在不少轻量级自动化场景里特别实用,比如小型工厂的安全区域监控、智能仓储的人员管理,或者实验室的自动化安全联锁。它把复杂的视觉分析留在边缘设备完成,只把最关键的“有没有人”这个布尔量信号发给PLC,让PLC专心做它最擅长的逻辑控制和设备驱动。
接下来,我就把从硬件选型、环境搭建、模型部署到Modbus通讯集成的完整过程,以及中间踩过的几个坑,详细拆解一遍。如果你也想试试用树莓派和OpenPLC搞点智能控制,这篇内容应该能帮你省下不少折腾的时间。
2. 硬件与软件栈选型:为什么是它们?
工欲善其事,必先利其器。这个项目的核心是让树莓派“看见”并“告诉”PLC,所以硬件和软件的选择每一步都有讲究,不是随便抓个摄像头和库就能用的。
2.1 核心硬件:树莓派与AI摄像头的考量
主控我选择了Raspberry Pi 4B 4GB版本。3B+理论上也能跑,但考虑到要同时运行视觉推理和通讯服务,4B更强的CPU和更大的内存会更从容。更重要的是,它的USB 3.0接口和PCIe通道对于某些高性能摄像头模块至关重要。
摄像头的选择是重中之重。普通USB网络摄像头(比如罗技C920)最容易上手,用OpenCV的cv2.VideoCapture就能读取,但它的所有图像处理压力都压在树莓派的CPU上。做实时人员检测,帧率可能只能勉强跑到10FPS左右,CPU占用率会很高。
所以我最终选择了树莓派官方的高质量摄像头(Raspberry Pi High Quality Camera)搭配一个广角镜头,并通过CSI-2接口连接。这不是一个严格意义上的“AI相机”,但它有以下几个优势:
- 低延迟、高带宽:CSI-2是直接连接到树莓派SoC的专用接口,图像数据吞吐量大,延迟远低于USB摄像头。
- 可利用硬件加速:树莓派的GPU(VideoCore VI)和专用的图像处理管线(ISP)可以对CSI摄像头的数据进行硬件级的缩放、色彩转换等预处理,极大减轻CPU负担。
- 灵活的镜头选择:可以根据监控距离和视角更换镜头,我用的广角镜头能覆盖更大的区域。
当然,如果你追求极致的推理性能,可以考虑像Google Coral USB Accelerator(TPU加速棒)搭配普通USB摄像头,或者使用内置NPU的专用AI摄像头模组。但综合成本、易得性和生态,树莓派官方CSI摄像头是一个平衡性很好的选择。
2.2 软件生态:OpenPLC与Modbus的必然性
在PLC端,我选择了OpenPLC。它是一个开源的、符合IEC 61131-3标准的软PLC运行时。为什么不用传统的西门子、三菱PLC?原因很简单:成本和开放性。
- 零硬件成本:OpenPLC可以运行在树莓派(甚至是一台旧的电脑)上,本身就是一个强大的控制器。
- 完全开源:你可以深入查看其Modbus通讯栈的实现,遇到问题有地方可查。
- 标准兼容:它支持标准的梯形图(LD)、功能块图(FBD)等编程语言,工业控制逻辑的编写方式和传统PLC无异。
通讯协议毫无悬念地选择了Modbus TCP。在工业环境里,Modbus就像普通话,几乎所有的设备(PLC、HMI、传感器、驱动器)都支持。它简单、可靠、易于调试。相比于Modbus RTU(串口),Modbus TCP基于以太网,布线方便,速度更快,更适合树莓派和运行OpenPLC的工控机(或另一台树莓派)之间的通讯。
整个数据流是这样的:树莓派上运行的Python人员检测程序 -> 通过pymodbus库封装检测结果 -> 作为保持寄存器(Holding Register)或线圈(Coil)写入 -> OpenPLC作为Modbus TCP从站(Slave)接收并映射到其内部变量 -> OpenPLC的逻辑程序根据变量值执行相应的控制动作(如启动报警)。
这个架构清晰地将“感知”和“控制”解耦,树莓派只负责“看”和“报”,复杂的连锁逻辑、设备时序控制、安全处理全部交给专业的PLC环境,这才是符合工业实践的做法。
3. 树莓派端环境搭建与人员检测模型部署
让树莓派学会“看人”是整个项目的第一步。这里的关键不是追求最高的检测精度,而是在有限的算力下实现稳定、实时的推理。
3.1 基础系统与驱动配置
首先,安装树莓派操作系统(Raspberry Pi OS Lite 64位版本),并启用SSH和配置好网络。接着是摄像头驱动,对于CSI摄像头,需要在sudo raspi-config中启用Camera Interface。完成后,可以用libcamera-hello命令测试摄像头是否正常工作。
对于AI推理,我们需要一个高效的框架。我放弃了臃肿的TensorFlow,选择了PyTorch配合TorchVision。虽然Arm64的PyTorch安装稍麻烦,但它的API更友好,模型转换和部署也更灵活。更重要的是,我们可以利用TorchVision官方提供的、经过预训练的轻量级模型。
# 安装PyTorch和TorchVision (具体版本号请查阅PyTorch官网针对树莓派的最新安装指令) pip3 install torch torchvision --extra-index-url https://download.pytorch.org/whl/cpu # 安装OpenCV用于图像采集和预处理 pip3 install opencv-python-headless # 安装Modbus客户端库 pip3 install pymodbus注意:务必安装
opencv-python-headless,这个版本没有GUI相关的依赖,体积更小,更适合服务器或无头模式运行的树莓派。
3.2 轻量级人员检测模型的选择与优化
直接使用庞大的Faster R-CNN或YOLOv5s在树莓派上跑实时检测是不现实的。我们的目标是“检测人”,这是一个单类别的检测任务,可以大大简化模型。
我测试了两种方案:
- MobileNetV3-SSD Lite:这是一个经典的移动端检测架构。TorchVision的
detection模块里就有预训练好的ssdlite320_mobilenet_v3_large模型,它在COCO数据集上训练过,能检测包括“人”在内的80类物体。我们可以只保留“person”这个类别的输出。 - YOLOv5n:Ultralytics发布的YOLOv5 nano版本是迄今为止最轻量的YOLO模型之一。通过PyTorch Hub可以轻松加载,并且专门为边缘设备优化过。
经过实测,在树莓派4B上,输入图像尺寸调整为320x320时:
- MobileNetV3-SSD Lite:推理速度约120ms/帧,CPU占用率较高,但集成简单。
- YOLOv5n:推理速度约220ms/帧,但准确率,特别是对小目标和部分遮挡的人的检测,明显优于SSD Lite。
我最终选择了YOLOv5n。虽然单帧慢一点,但考虑到我们的应用场景(安全监控)对漏报的容忍度极低,准确率优先。4-5 FPS的检测速度对于“人员闯入”这种非瞬间事件来说,已经足够触发PLC联动了。
部署时,有几个关键的优化点:
- 模型预热:在循环开始前,先用一张空白图片跑一次推理,触发PyTorch的JIT编译和底层优化,避免第一次检测时的长时间延迟。
- 非极大值抑制(NMS)阈值调整:由于只检测“人”,可以适当提高NMS的
iou_threshold(比如从0.45调到0.6),让模型在一个人体可能产生多个重叠框时,更快地合并成一个,减少后处理时间。 - 置信度阈值动态调整:根据环境光照(白天/夜晚),可以微调检测的置信度阈值。白天光线好,阈值可以设高些(如0.7)以减少误报;夜晚或光线差时,可以适当降低(如0.5)以避免漏报,但同时需要在PLC逻辑端增加去抖动滤波。
import torch import cv2 # 加载模型 (首次运行会自动从网上下载) model = torch.hub.load('ultralytics/yolov5', 'yolov5n', pretrained=True) model.conf = 0.6 # 置信度阈值 model.iou = 0.6 # NMS IoU阈值 model.classes = [0] # 只检测'person'类 (COCO数据集中人的ID是0) # 预热模型 _ = model(torch.zeros(1, 3, 320, 320)) # 图像采集循环示例 cap = cv2.VideoCapture(0) # 对于CSI摄像头,可能需要使用GStreamer管道或libcamera while True: ret, frame = cap.read() if not ret: break # 推理 results = model(frame) # results.pandas().xyxy[0] 包含了检测框、置信度、类别信息 person_detected = len(results.pandas().xyxy[0]) > 0 # 将 person_detected 状态通过Modbus发送出去4. OpenPLC环境配置与Modbus从站设置
现在,我们需要让OpenPLC准备好接收来自树莓派的信号。我选择将OpenPLC Runtime安装在一台旧的x86工控机上,与树莓派处于同一局域网。当然,你也可以把它安装在树莓派本机上(通过容器或直接安装),实现“All in One”,但那样会争夺计算资源,不推荐用于要求高的场景。
4.1 OpenPLC的安装与基础项目创建
从OpenPLC官网下载适用于你操作系统(Linux/Windows)的编辑器(OpenPLC Editor)和运行时(OpenPLC Runtime)。安装完成后,首先在运行时的机器上启动OpenPLC Runtime服务,它会提供一个Web配置界面(默认端口8080)。
- 创建新项目:在OpenPLC Editor中,创建一个新的“连续功能图(CFC)”或“梯形图(LD)”项目。我们只需要一个简单的布尔变量来接收人员检测状态。
- 定义变量:在变量声明区,定义一个布尔型变量,命名为
PersonDetected。这个变量将作为我们与外部世界通讯的接口。 - 编写简单逻辑:在程序编辑区,可以拖拽一个常开触点,关联到
PersonDetected变量,后面连接一个线圈输出,命名为AlarmOutput。这样,当PersonDetected为True时,AlarmOutput就为True。你可以把这个输出映射到实际的物理输出点(如果OpenPLC连接了硬件IO模块),或者仅作为内部标志用于更复杂的逻辑。 - 设置Modbus映射:这是最关键的一步。在OpenPLC Editor的“资源”或“设置”中,找到Modbus映射表。我们需要将
PersonDetected这个内部变量,映射到Modbus的某个地址空间。通常,离散量输入(Coils, 可读可写)的地址范围是0xxxx,保持寄存器(Holding Registers, 可读可写)是4xxxx。对于简单的布尔信号,映射到一个Coil地址更符合习惯,例如地址00001。
4.2 配置OpenPLC为Modbus TCP从站
在OpenPLC Runtime的Web管理界面(通常是http://<runtime_ip>:8080)进行配置:
- 进入“Slave Devices”设置。
- 添加一个新的Modbus设备,选择类型为“Modbus TCP Slave”。
- 设置从站参数:
Device Name: 例如 “AI_Camera_Link”。Port: Modbus TCP标准端口是502,确保防火墙开放此端口。Slave ID: 在Modbus TCP中,这个ID有时被忽略或用作单元标识符,通常设为1即可。Address: 保持0.0.0.0以监听所有网络接口。Scan Rate: 轮询速率,设为100ms足够快。
- 关键步骤:数据映射。在这里,你需要将之前在编辑器中映射的Modbus地址(如Coil 00001),与OpenPLC内部的
PersonDetected变量绑定。这个界面通常提供一个表格,让你选择Modbus地址类型(Coil/Register)、起始地址,并关联一个内部变量。 - 保存并启动:保存设置,然后在“Dashboard”页面启动PLC运行时。此时,OpenPLC就开始在502端口监听Modbus TCP请求,并准备读写我们映射的变量了。
为了测试Modbus从站是否工作,我强烈推荐在调试阶段使用Modbus Poll(主站模拟器)和Modbus Slave(从站模拟器)这对黄金组合。你可以先用Modbus Slave模拟一个从站,用Modbus Poll去读写,熟悉协议。然后再用Modbus Poll直接连接我们刚配置好的OpenPLC,尝试写入Coil 00001的值,观察OpenPLC Web界面里PersonDetected变量的状态是否随之改变。这一步能极大避免后续集成时的通讯类问题。
5. PyModbus通讯集成与数据同步逻辑
树莓派端的程序检测到人后,需要可靠地将这个状态“推送”到OpenPLC。我们使用pymodbus库来实现Modbus TCP客户端的功能。
5.1 建立稳定的Modbus TCP连接
首先,要处理网络的不稳定性。工业现场网络也可能有波动,我们的程序不能因为一次连接失败就崩溃。
from pymodbus.client import ModbusTcpClient import time import logging logging.basicConfig(level=logging.INFO) PLC_IP = '192.168.1.100' # OpenPLC运行时所在机器的IP PLC_PORT = 502 COIL_ADDRESS = 0 # Modbus地址,对应Coil 00001。注意pymodbus通常使用0基地址。 def create_modbus_client(): """创建并连接Modbus客户端,包含重试机制""" retry_count = 0 max_retries = 5 while retry_count < max_retries: try: client = ModbusTcpClient(PLC_IP, port=PLC_PORT) if client.connect(): logging.info(f"成功连接到Modbus服务器 {PLC_IP}:{PLC_PORT}") return client else: logging.warning(f"连接失败,第{retry_count+1}次重试...") time.sleep(2) retry_count += 1 except Exception as e: logging.error(f"连接时发生异常: {e}") time.sleep(2) retry_count += 1 logging.error(f"无法连接到Modbus服务器,已达到最大重试次数{max_retries}") return None client = create_modbus_client()5.2 状态写入与防抖滤波设计
直接每次检测到人就立刻写入True,没检测到就立刻写入False,会产生大量频繁的通讯请求,并且任何单帧的误检都会导致PLC误动作。因此,必须在树莓派端加入**软件防抖(Debounce)**逻辑。
防抖的逻辑是:只有当“检测到人”的状态持续一定时间(比如1秒),我们才认为这是一个有效事件,并通知PLC。同样,只有当“未检测到人”的状态持续一定时间,才认为人确实离开了。
import collections class PersonDetectorDebouncer: def __init__(self, window_size=5, positive_threshold=4): """ Args: window_size: 状态缓存队列长度,对应时间窗口 (window_size * 检测周期)。 positive_threshold: 判定为‘有人’所需的最小正样本数。 """ self.state_buffer = collections.deque(maxlen=window_size) self.window_size = window_size self.positive_threshold = positive_threshold self.last_reported_state = False def update(self, current_detection): """更新当前检测状态,并返回经过防抖处理后的稳定状态""" self.state_buffer.append(current_detection) if len(self.state_buffer) < self.window_size: # 窗口未填满,不改变状态 return self.last_reported_state positive_count = sum(self.state_buffer) # 判断逻辑:窗口内正样本数超过阈值,则判定为有人;否则无人。 new_state = positive_count >= self.positive_threshold # 只有状态发生变化时才返回新状态并记录 if new_state != self.last_reported_state: self.last_reported_state = new_state return new_state else: return None # 状态未变化,返回None # 在主循环中使用 detector = PersonDetectorDebouncer(window_size=5, positive_threshold=4) last_write_time = time.time() write_interval = 0.2 # 最小写入间隔,避免过度通讯 while True: # ... 图像采集和模型推理,得到 person_detected (True/False) stable_state = detector.update(person_detected) if stable_state is not None and (time.time() - last_write_time) > write_interval: # 状态稳定且发生了变化,且距离上次写入已过最小间隔 try: # 写入Modbus Coil。注意:write_coil的address参数是0基。 # 写入 True 或 False response = client.write_coil(COIL_ADDRESS, stable_state) if response.isError(): logging.error(f"写入Modbus失败: {response}") else: logging.info(f"状态已更新为: {stable_state}") last_write_time = time.time() except Exception as e: logging.error(f"通讯异常: {e}") # 可以考虑在这里加入重连逻辑 client = create_modbus_client()这个设计确保了系统的抗干扰能力。即使模型在某几帧里误检了飞过的鸟或晃动的窗帘,只要不是连续发生,就不会触发PLC动作。
6. 系统联调与实战中的关键问题排查
把所有部分连接起来,才是挑战的开始。下面是我在联调过程中遇到的几个典型问题及其解决方法。
6.1 通讯超时与连接中断
问题现象:树莓派程序运行一段时间后,日志开始报“Connection timed out”或“Modbus IOException”,之后检测状态无法再更新到PLC。
排查过程:
- 检查网络:
pingOpenPLC主机,持续一段时间,看是否有丢包或延迟激增。工业现场网线质量、交换机端口都可能有问题。 - 检查OpenPLC Runtime状态:登录Web界面,查看PLC是否仍在“Running”状态,有时复杂的逻辑错误可能导致运行时崩溃。
- 检查防火墙:确认OpenPLC主机(尤其是Windows系统)的防火墙是否阻止了502端口,或者是否只允许了特定IP。可以将防火墙暂时关闭测试。
- 分析
pymodbus日志:启用DEBUG级别日志,发现连接断开后,客户端没有自动重连机制。
解决方案:
- 在树莓派程序中实现一个心跳机制和自动重连。除了上面的
create_modbus_client重连函数,还可以定期(比如每读写10次)尝试一个简单的读操作(client.read_coils),如果失败则触发重连流程。 - 在OpenPLC端,可以考虑使用其“Watchdog”功能,或者编写一个简单的通讯健康检查逻辑。
6.2 Modbus地址映射错误
问题现象:用Modbus Poll能读写成功,但树莓派程序写入后,OpenPLC内的变量状态无变化。
排查过程:
- 确认地址:这是最常见的问题。Modbus地址有“1基”和“0基”之分。协议规范通常是1基(如00001),但很多软件库(包括
pymodbus)使用0基地址。我程序中写的COIL_ADDRESS = 0,对应的是Coil 00001。需要确保OpenPLC映射表里设置的地址也是从1开始(00001),而不是0。 - 确认数据类型:我们传输的是布尔量,用的是Coil(0xxxx)。如果错误地映射到了保持寄存器(4xxxx),那么用
write_coil是写不进去的。在OpenPLC的Modbus设备映射配置中,必须明确选择“Coil (0x)”类型。 - 使用Modbus Poll交叉验证:用Modbus Poll同时连接OpenPLC,并监控同一个Coil地址。当树莓派程序写入时,观察Modbus Poll里的值是否同步变化。如果Modbus Poll里变了而OpenPLC变量没变,问题就在OpenPLC内部的变量映射上;如果Modbus Poll里都没变,问题就在树莓派程序或网络链路上。
6.3 树莓派推理性能波动与误报
问题现象:白天系统运行稳定,夜晚误报警增多;或者当树莓派同时运行其他任务时,检测延迟变大,导致防抖逻辑失效。
排查过程:
- 监控资源:使用
htop命令监控树莓派的CPU和内存使用率。发现当推理帧率下降时,CPU占用率常达到100%。 - 分析图像质量:夜晚光线不足,摄像头采集的图像噪声大,对比度低,模型置信度下降。为了不漏检,程序降低了置信度阈值,但同时也引入了更多背景误报(如将昏暗处的物体轮廓误认为人)。
解决方案:
- 优化树莓派进程优先级:使用
nice和ionice命令,赋予检测程序更高的CPU调度优先级和IO优先级,减少被其他后台任务干扰。nice -n -10 python3 person_detection_modbus.py - 引入动态参数调整:根据环境光传感器读数或图像平均亮度,动态调整模型的置信度阈值和防抖窗口参数。夜晚时,可以适当增大防抖窗口(
window_size),要求更长时间持续的检测才判定为有效。 - 硬件辅助:如果条件允许,可以为树莓派增加一个小型散热风扇,防止因过热降频导致性能下降。也可以考虑使用带红外补光的摄像头,提升夜间图像质量。
6.4 OpenPLC逻辑处理与响应延迟
问题现象:树莓派状态已更新,PLC输出动作有明显延迟(超过1秒)。
排查过程:
- 检查OpenPLC扫描周期:在OpenPLC项目设置中,有一个“循环时间”或“扫描周期”。如果这个值设置得太大(比如默认的100ms),PLC处理完一段逻辑后,会等待这个周期结束才开始下一次扫描。对于需要快速响应的应用,可以将其适当调小(如20ms)。
- 优化PLC程序逻辑:检查梯形图程序是否过于复杂,包含了大量的定时器、计数器或复杂的数学运算,这些都会增加单个扫描周期的时间。确保人员检测触发的逻辑路径尽可能简洁。
- 使用立即输出:在某些OpenPLC实现中,标准的线圈输出可能在扫描周期结束时才统一更新到物理输出。查阅OpenPLC文档,看是否有“立即输出”指令,可以在逻辑执行中即刻更新输出状态,减少延迟。
通过以上四个层面的联调和问题解决,一个稳定可靠的“树莓派AI视觉检测+OpenPLC控制”的边缘智能系统就真正搭建完成了。这个方案的成功,关键在于理解每个组件的边界和特性,并在它们之间建立一条简单、健壮的数据通道。它证明了用低成本的开源硬件和软件,完全可以构建出满足特定工业需求的智能控制系统。
