简介:这份资源是一套面向工业自动化与上位机开发者的 C# 以太网通信例程,由国外工程师编写,用于实现 AB PLC(Allen Bradley 可编程逻辑控制器)与 PC 之间的数据交互。适合已具备一定 C# 基础、希望掌握 PLC 实时监控、读写与调试方法的开发者参考学习。压缩包共 45 个文件,约 308KB,以 vb 源码、resx/resources 资源文件、exe 可执行程序、manifest 清单及 sln 解决方案文件为主,另含少量 xml、dll 与配置文件,完整保留了 Visual Studio 工程结构与调试信息。目前已有 261 人学习下载。例程围绕建立连接、设备发现、数据映射、读写操作、错误处理与连接关闭等关键环节展开,读者可借此理解如何借助 RSNetworx for Ethernet/IP 或 ODX 等库完成通信,并参考其代码组织方式与排错思路,快速搭建自己的 PLC 通信程序。
1. 从一份老外写的 AB PLC 以太网通讯 C# 例程说起
工控上位机开发里,AB PLC(Allen-Bradley,罗克韦尔自动化)和 PC 之间的以太网通讯是个绕不开的活。很多现场用 ControlLogix、CompactLogix 系列,上位机要么用官方 FactoryTalk,要么自己写 C# 程序读写标签。官方方案贵、重、不灵活,自己写又卡在协议细节上——CIP、EtherNet/IP、标签寻址、会话注册,每一步都有坑。这份资源是一份老外编写的 C# 例程压缩包,专门演示 AB PLC 与 PC 通过以太网通讯的完整实现。它解决的核心问题是:让你不用从零啃 EtherNet/IP 规范,直接拿到一份能跑通的 C# 代码骨架,改改 IP 和标签名就能对接现场设备。适合做上位机、SCADA、数据采集的 C# 开发者,也适合刚接触 AB PLC 通讯、想搞明白底层怎么走的人。下面我按「这是什么 → 怎么用 → 坑在哪」的顺序,把这份例程拆开讲透。
2. 拆开例程看结构:C# 侧到底怎么跟 AB PLC 搭上话
2.1 先搞清楚 EtherNet/IP 和 CIP 的关系
AB PLC 走的是 EtherNet/IP 协议,底层是标准 TCP/UDP,上层跑 CIP(Common Industrial Protocol)。CIP 又分显式消息和隐式消息:显式消息用来读写标签、查设备信息,走 TCP 端口 44818;隐式消息用来传 I/O 数据,走 UDP。这份 C# 例程做的是显式消息通讯,也就是上位机主动发请求读写 PLC 里的标签值。
CIP 报文结构分几层:封装头(Encapsulation Header)固定 24 字节,里面带命令码、长度、会话句柄;后面跟 CIP 数据区,包含服务码、请求路径、数据。请求路径里最关键的是标签名怎么编码——AB PLC 的标签寻址用 ANSI 字符串加逻辑段,不是简单拼个字符串就行。例程里一般会封装一个BuildCIPPath方法,把标签名转成 CIP 能识别的路径格式。
常见做法是:先发 Register Session 命令建立会话,拿到会话句柄;然后发 Send RR Data 命令,把 CIP 请求塞进去;PLC 返回响应后解析数据区。整个过程是请求-响应模式,同步阻塞,适合低频采集,不适合高频实时控制。
提示:EtherNet/IP 的 TCP 端口是 44818,UDP 端口也是 44818。防火墙别只开 TCP,有些设备发现和隐式消息走 UDP。
2.2 例程里的核心类和方法拆解
老外写的这份例程,结构一般比较清晰,核心类大概这么几个:
EthernetIPClient:负责 TCP 连接、会话注册、发送和接收原始字节。CIPClient:封装 CIP 请求构造和响应解析。TagReader/TagWriter:对外暴露ReadTag、WriteTag方法。Program:控制台入口,演示连接、读标签、写标签、断开。
下面我按典型实现给一段可抄的代码骨架,不是原封不动复制,而是把关键逻辑抽出来,方便你对照例程理解。
// 建立 TCP 连接并注册会话 public class EthernetIPClient { private TcpClient _tcpClient; private NetworkStream _stream; private uint _sessionHandle; public void Connect(string ip, int port = 44818) { _tcpClient = new TcpClient(); _tcpClient.Connect(ip, port); _stream = _tcpClient.GetStream(); RegisterSession(); } private void RegisterSession() { // 封装头 24 字节 + 注册会话数据 byte[] request = new byte[28]; // 命令码 0x0065 = Register Session request[0] = 0x65; request[1] = 0x00; // 长度 4 request[2] = 0x04; request[3] = 0x00; // 协议版本 1 request[4] = 0x01; request[5] = 0x00; // 后面填充会话句柄 0、状态 0、上下文等 _stream.Write(request, 0, request.Length); byte[] response = new byte[28]; _stream.Read(response, 0, response.Length); // 会话句柄在响应第 4-7 字节 _sessionHandle = BitConverter.ToUInt32(response, 4); } }这段代码的逻辑说明:RegisterSession是 EtherNet/IP 通讯的第一步,不注册会话后面所有请求都会被拒。封装头里命令码0x0065是固定值,长度字段写 4 表示后面跟 4 字节数据(协议版本和选项)。响应里第 4 到第 7 字节是 PLC 分配的会话句柄,后续每个请求都要带上这个句柄。参数方面,IP 填 PLC 的以太网地址,端口默认 44818,一般不用改。
2.3 读标签和写标签的请求构造
读标签用 CIP 服务码0x4C(Read Tag),写标签用0x4D(Write Tag)。请求路径里要放标签名的 ANSI 字符串,前面加长度和字符集标识。
// 构造读标签请求 private byte[] BuildReadTagRequest(string tagName) { // CIP 路径:ANSI 字符串段 byte[] tagBytes = Encoding.ASCII.GetBytes(tagName); int pathLength = tagBytes.Length + 2; // 加长度字节和填充 if (pathLength % 2 != 0) pathLength++; // 字对齐 byte[] cip = new byte[2 + pathLength + 2]; cip[0] = 0x4C; // Read Tag 服务码 cip[1] = (byte)(pathLength / 2); // 路径字数 cip[2] = (byte)tagBytes.Length; // 标签名长度 Array.Copy(tagBytes, 0, cip, 3, tagBytes.Length); // 后面跟要读的元素个数,UINT16 cip[cip.Length - 2] = 0x01; cip[cip.Length - 1] = 0x00; return cip; }逻辑说明:CIP 路径按「字」对齐,每个字 2 字节,所以路径长度要算成字数。标签名前面先放一个长度字节,再放 ASCII 字符。最后两个字节是要读的元素个数,读单个值就写 1。写标签请求类似,只是在路径后面跟数据类型和要写入的值。参数上,标签名要和 PLC 里定义的完全一致,大小写敏感,带空格或特殊字符的标签要特别小心。
注意:AB PLC 的标签名区分大小写,
MyTag和mytag是两个不同标签。现场调试时先在 RSLogix 或 Studio 5000 里确认标签名拼写。
2.4 响应解析和数据类型映射
PLC 返回的响应里,数据区前面有几个字节的状态信息,后面才是实际值。不同数据类型长度不一样:BOOL 占 1 字节(但 CIP 里按位打包),SINT 1 字节,INT 2 字节,DINT 4 字节,REAL 4 字节浮点。例程里一般会写一个ParseResponse方法,根据请求时指定的类型去解析。
// 解析读标签响应 private object ParseReadResponse(byte[] response, Type targetType) { // 响应前 4 字节是接口句柄和超时,接着是 CIP 响应 int cipStart = 24 + 4; // 封装头 + 接口句柄等 byte status = response[cipStart]; if (status != 0) throw new Exception($"CIP 错误码: 0x{status:X2}"); int dataStart = cipStart + 4; // 跳过服务码和状态 if (targetType == typeof(int)) return BitConverter.ToInt32(response, dataStart); if (targetType == typeof(float)) return BitConverter.ToSingle(response, dataStart); if (targetType == typeof(bool)) return response[dataStart] != 0; return null; }逻辑说明:响应里先检查 CIP 状态码,非 0 说明 PLC 拒绝了请求,常见错误码0x05是路径找不到,0x13是数据不够。数据起始位置在封装头 24 字节加接口句柄 4 字节再加 CIP 头 4 字节之后。参数上,targetType要和 PLC 里标签的实际类型匹配,读 DINT 却按 INT 解析会得到错误值。浮点数要注意字节序,AB PLC 是小端,C# 的BitConverter在 x86/x64 上也是小端,直接转就行。
3. 把例程跑起来:环境、连接、读写三步走
3.1 开发环境和依赖准备
这份例程是 C# 写的,大概率是 .NET Framework 或 .NET Core 控制台项目。你本地需要装 Visual Studio 或者 Rider,.NET 版本看例程的 csproj 文件。如果例程用的是 .NET Framework 4.x,Windows 上直接跑;如果是 .NET Core 3.1 或 .NET 5+,跨平台也能编译。
不需要额外 NuGet 包,因为例程自己实现了 EtherNet/IP 协议栈。如果你要自己扩展,可以考虑用libplctag的 C# 封装,但这份例程的价值就在于不依赖第三方库,纯手写协议,方便你理解底层。
连接前确认几件事:PC 和 PLC 在同一网段,能 ping 通;PLC 的以太网模块已配置好 IP;防火墙放行 44818 端口。常见做法是先用ping和telnet测端口,再跑例程。
# 测试 PLC 是否可达 ping 192.168.1.10 # 测试 44818 端口是否开放(Windows) Test-NetConnection -ComputerName 192.168.1.10 -Port 44818参数说明:IP 换成你现场 PLC 的实际地址。如果 ping 通但端口不通,检查 PLC 的以太网模块配置和防火墙。
3.2 修改配置并建立连接
例程里一般会把 IP 和标签名写在Program.cs或者配置文件里。找到类似plcIp、tagName的变量,改成你现场的值。
static void Main(string[] args) { var client = new EthernetIPClient(); client.Connect("192.168.1.10"); // 改成你的 PLC IP var reader = new TagReader(client); var value = reader.ReadTag("MyTag", typeof(int)); Console.WriteLine($"读取值: {value}"); reader.WriteTag("MyTag", 123); Console.WriteLine("写入完成"); client.Disconnect(); }逻辑说明:先Connect建立 TCP 连接并注册会话,然后ReadTag读一个 DINT 标签,再WriteTag写回去。参数上,标签名要和 PLC 里一致,类型要和标签定义匹配。如果连接失败,先看异常信息是超时还是被拒绝,超时多半是网络或防火墙,被拒绝可能是 PLC 没开以太网通讯。
提示:第一次跑建议先只读不写,确认读到的值和 PLC 里监控的一致,再试写操作。写错标签可能影响现场设备。
3.3 读写验证和常见返回码
跑通之后,你会看到控制台打印读到的值。如果读出来是 0 或者乱码,先确认标签类型和解析类型是否匹配。PLC 里 DINT 是 32 位整数,REAL 是 32 位浮点,BOOL 是位。读 BOOL 时 CIP 返回的是打包的位,例程里一般会单独处理。
常见 CIP 返回码对照:
| 状态码 | 含义 | 排查方向 |
|---|---|---|
| 0x00 | 成功 | 正常 |
| 0x05 | 路径找不到 | 标签名拼写、大小写、作用域 |
| 0x13 | 数据不足 | 请求长度或元素个数不对 |
| 0x1E | 数据类型不匹配 | 读写类型和标签定义不一致 |
| 0x26 | 权限不足 | 标签只读或会话未注册 |
这张表建议存下来,现场调试时对着看能省不少时间。路径找不到是最常见的,九成是标签名写错了,或者标签在 Program 作用域下但你没带 Program 前缀。
3.4 用 Wireshark 抓包验证协议交互
如果例程跑不通,或者你想确认底层报文对不对,用 Wireshark 抓包是最直接的办法。过滤条件写tcp.port == 44818,能看到 Register Session、Send RR Data 的完整交互。
抓包时重点看几个地方:Register Session 响应里会话句柄是不是非 0;Send RR Data 请求里的 CIP 路径字节是不是你期望的标签名;PLC 返回的状态码是多少。常见做法是先在 Wireshark 里确认请求发出去了,再看响应,能快速定位是 PC 侧构造错了还是 PLC 侧拒绝了。
# Wireshark 过滤表达式 tcp.port == 44818 && ip.addr == 192.168.1.10参数说明:ip.addr换成 PLC 的 IP。如果抓不到包,检查网卡选对了没,或者 PC 和 PLC 之间有没有交换机镜像口。
4. 避坑排查:AB PLC 以太网通讯里最容易翻车的五件事
4.1 会话注册失败,后面全挂
现象:程序一启动就抛异常,或者所有读写请求都返回错误。原因:Register Session 没成功,会话句柄是 0,后续请求全被 PLC 拒绝。解决:先单独测 Register Session,确认响应里会话句柄非 0。如果一直是 0,检查 TCP 连接是否真的建立了,有些 PLC 要求先发 Register Session 才能发其他命令。另外注意会话有超时,长时间不通讯要重新注册。
4.2 标签名大小写和路径前缀写错
现象:返回 0x05 路径找不到。原因:AB PLC 标签名大小写敏感,且不同作用域下标签路径不一样。Controller 作用域的标签直接写名字,Program 作用域的标签要写Program:MainProgram.TagName。解决:在 Studio 5000 里复制标签名,别手敲。带 Program 前缀的标签,路径构造时要把冒号也编码进去。
4.3 数据类型不匹配导致读出乱码
现象:读 DINT 读出个负数或者超大数,读 REAL 读出个极小值。原因:请求时指定的类型和 PLC 标签实际类型不一致,或者解析时偏移量算错了。解决:先在 PLC 里确认标签数据类型,C# 侧用对应的BitConverter方法解析。DINT 用ToInt32,REAL 用ToSingle,INT 用ToInt16。注意 BOOL 在 CIP 里是按位打包的,不能直接按字节读。
4.4 防火墙和网段问题导致连不上
现象:ping 通但 TCP 连接超时。原因:Windows 防火墙拦了出站或入站 44818,或者 PC 和 PLC 不在同一网段但路由没配好。解决:先Test-NetConnection测端口,不通就关防火墙试。跨网段的话确认网关和路由,有些 PLC 不支持跨网段通讯,需要配路由表。现场血泪经验:别在 PLC 正在生产的时候改网络配置。
4.5 高频读写把 PLC 通讯资源占满
现象:程序跑一段时间后 PLC 响应变慢,甚至其他上位机连不上。原因:CIP 显式消息是请求-响应模式,每次读写都建一次交互,高频轮询会占满 PLC 的通讯任务。解决:降低轮询频率,或者用隐式消息(UDP)传 I/O 数据。常见做法是批量读,一次请求读多个标签,减少交互次数。AB PLC 的显式消息连接数有限,别开太多并发。
5. 进阶:批量读写和连接复用的具体技巧
例程跑通之后,下一步就是把它用到实际项目里。单个标签读写够演示,但现场往往要读几十上百个标签。这时候有两个方向可以优化:批量读写和连接复用。
批量读写在 CIP 里对应 Multiple Service Packet,一次请求里塞多个 Read Tag 服务,PLC 返回一个打包响应。这样能把几十次交互压成一次,效率提升明显。实现上,请求数据区里先放服务个数,然后每个子请求带自己的路径和长度。解析响应时按顺序拆包。常见做法是每批不超过 20 个标签,太多会超过 TCP 包大小,反而要分片。
连接复用是指整个程序生命周期只注册一次会话,所有读写共用一个TcpClient和NetworkStream。别每次读写都新建连接,那样握手开销大,还容易把 PLC 的连接数占满。我一般会在EthernetIPClient里加个IsConnected属性,断线了自动重连,重连时重新注册会话。
// 批量读标签的请求构造思路 private byte[] BuildMultipleReadRequest(string[] tagNames) { // 每个子请求:服务码 0x4C + 路径长度 + 路径 + 元素个数 // 外层:服务个数 + 各子请求偏移 // 具体字节拼接略,核心是算好每个子请求的偏移量 // 响应里按偏移依次解析 }这段代码的关键是偏移量计算,每个子请求的起始位置要按字对齐,响应解析时按同样顺序读。参数上,标签数量控制在 20 以内,类型尽量一致,混类型也能处理但解析逻辑会复杂些。
验证批量读写是否生效,还是靠 Wireshark。抓包看一次 Send RR Data 里是不是带了多个服务,PLC 响应里是不是一次返回了所有值。如果 PLC 返回 0x1E 或者部分成功,检查是不是某个标签类型不匹配拖累了整批。
从那以后我每次对接新 PLC,都强制先跑一遍单标签读写,确认会话、路径、类型都对,再上批量。这个习惯帮我省了至少三次现场返工。希望帮到你。
本文还有配套的精品资源,点击获取