云计算百科
云计算领域专业知识百科平台

C#深度解析国产PLC协议:打通AI视觉与PLC控制的全链路实战

在这里插入图片描述

做工业上位机开发的朋友应该都有体会:AI视觉检测跑通不难,难的是最后和PLC联动这一公里。尤其是近几年国产PLC在3C、锂电、包装线的用量越来越大,很多团队要么只会用Modbus凑合用,要么对厂商自定义协议摸不清门路,结果就是视觉数据传得慢、指令卡壳、现场丢信号的问题层出不穷。

去年我做3C产品缺陷分拣项目的时候,从0到1啃了汇川H5U的以太网协议,用C#实现了完整的视觉-PLC交互闭环,实测单轮交互延迟稳定在15ms以内,连续跑了三个月没出过大问题。今天把整个实现过程、优化思路和踩过的坑全部分享出来。

一、前期准备:环境与整体架构

动手写代码之前,先把系统角色和数据流理清楚,这是后续所有交互的基础。

1. 硬件与软件环境

  • 上位机:基于C# .NET 6开发,负责视觉结果中转、协议解析和PLC指令下发,VS2022开发环境
  • PLC:汇川H5U-1614MTD,国产主流以太网型PLC,支持自有高速协议与Modbus TCP双模式
  • AI视觉端:YOLOv8检测模型,推理完成后输出JSON格式结果,包含OK/NG标识、缺陷类型、像素坐标
  • 组网:所有设备通过千兆交换机组成局域网,固定IP分配,避免DHCP波动导致断连

2. 完整交互闭环

