
简介这份资源是一套面向工业自动化与上位机开发者的C#以太网通信例程由国外工程师编写用于解决AB PLCAllen Bradley可编程逻辑控制器与PC之间通过Ethernet/IP进行数据交互的问题。适合具备一定C#基础、希望掌握PLC实时监控、读写与调试技能的开发者参考学习。压缩包共45个文件约308KB以vb与vbproj项目源码、resx与resources资源文件、exe与dll可执行及库文件、sln解决方案与suo用户配置为主另含manifest、xml、pdb等辅助文件完整保留了Visual Studio工程结构。资源围绕连接建立、设备发现、数据映射、读写操作、错误处理、持续监控与连接关闭等关键环节展开读者可借此理解通信库API的调用方式与工程组织思路并在此基础上调试优化提升通信效率与稳定性。目前已有261人学习下载适合作为AB PLC以太网通信入门的实践参考。1. AB PLC 与 PC 走以太网通讯一份 C# 例程能省掉你多少试错车间里一台 Allen-Bradley ControlLogix 或 CompactLogix 正在跑产线上位机这边要用 C# 把产量、节拍、报警状态实时抓过来或者反过来把配方参数写下去。很多人第一反应是找现成驱动结果翻到一份老外写的 C# 例程压缩包里面是 EtherNet/IP 的报文封装。问题来了这份例程到底能不能直接用、它绕过了哪些商业库、AB PLC 的以太网口和普通 TCP 有什么不一样。这篇就把 AB PLC 与 PC 通过以太网通讯的 C# 实现路径拆开从协议栈讲到能跑起来的最小代码再到现场最容易翻车的几个点。适合已经会 C#、手上有 AB PLC、不想为一个小项目买整套 OPC 授权的一线开发和上位机工程师。2. 先搞懂 AB PLC 以太网通讯到底在传什么2.1 EtherNet/IP 不是普通 TCPCIP 才是核心AB PLC 的以太网口跑的是 EtherNet/IP底层确实是标准以太网和 TCP/UDP但上面套了一层 CIPCommon Industrial Protocol。你可以把它理解成TCP 负责把字节送到CIP 负责告诉 PLC「我要读哪个标签、读几个、什么类型」。老外那份 C# 例程之所以有价值就是它把 CIP 的封装、会话注册、标签读写报文都手写出来了不依赖罗克韦尔的商业 DLL。CIP 报文分两类一类是显式消息Explicit Messaging走 TCP 44818 端口用来读写标签、拿设备信息适合上位机做数据采集和参数下发另一类是隐式消息Implicit Messaging走 UDP 2222 端口用于实时 I/O 数据交换周期性强但对上位机来说实现复杂。绝大多数 C# 上位机场景用显式消息就够了。你要读一个 DINT 数组或者写一个 REAL走 TCP 44818 完全能覆盖。这里有个容易混的点AB PLC 的标签名不是随便写的。ControlLogix 里标签有作用域程序级标签要带 PROGRAM 前缀比如PROGRAM:MainProgram.MyTag。例程里如果只处理了控制器级标签你拿去读程序级标签就会返回错误码。这个后面避坑章节会细说。2.2 为什么不用现成 OPC Server偏要自己写 C#现场常见做法是装一个 KEPServer 或者 RSLinx然后用 OPC DA/UA 去读。这套方案稳定、生态好但有两个现实问题一是授权成本一个小项目可能就为了读十几个标签买一套 OPC 不划算二是部署依赖客户现场机器不一定让你装一堆服务。自己用 C# 写 CIP 通讯编译出来就是一个 exe拷过去就能跑没有额外安装步骤。当然自己写也有代价。CIP 的报文结构、会话句柄、超时重连都要自己管。老外例程通常把底层封装成一个类你调用ReadTag、WriteTag就行但一旦现场网络抖动或者 PLC 重启会话失效的处理逻辑得自己补。我一般会在例程基础上加一层连接状态机和重试队列不然产线一停一启上位机就假死了。从选型角度看如果你的项目标签数量在几百以内、刷新周期要求 100ms 以上、又不想引入商业组件自己写 C# 走 EtherNet/IP 显式消息是合理的。如果要求 10ms 级同步或者大量标签高频刷新还是老老实实上 OPC UA 或者专用通讯卡。2.3 一份典型 C# 例程里都有什么老外写的这类例程结构通常不复杂但该有的都有。核心文件一般包括一个 EtherNetIP 通讯类负责 TCP 连接和 CIP 报文组装一个 CIP 报文构造/解析类处理标签读写请求一个数据转换工具类处理 AB PLC 的原子类型和 C# 类型的映射再加一个控制台或 WinForm 的调用示例。你要关注的是它有没有处理这几件事会话注册Register Session有没有发读标签用的是 CIP 服务码 0x4C 还是 0x52写标签有没有区分不同数据类型的长度返回状态码有没有解析。如果例程里这些都有那它就是一个可用的起点。如果只贴了一段发原始字节的代码那你还得自己补很多。数据类型映射是另一个重点。AB PLC 的 DINT 是 32 位有符号整数对应 C# 的 intREAL 是 32 位浮点对应 floatBOOL 在 CIP 里是按位打包的读回来要自己拆。字符串更麻烦AB 的 STRING 是结构体前 4 个字节是长度后面才是字符数据。例程如果没处理这些你读回来的数据就是乱的。3. 把 C# 例程跑起来的最小步骤3.1 环境准备和 PLC 侧要确认的参数动手之前先在 PLC 侧确认几件事。第一PLC 的以太网 IP 要固定别用 DHCP上位机代码里写死 IP 最省事。第二确认你要读写的标签已经建好并且在控制器作用域或者你知道的程序作用域里。第三如果 PLC 有安全策略或者防火墙确认 44818 端口是通的。PC 这边装 Visual Studio新建一个 .NET Framework 或者 .NET 6 的控制台项目都行。例程如果是老版本 .NET用 .NET Framework 4.7.2 兼容性最好。不需要额外 NuGet 包因为 CIP 报文是自己拼的只用 System.Net.Sockets。先在命令行验证网络通不通ping 192.168.1.10 telnet 192.168.1.10 44818ping 通只说明网络层没问题telnet 能连上 44818 才说明 PLC 的 EtherNet/IP 服务在监听。如果 telnet 连不上先查 PLC 的以太网配置和网线别急着调代码。3.2 会话注册第一步发什么报文EtherNet/IP 的通讯要先注册会话。你发一个 Register Session 报文PLC 返回一个会话句柄后续所有请求都要带上这个句柄。报文结构是 24 字节的封装头加 4 字节的协议版本和选项。// 构造 Register Session 报文 byte[] BuildRegisterSession() { byte[] packet new byte[28]; // 命令码 0x0065 表示 Register Session packet[0] 0x65; packet[1] 0x00; // 长度 4表示后面跟 4 字节数据 packet[2] 0x04; packet[3] 0x00; // 会话句柄注册时填 0 // packet[4..7] 0 // 状态码请求时填 0 // packet[8..11] 0 // 发送方上下文可自定义这里填 0 // packet[12..19] 0 // 选项填 0 // packet[20..23] 0 // 协议版本 1 packet[24] 0x01; packet[25] 0x00; // 选项标志 0 packet[26] 0x00; packet[27] 0x00; return packet; }这段代码的关键是命令码 0x0065 和协议版本 1。发出去之后PLC 返回的报文里第 4 到第 7 字节就是会话句柄你要把它存下来后面每个请求都填进去。如果返回的状态码不是 0说明注册失败常见原因是端口不对或者 PLC 没开 EtherNet/IP 服务。参数说明命令码固定 0x0065长度字段是数据部分的字节数注册时是 4协议版本目前都是 1。发送方上下文可以填任意值PLC 会原样返回你可以用它来匹配请求和响应。3.3 读标签CIP 请求怎么拼读标签用 CIP 服务码 0x4CRead Tag Service。请求里要带标签名和要读的元素个数。标签名按 ANSI 字符串编码前面加一个长度字节。// 构造读标签请求tagName 如 MyDINT byte[] BuildReadTagRequest(string tagName, ushort elementCount) { // CIP 部分服务码 1 字节 请求路径长度 1 字节 路径 元素个数 2 字节 byte[] tagBytes System.Text.Encoding.ASCII.GetBytes(tagName); int pathLength tagBytes.Length 2; // 加 2 是因为路径里有 0x91 和长度字节 byte[] cip new byte[2 pathLength 2]; cip[0] 0x4C; // Read Tag cip[1] (byte)pathLength; cip[2] 0x91; // 符号段标识 cip[3] (byte)tagBytes.Length; Array.Copy(tagBytes, 0, cip, 4, tagBytes.Length); cip[4 tagBytes.Length] (byte)(elementCount 0xFF); cip[5 tagBytes.Length] (byte)(elementCount 8); return cip; }路径里的 0x91 是 CIP 符号段标识告诉 PLC 后面跟的是标签名。标签名长度用一个字节表示所以标签名不能超过 255 个字符实际也不会那么长。元素个数是小端序读一个就填 1。发请求的时候要把这段 CIP 数据包在封装头里封装头的命令码用 0x006FSend RR Data会话句柄填注册时拿到的值。PLC 返回的数据里前面是 CIP 响应头后面才是标签的值。你要根据标签的数据类型去解析对应字节数。3.4 写标签和数据类型对应关系写标签用服务码 0x4EWrite Tag Service。请求里除了标签名和元素个数还要带数据类型码和实际数据。AB PLC 的类型码BOOL 是 0x00C1SINT 是 0x00C2INT 是 0x00C3DINT 是 0x00C4REAL 是 0x00CA。// 写一个 DINT 标签 byte[] BuildWriteDINTRequest(string tagName, int value) { byte[] tagBytes System.Text.Encoding.ASCII.GetBytes(tagName); int pathLength tagBytes.Length 2; // 服务码 路径长度 路径 类型码(2) 元素个数(2) 数据(4) byte[] cip new byte[2 pathLength 2 2 4]; cip[0] 0x4E; cip[1] (byte)pathLength; cip[2] 0x91; cip[3] (byte)tagBytes.Length; Array.Copy(tagBytes, 0, cip, 4, tagBytes.Length); int offset 4 tagBytes.Length; cip[offset] 0xC4; // DINT 类型码低字节 cip[offset 1] 0x00; // 高字节 cip[offset 2] 0x01; // 元素个数 1 cip[offset 3] 0x00; // 写入值小端序 cip[offset 4] (byte)(value 0xFF); cip[offset 5] (byte)((value 8) 0xFF); cip[offset 6] (byte)((value 16) 0xFF); cip[offset 7] (byte)((value 24) 0xFF); return cip; }写 REAL 的时候类型码换成 0xCA数据部分用BitConverter.GetBytes(floatValue)拿到的 4 字节。写 BOOL 要注意CIP 里 BOOL 是按字节传的但实际 PLC 里是按位存储写的时候一般用 SINT 类型码加 0 或 1或者直接写 BOOL 类型码 0xC1 加一个字节。参数说明类型码必须和 PLC 里标签的实际类型一致写错会返回 0x1E 或者 0x13 错误。元素个数写 1 表示写单个元素。数据字节序是小端和 x86 一致所以BitConverter在 Windows 上直接可用。4. 现场调试最容易翻车的几个地方4.1 标签名带 PROGRAM 前缀读不到现象读控制器级标签正常读程序级标签返回错误码 0x05 或者 0x1E。原因ControlLogix 的程序级标签在 CIP 路径里需要带PROGRAM:程序名.前缀例程如果只按普通标签名拼路径PLC 找不到。解决在标签名前拼上PROGRAM:MainProgram.注意大小写要和 PLC 里一致。如果不确定先在 RSLogix 或者 Studio 5000 里看标签的完整路径。4.2 会话句柄过期导致后续请求全部失败现象程序跑一段时间后所有读写都返回错误重启程序又好了。原因PLC 重启或者网络断开后原来的会话句柄失效但代码里还在用旧句柄发请求。解决在每次请求返回错误码时判断是否需要重新注册会话。我一般会在通讯类里加一个EnsureSession方法检测到句柄无效就重新发 Register Session然后重试当前请求。4.3 读数组时元素个数和返回长度对不上现象读一个 DINT[10] 数组返回的数据长度不对解析出来只有前几个值是对的。原因CIP 响应里数据部分是连续排列的但如果你请求的元素个数和实际数组长度不一致PLC 可能只返回部分数据或者报错。解决读数组时元素个数填数组实际长度解析时按元素个数 × 单元素字节数去取。DINT 数组就是 10 × 4 40 字节。注意返回报文里可能还有填充字节要按 CIP 响应头里的长度字段来截取。4.4 网络抖动时 TCP 连接假死现象网线拔掉再插上程序不报错但也不刷新数据了。原因TCP 连接在物理断开时不一定立刻抛异常Socket 可能还处于连接状态但实际已经发不出数据。解决加心跳机制定期发一个轻量请求比如读设备信息如果连续几次超时就主动关闭 Socket 重连。另外设置 Socket 的ReceiveTimeout和SendTimeout别用默认的无限等待。4.5 写 REAL 时字节序搞反现象写下去 1.5PLC 里显示成一个很大的数或者很小的数。原因REAL 的 4 字节在 CIP 里是小端序但如果你用BitConverter.GetBytes之后又手动反转了字节就会出错。解决Windows 上BitConverter本身就是小端直接发就行不要再做Array.Reverse。如果是在大端平台或者跨平台场景用BitConverter.IsLittleEndian判断一下。5. 让这份例程真正能上产线的几个改造5.1 把同步请求改成带超时的异步封装老外例程大多是同步的Socket.Send和Receive在 UI 线程里调用会卡界面在网络不好时还会长时间阻塞。我一般会把它包成Taskbyte[]用Task.Run或者Socket的异步方法再加一个CancellationToken控制超时。async Taskbyte[] SendRequestAsync(byte[] request, int timeoutMs) { using var cts new CancellationTokenSource(timeoutMs); // 把同步 Send/Receive 包到 Task 里超时抛异常 return await Task.Run(() { _socket.Send(request); byte[] buffer new byte[4096]; int len _socket.Receive(buffer); Array.Resize(ref buffer, len); return buffer; }, cts.Token); }这样上层调用可以await超时了就走重连逻辑。参数timeoutMs根据现场网络质量设一般 2000 到 5000 毫秒。注意Socket.Receive返回的是一次收到的字节数CIP 响应可能分多次到达严格来说要循环读到封装头里的长度字段满足为止。例程如果没处理这个大数据量读数组时可能丢包。5.2 加一层标签配置表别把标签名写死在代码里现场调试时标签名经常变写死在代码里每次都要重新编译。我习惯用一个 JSON 或者 CSV 配标签名、类型、刷新周期程序启动时加载。class TagConfig { public string Name { get; set; } public string Type { get; set; } // DINT, REAL, BOOL public int PollMs { get; set; } }然后根据Type决定解析字节数和类型码。这样现场改标签不用动代码改配置文件重启就行。对于标签多的项目还可以按刷新周期分组快的 100ms 一组慢的 1000ms 一组避免所有标签挤在一个循环里。5.3 用错误码定位问题别只看异常CIP 响应里的状态码是排查问题的关键。0x00 是成功0x05 是路径找不到0x1E 是数据类型不匹配0x13 是数据量不对。把这些码翻译成中文提示现场排查效率会高很多。string GetCipError(byte status) { return status switch { 0x00 成功, 0x05 路径错误检查标签名和PROGRAM前缀, 0x13 数据量不匹配, 0x1E 数据类型不匹配, _ $未知错误 0x{status:X2} }; }我一般会在日志里把请求报文和响应报文的前几十字节打出来配合状态码看基本能定位到是标签名问题还是类型问题。这个习惯帮我省了很多来回猜的时间。5.4 验证方法先用第三方工具对一遍自己写的代码读出来的值对不对别只信自己的程序。可以先用 KEPServer 或者免费的 EtherNet/IP 测试工具读同一个标签对比数值。如果第三方工具读出来是 123你的程序读出来也是 123说明解析没问题。如果对不上先查字节序和类型码。另一个验证方法是写一个已知值下去然后在 Studio 5000 的标签监视里看。比如写 DINT 标签值为 100PLC 里显示 100说明写报文正确。这个方法最直接能快速排除是读的问题还是写的问题。5.5 长期运行要加日志和重连统计产线上的上位机可能一跑就是几个月没有日志出了问题根本没法查。我会在通讯类里加一个简单的日志队列记录每次重连的时间、错误码、重试次数。不用太复杂写到一个文本文件里按天切分就行。重连统计能帮你判断网络质量。如果一天重连几十次那就要查网线、交换机或者 PLC 的负载而不是只改代码。我见过一个现场重连频繁是因为交换机端口协商成了半双工换了一根网线就好了。这种问题代码层面看不出来但日志里的重连频率会提醒你。这份老外例程的价值在于它把 EtherNet/IP 显式消息的底层细节摊开了你可以按自己的需求改。但直接拿来上产线上面这些改造基本都得补。我自己的习惯是先跑通读一个 DINT再跑通写一个 DINT然后加数组、加 REAL、加 BOOL最后加配置表和重连。一步一步来比一上来就搞几十个标签稳得多。希望帮到你。本文还有配套的精品资源点击获取