整个产线检测的控制逻辑分为四步,是典型的半双工握手交互:

  • PLC工位传感器到位 → 向指定寄存器写入触发标志
  • 上位机轮询到触发信号 → 通知相机拍照,调用AI模型推理
  • 检测完成 → 上位机将结果按协议格式写入PLC对应寄存器
  • PLC读取到结果与完成标志 → 执行分拣、剔除或停机动作
  • AI视觉模块C国产PLCAI视觉模块C国产PLC#mermaid-svg-GdPGPzdji96uvCc9{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GdPGPzdji96uvCc9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GdPGPzdji96uvCc9 .error-icon{fill:#552222;}#mermaid-svg-GdPGPzdji96uvCc9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GdPGPzdji96uvCc9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GdPGPzdji96uvCc9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GdPGPzdji96uvCc9 .marker.cross{stroke:#333333;}#mermaid-svg-GdPGPzdji96uvCc9 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GdPGPzdji96uvCc9 p{margin:0;}#mermaid-svg-GdPGPzdji96uvCc9 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GdPGPzdji96uvCc9 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-GdPGPzdji96uvCc9 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GdPGPzdji96uvCc9 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-GdPGPzdji96uvCc9 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-GdPGPzdji96uvCc9 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-GdPGPzdji96uvCc9 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-GdPGPzdji96uvCc9 .sequenceNumber{fill:white;}#mermaid-svg-GdPGPzdji96uvCc9 #sequencenumber{fill:#333;}#mermaid-svg-GdPGPzdji96uvCc9 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-GdPGPzdji96uvCc9 .messageText{fill:#333;stroke:none;}#mermaid-svg-GdPGPzdji96uvCc9 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GdPGPzdji96uvCc9 .labelText,#mermaid-svg-GdPGPzdji96uvCc9 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-GdPGPzdji96uvCc9 .loopText,#mermaid-svg-GdPGPzdji96uvCc9 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-GdPGPzdji96uvCc9 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-GdPGPzdji96uvCc9 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-GdPGPzdji96uvCc9 .noteText,#mermaid-svg-GdPGPzdji96uvCc9 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-GdPGPzdji96uvCc9 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GdPGPzdji96uvCc9 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GdPGPzdji96uvCc9 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-GdPGPzdji96uvCc9 .actorPopupMenu{position:absolute;}#mermaid-svg-GdPGPzdji96uvCc9 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-GdPGPzdji96uvCc9 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-GdPGPzdji96uvCc9 .actor-man circle,#mermaid-svg-GdPGPzdji96uvCc9 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-GdPGPzdji96uvCc9 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}写入触发标志位(拍照请求)触发相机采集+模型推理返回检测结果与坐标写入结果数据+完成标志位读取结果并执行控制动作

    二、分步实操一:国产PLC以太网协议拆解

    很多人以为国产PLC都用Modbus,其实头部厂商基本都有自己的以太网私有协议,批量读写效率比标准Modbus TCP高30%以上,还支持位操作、断点续传等特性。只是官方文档不对外公开,需要自己抓包拆解。

    当时为了摸透帧结构,我用Wireshark抓了整整两天,对比官方组态软件的收发数据,才把完整报文格式理清楚。

    1. 基础报文结构

    汇川H5U以太网协议的报文分为帧头、长度域、功能域、数据域、校验位五部分:

    • 帧头:固定2字节 0xAA 0xBB,作为报文起始标识,用于粘包分包处理
    • 长度域:2字节,记录功能域+数据域的总长度,采用小端模式存储
    • 功能码:1字节,0x01=读寄存器,0x02=写单寄存器,0x03=批量写寄存器
    • 数据域:变长,根据功能码携带起始地址、寄存器数量、读写数据等内容
    • 校验位:2字节CRC16-Modbus校验,低字节在前,高字节在后

    2. 典型读写报文示例

    读取D0~D9共10个寄存器的请求报文:
    AA BB 06 00 01 00 00 0A 00 XX XX

    • AA BB:帧头标识
    • 06 00:数据长度6字节(小端模式,对应十进制6)
    • 01:读寄存器功能码
    • 00 00:起始地址D0(对应十六进制0x0000)
    • 0A 00:读取数量10个
    • XX XX:CRC16校验值

    PLC返回的响应报文结构一致,数据域部分会替换为对应寄存器的数值。

    三、分步实操二:C# 实现协议核心通信层

    通信层直接用原生Socket实现,不依赖任何第三方驱动,好处是体积小、可控性强、没有授权费用,适合轻量化上位机项目。

    1. 客户端基础封装

    public class DomesticPlcClient
    {
    private Socket _socket;
    private readonly string _ip;
    private readonly int _port = 502;
    private readonly byte[] _frameHeader = { 0xAA, 0xBB };

    public bool IsConnected => _socket?.Connected ?? false;

    public DomesticPlcClient(string ip)
    {
    _ip = ip;
    }
    }

    2. 连接与超时控制

    工业现场网络波动大,超时参数不能太长也不能太短,实测1000ms是比较均衡的取值,既不会因为短暂波动误判断连,也不会卡住主线程太久。

    public bool Connect(int timeoutMs = 1000)
    {
    try
    {
    _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _socket.SendTimeout = timeoutMs;
    _socket.ReceiveTimeout = timeoutMs;
    _socket.Connect(_ip, _port);
    return true;
    }
    catch (Exception ex)
    {
    Console.WriteLine($"PLC连接失败:{ex.Message}");
    return false;
    }
    }

    3. 读寄存器核心实现

    这里最容易踩坑的是字节序。大部分国产PLC是小端存储,和C#默认一致,但部分老款机型是大端,读出来数值不对的时候优先调换字节顺序。

    public ushort[] ReadRegisters(ushort startAddr, ushort count)
    {
    if (!IsConnected) throw new InvalidOperationException("PLC未连接");

    // 拼接请求报文
    int dataLength = 4; // 功能码1 + 地址2 + 数量2 = 5?不对,功能码1+地址2+数量2=5字节,这里按实际协议调整
    byte[] sendBuffer = new byte[2 + 2 + 5 + 2]; // 帧头2 + 长度2 + 功能数据5 + 校验2
    Buffer.BlockCopy(_frameHeader, 0, sendBuffer, 0, 2);
    BitConverter.GetBytes((ushort)5).CopyTo(sendBuffer, 2);
    sendBuffer[4] = 0x01;
    BitConverter.GetBytes(startAddr).CopyTo(sendBuffer, 5);
    BitConverter.GetBytes(count).CopyTo(sendBuffer, 7);

    // 追加CRC校验
    ushort crcValue = Crc16(sendBuffer, 0, sendBuffer.Length 2);
    BitConverter.GetBytes(crcValue).CopyTo(sendBuffer, sendBuffer.Length 2);

    // 收发数据
    _socket.Send(sendBuffer);
    byte[] recvBuffer = new byte[1024];
    int recvLen = _socket.Receive(recvBuffer);

    // 基础校验
    if (recvLen < 6 || recvBuffer[0] != 0xAA || recvBuffer[1] != 0xBB)
    throw new IOException("无效响应帧头");
    if (Crc16(recvBuffer, 0, recvLen 2) != BitConverter.ToUInt16(recvBuffer, recvLen 2))
    throw new IOException("CRC校验失败");

    // 解析寄存器数据
    ushort[] result = new ushort[count];
    for (int i = 0; i < count; i++)
    {
    result[i] = BitConverter.ToUInt16(recvBuffer, 6 + i * 2);
    }
    return result;
    }

    4. CRC16校验算法

    国产PLC基本都用标准Modbus CRC16算法,多项式0xA001,直接复用即可。

    private ushort Crc16(byte[] data, int offset, int length)
    {
    ushort crc = 0xFFFF;
    for (int i = offset; i < offset + length; i++)
    {
    crc ^= data[i];
    for (int j = 0; j < 8; j++)
    {
    if ((crc & 0x0001) != 0)
    crc = (ushort)((crc >> 1) ^ 0xA001);
    else
    crc >>= 1;
    }
    }
    return crc;
    }

    写寄存器的逻辑和读大同小异,替换功能码、在数据域追加写入值即可,这里就不重复贴代码了。

    四、分步实操三:AI视觉数据与PLC寄存器映射

    通信打通只是第一步,怎么把AI输出的结构化数据,转换成PLC能识别的寄存器数值,才是联动的核心。这部分没有统一标准,完全取决于和PLC工程师约定的地址分配。

    1. 寄存器地址分配约定

    我项目里用的地址分配方案,覆盖了绝大多数分拣场景的需求,大家可以参考:

    寄存器地址功能说明数据类型读写方向
    D0 拍照触发标志位 bool PLC写,上位机读
    D1 检测完成标志位 bool 上位机写,PLC读
    D2 检测结果:0=OK,1=NG ushort 上位机写,PLC读
    D3-D4 缺陷X坐标(32位浮点数) float 上位机写,PLC读
    D5-D6 缺陷Y坐标(32位浮点数) float 上位机写,PLC读
    D7 缺陷类型编码 ushort 上位机写,PLC读

    2. AI结果转换写入逻辑

    AI输出JSON结果后,先解析成实体类,再按协议格式转换成寄存器数值,批量写入PLC。

    public void WriteAiDetectResult(AiResult result)
    {
    ushort[] regData = new ushort[7];
    regData[0] = (ushort)(result.IsFinish ? 1 : 0);
    regData[1] = (ushort)(result.IsDefect ? 1 : 0);
    regData[2] = result.DefectTypeCode;

    // float拆分为两个16位寄存器
    byte[] xBytes = BitConverter.GetBytes(result.PositionX);
    byte[] yBytes = BitConverter.GetBytes(result.PositionY);
    regData[3] = BitConverter.ToUInt16(xBytes, 0);
    regData[4] = BitConverter.ToUInt16(xBytes, 2);
    regData[5] = BitConverter.ToUInt16(yBytes, 0);
    regData[6] = BitConverter.ToUInt16(yBytes, 2);

    // 从D1地址开始批量写入7个寄存器
    WriteMultipleRegisters(1, regData);
    }

    3. 握手机制避免丢数据

    千万不要直接循环覆盖寄存器,一定要做双向握手。我见过很多项目丢数据、误动作,根源都是没做握手。

  • PLC工位到位 → D0置1
  • 上位机检测到D0=1 → 触发拍照推理,完成后写入结果,D1置1
  • PLC检测到D1=1 → 读取结果,执行动作,然后D0置0
  • 上位机检测到D0=0 → D1置0,等待下一轮触发
  • 这套机制虽然简单,但能彻底避免数据覆盖、漏处理的问题。

    五、分步实操四:实时性与稳定性优化

    工业现场的通信,稳定永远是第一位的,在此基础上再追求速度。分享几个实测有效的优化手段。

    1. 长连接+心跳检测

    不要每次读写都建连断开,TCP三次握手的开销在高频场景下非常明显。保持长连接,每5秒读一次固定寄存器做心跳,检测到断开自动重连。
    实测长连接比短连接的响应速度快5~10倍,分拣线这种每秒触发好几次的场景,差距尤其明显。

    2. 批量读写替代单寄存器操作

    尽量一次读写多个寄存器,不要循环单个操作。比如写7个寄存器,一次批量写入的耗时和写1个几乎没差,但循环7次耗时会翻好几倍。
    我项目里把原来的单寄存器逐个写,改成批量写入后,单轮写入耗时从20ms降到了3ms以内。

    3. 异步通信不阻塞UI

    上位机一般都带操作界面,同步Socket调用会直接卡住UI线程。所有通信操作都放到Task.Run里执行,或者用Socket异步API,保证界面流畅。

    4. 信号防抖过滤

    现场传感器、PLC输入都可能有电气抖动,触发信号要做防抖处理,比如连续两次读到标志位为1,才判定为有效触发,避免误拍误动作。

    六、问题排查:国产PLC通信常见踩坑

    这部分都是我实际踩过的坑,很多新手卡一两天都找不到原因,列出来帮大家避坑。

    1. 寄存器地址偏移

    国产PLC的地址规则特别乱,有的是0-based,有的是1-based,有的文档写十进制,有的写十六进制。
    读不到数据的时候,先把起始地址±1试试,90%的问题都出在这里。汇川H5U的D寄存器是0-based,而信捷部分机型是1-based,这点要特别注意。

    2. 字节序与数据格式

    16位寄存器的高低字节、32位浮点数的寄存器顺序,不同厂商、不同系列都可能不一样。
    读出来的数值明显不对的时候,先调换两个字节的顺序试试;float数值不对,就调换两个寄存器的前后顺序,基本都能解决。

    3. TCP连接数限制

    国产PLC的以太网连接数普遍不高,很多型号最多只支持8个TCP连接,多了就会拒绝连接或者踢掉旧连接。
    上位机一定要复用连接,不要开多个线程分别建连,否则现场很容易出现时通时断的玄学问题。

    4. 超时与重连机制

    工业现场网线松动、交换机重启都是常事,一定要做超时重连。建议设置3次重试,每次间隔500ms,重试失败再触发完整重连逻辑,不要一次失败就直接报错。

    七、总结

    国产PLC的协议解析看起来门槛高,其实核心就是报文拼接、校验计算、数据转换三步。把帧结构理清楚,把握手机制做扎实,再把稳定性优化到位,AI视觉和PLC的交互闭环其实并不难。

    相比第三方OPC服务器或者商业驱动,自己用C#实现协议的好处是可控性强、部署简单、没有授权成本,还能根据项目需求做定制优化,特别适合嵌入式上位机、轻量化设备这类场景。

    最后提醒一句,工业现场调试一定要做好安全防护,PLC写入指令前先做空载测试,确认逻辑没问题再联机运行,避免误动作损坏设备。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » C#深度解析国产PLC协议:打通AI视觉与PLC控制的全链路实战
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!