十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零开发AB PLC通信协议:CIP/PCCC协议解析与Python实战

从零开发AB PLC通信协议:CIP/PCCC协议解析与Python实战 1. 项目缘起为什么需要自己动手搞AB PLC协议在工业自动化这个行当里ABAllen-Bradley的PLC可编程逻辑控制器就像车间里的“大脑”地位举足轻重。但很多时候我们这些做系统集成、数据采集或者MES制造执行系统开发的会遇到一个挺头疼的问题怎么让上位机、SCADA数据采集与监控系统或者我们自己写的应用跟这些AB PLC“说上话”官方当然有解决方案比如罗克韦尔自家的FactoryTalk、KEPServerEX OPC服务器或者一些第三方的驱动库。但用过的都知道要么是授权费用不菲要么是灵活性受限要么是部署起来一堆依赖在特定场景下比如轻量级边缘计算、定制化协议网关、老旧系统改造显得特别笨重。这时候自己动手开发一套AB PLC的通信协议栈就从“可选项”变成了“必选项”。这活儿听起来挺硬核像是资深工程师的专属领域但实际拆解下来核心就是理解AB那套封闭但又自成体系的通信规则然后用代码把它“翻译”成我们能用的数据。我最近刚完成一个项目需要从多台不同系列的AB PLC包括CompactLogix、Micro800系列里实时读取生产数据并推送到云端数据库。受限于成本和架构没法上全套的OPC方案于是硬着头皮把AB的几个主流协议主要是CIP和PCCC的变种摸了一遍攒了不少经验和教训。这篇文章我就把自己从零开始折腾AB PLC协议开发的过程、核心原理、踩过的坑以及一些实用的代码片段做个系统的梳理。目标不是给你一个能直接拷贝的万能库那也不现实协议细节和PLC型号、固件版本强相关而是给你一张“地图”和一套“工具”让你知道这条路该怎么走遇到障碍该怎么绕过去。无论你是想做个简单的数据采集工具还是开发一个专用的协议转换网关希望这些内容都能帮到你。2. 协议家族概览AB PLC的“语言体系”在跟AB PLC对话之前得先搞清楚它到底会说哪几种“方言”。AB的通信协议不是一个单一的东西而是一个随着产品线演进形成的家族。主要可以分为两大类较新的、面向对象的CIP协议族和较老的、基于消息的PCCC协议族。理解它们的适用场景是选型的第一步。2.1 CIP现代AB设备的通用语CIPCommon Industrial Protocol通用工业协议是罗克韦尔自动化提出的基于对象模型的工业通信协议。你可以把它理解成工业领域的“HTTPSOAP/REST”它定义了一套标准的服务、对象和数据类型。在AB的世界里EtherNet/IP和ControlNet等网络都是基于CIP的应用层协议。核心特点面向对象PLC中的一切程序、标签、模块都被抽象为对象Object每个对象有类Class、实例Instance和属性Attribute。比如一个Program对象类下有多个程序实例每个实例有状态、大小等属性。显式消息Explicit Messaging这是最常用的方式用于非实时、一对一的请求/响应通信例如读取/写入标签值。它使用TCP/IP通常端口44818作为传输层协议格式相对规整。隐式消息Implicit Messaging用于实时性要求高的I/O数据交换通常通过UDP和组播实现是预配置好的周期性数据交换。我们做数据采集大部分时候打交道的是显式消息。服务Services对对象执行的操作比如Get_Attribute_Single读取单个属性、Set_Attribute_Single写入单个属性、Read_Tag、Write_Tag等。开发重点实现CIP显式消息协议关键在于构建正确的CIP封装报文。报文是分层封装的最外层是以太网帧/IP包/TCP段里面是封装头Encapsulation Header再里面才是CIP数据。封装头包含了会话句柄、命令代码如0x6F代表发送RRData用于CIP服务等信息。CIP数据部分则包含了服务请求路径Path和具体服务数据。2.2 PCCC/DF1传统PLC的“方言”PCCCProgrammable Controller Communication Commands是一套更早的、用于与SLC 500、MicroLogix、PLC-5等老型号PLC通信的命令集。DF1是其在串行链路RS-232/485上的物理层和数据链路层协议。后来为了在以太网上传输PCCC命令又衍生出了CIP PCCC模式即把PCCC命令作为数据负载封装在CIP显式消息中进行传输。核心特点基于命令/响应协议由一系列固定的命令码Command Code构成例如0x0F是读取数据文件0xAA是诊断状态。每个命令有固定的报文格式。文件号寻址数据不按标签名寻址而是按“文件类型”如N-整型F-浮点B-位和“文件号”、“元素号”来定位。比如N7:0表示整型文件7的第0个元素。常用于较老型号Micro800系列的部分型号、SLC 500等主要使用这种方式。开发重点实现PCCC协议需要准确构造命令帧。如果是串口DF1还要处理数据链路层的帧头、帧尾、校验以及可能的纠错重发机制。如果是基于以太网的CIP PCCC则需要先建立CIP会话然后将构造好的PCCC命令帧作为CIP服务的特定数据发送。注意选择哪种协议首要取决于目标PLC的型号和支持的通信方式。新型的ControlLogix、CompactLogix通常首选纯CIPEtherNet/IP。而Micro800或一些兼容性场景下可能需要使用CIP PCCC。最稳妥的方法是查阅PLC的具体手册或通过Wireshark抓包分析其与官方软件如Connected Components Workbench, Studio 5000 Logix Designer的通信过程。3. 核心实战从抓包分析到报文构造理论说再多不如动手抓个包看得明白。协议开发Wireshark是你的第一导师。下面我以读取一个ControlLogix PLC的标签值为例拆解整个流程。3.1 环境搭建与抓包准备硬件一台AB PLC如CompactLogix 5380一台安装有Studio 5000和Wireshark的工程师站电脑通过交换机连接在同一局域网。软件配置在Studio 5000中为PLC配置好IP地址并创建一个简单的测试程序里面定义几个不同类型的标签例如DINT_Test双整数、REAL_Test浮点数、BOOL_Test布尔量。抓包在Wireshark中选择正确的网卡开始抓包。然后在Studio 5000的“通信”窗口中在线连接PLC并打开“监视标签”视图观察标签值。此时Wireshark会捕获到所有的通信报文。3.2 解密一个典型的CIP读取标签请求过滤Wireshark的显示只关注与PLC IP地址相关的TCP流量例如tcp and ip.addr 192.168.1.10。你会看到类似下图的会话No. Time Source Destination Protocol Length Info 100 10.123456 192.168.1.100 192.168.1.10 TCP 66 49252 → 44818 [PSH, ACK] Seq1 Ack1 Winxxx Len0 101 10.123457 192.168.1.10 192.168.1.100 TCP 66 44818 → 49252 [ACK] Seq1 Ack45 Winxxx Len0 102 10.123458 192.168.1.100 192.168.1.10 TCP 1514 [TCP segment of a reassembled PDU] 103 10.123459 192.168.1.10 192.168.1.100 TCP 66 [ACK] 104 10.123460 192.168.1.10 192.168.1.100 TCP 1514 [TCP segment of a reassembled PDU]我们需要关注的是携带实际数据的数据包。右键点击一个看起来是请求的包比如No.102选择“追踪流” - “TCP流”。你会看到一个完整的请求-响应字节流。请求报文解剖简化版一个完整的CIP读取标签请求从外到内分为三层封装TCP/IP层标准的以太网帧、IP包头、TCP包头。目标端口是44818。CIP封装层Encapsulation Header命令Command0x6F。这代表“SendRRData”用于发送请求并要求回复数据。长度Length后面数据部分的长度。会话句柄Session Handle在会话建立时由PLC分配的一个4字节ID后续所有通信都要带上它。建立会话的命令是0x65RegisterSession。状态Status0x0000表示成功。发送者上下文Sender Context一个8字节的标识用于匹配请求和响应通常由客户端生成一个随机值。选项Options0x0000。CIP数据层Interface CIP Data接口句柄Interface Handle0x00000000表示CIP。超时Timeout0x0000表示无限等待。项数Item Count0x0002表示后面跟了两个地址项Item。地址项1Item 1类型0x0000Null Address长度0x0000。地址项2Item 2类型0x00B2Connected Data Item长度后面跟实际数据长度。CIP服务数据Service Data服务路径Path指向目标对象。例如读取标签的路径可能是0x20, 0x06, 0x24, 0x01。这需要解析0x20逻辑段端口号0x06背板0x24Class ID 这里是0x2436代表Symbol Class不完全是更常见的是直接指定标签名。实际上对于Logix标签路径通常包含“Symbol Instance”等信息更通用的方式是使用“ANSI Ext. Symbolic”片段类型0x91后跟编码的标签名。服务代码Service Code0x4C这是Read Tag服务的代码。请求数据包含要读取的标签名称如“MyTag”的ASCII编码可能带结构体成员访问MyTag.SubElement、元素数量、数据类型等信息。看到这里你可能有点晕确实手动构造这个路径是最复杂的一环。一个取巧的方法是先用官方软件正常通信抓取读取你目标标签的报文然后直接模仿它的路径结构。路径的解析规则CIP Segment本身就是一个专题。3.3 响应报文解析与错误处理PLC的响应报文结构类似在CIP封装层命令会是0x6FSendRRData的回复状态码需要检查。重点在CIP数据层的响应部分服务代码请求的代码0x80表示响应例如0xCC0x4C0x80。回复状态Reply Status一个字节0x00表示成功。其他值代表错误如0x05路径错误、0x08资源不可用、0x15数据类型不匹配等。必须处理这些状态码这是调试时最重要的信息。附加状态Extended Status可选的更详细错误信息。响应数据如果成功这里就是读取到的原始字节数据。你需要根据请求中指定的数据类型DINT, REAL, BOOL等来解析这些字节注意AB PLC通常使用小端字节序。实操心得不要试图一次性理解所有路径规则。先从模仿一个成功的抓包报文开始。用Python或C#写个小程序把抓到的请求报文字节数组原封不动地发出去如果能收到正确响应就成功了一大半。然后逐步替换其中的标签名字段观察变化。4. 关键难点与避坑指南自己开发协议肯定会遇到各种坑。下面是我总结的几个最典型的难点和解决方案。4.1 标签路径的编码最大的“黑盒”如前所述如何将“Program:MainProgram.MyDINTTag”这样的标签名转换成CIP报文里那一串十六进制路径是首要难题。解决方案抓包逆向最可靠的方法。用Studio 5000对不同类型标签基本类型、数组、结构体成员进行读写操作抓包对比路径部分的差异。你会发现对于全局标签和程序作用域标签路径是不同的。理解CIP路径段格式一个路径由多个段Segment组成。每个段第一个字节是类型/格式。常见的有0x91ANSI扩展符号段ANSI Extended Symbolic。后面跟一个字节的长度L再跟L个字节的标签名ASCII码。这是用于指定标签名的主要方式。0x28逻辑段Logical Segment用于指定端口、桥路等。0x20端口段Port Segment。段可以嵌套例如访问结构体成员路径 [标签名段] [成员名段]。使用已知的Class/Instance ID对于一些固定对象如Identity ObjectClass 0x01可以直接使用数字路径。但对于用户自定义标签必须用符号名。参考开源实现研究像pycomm3、cpppo这样的Python库或者libplctagC库的源代码看它们是如何构造路径的。这是快速学习的捷径。踩坑记录我曾试图直接根据文档手动计算路径结果屡屡失败。后来发现对于数组元素的访问如MyArray[5]路径中不仅需要标签名段还需要一个特殊的“索引段”其编码方式也有特定规则。最终通过对比抓包数据才确定了正确的格式。4.2 数据类型与字节序的“陷阱”AB PLC内部的数据表示和我们的PC可能不同。字节序Endianness大多数现代AB PLC基于Logix平台使用小端字节序Little-Endian。也就是说一个DINT32位整数值0x12345678在网络上传输的字节顺序是0x78, 0x56, 0x34, 0x12。你在解析响应数据时必须进行反转。数据类型代码Data Type CodesCIP协议为每种数据类型分配了一个16位的代码。例如0xC1代表BOOL0xC2代表SINT0xC3代表INT0xC4代表DINT0xCA代表REAL。在读写标签时有时需要在报文中指定这些类型代码。字符串类型AB的字符串是带有长度字节和最大长度字节的结构。例如一个STRING[40]类型在内存中可能占用42个字节2字节头部40字节数据。处理时需要特别小心。提示在构造写标签请求时数据部分必须严格按照PLC期望的格式和字节序排列。一个有效的测试方法是先用你的代码读取一个已知值的标签看看解析出来的字节数组是什么样子然后以此为模板构造写入请求。4.3 会话管理与连接保活CIP通信不是无状态的。它需要先建立一个会话Session。注册会话RegisterSession发送命令码为0x65的封装报文数据部分通常为空。PLC回复的报文中会包含一个4字节的会话句柄Session Handle。这个句柄在后续所有通信的封装头中都必须携带。保活Keep-AliveTCP连接本身有保活机制但CIP会话也可能超时。有些简单的客户端实现会忽略这一点但对于需要长期稳定连接的场景需要定期例如每分钟发送一个空的SendRRData命令或特定的“未连接列表”请求来保持会话活跃。更规范的做法是处理PLC可能发送的“连接超时”错误并实现重连机制。注销会话UnRegisterSession通信结束时发送命令码为0x66的报文并携带会话句柄以礼貌地释放PLC端的资源。4.4 性能优化多标签读取与异步处理频繁地单个读取标签效率极低尤其是需要监控几十上百个标签时。AB CIP协议支持多标签读取Read Tag Fragmented/Read Tag Service for multiple tags。原理在一个Read Tag服务请求中可以串联多个“请求路径”。每个路径指向一个标签。PLC会在一个响应报文中按顺序返回所有标签的数据和状态。好处极大减少了网络往返次数和报文开销提升采集效率数倍。限制单个请求的总大小是有限制的受PLC型号和固件限制通常几KB到几十KB如果标签太多或数据太大需要分批次读取。实现在构造请求时服务代码后的请求数据部分不再是单个标签的信息而是一个列表。列表中的每一项包含该标签请求的数据长度、路径信息。响应部分也会是一个对应的列表。对于高性能应用还需要考虑使用异步I/O和非阻塞Socket避免通信线程被阻塞影响整体程序响应。5. 从零搭建一个简易的AB PLC读取客户端Python示例理论说了这么多我们动手写一个最核心的片段用Python的socket库实现一个读取单个DINT标签的客户端。这里省略了错误处理的完整代码聚焦于核心流程。import socket import struct import time import random class SimpleABClient: def __init__(self, plc_ip, plc_port44818): self.plc_ip plc_ip self.plc_port plc_port self.session_handle 0 self.sock None def connect(self): 建立TCP连接并注册CIP会话 self.sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.settimeout(5.0) # 设置超时 self.sock.connect((self.plc_ip, self.plc_port)) self._register_session() def _register_session(self): 发送注册会话命令 (Command: 0x65) # CIP封装头 command 0x65 length 0 session_handle 0 status 0 sender_context random.randbytes(8) # 随机生成8字节发送上下文 options 0 # 构建封装报文 encap_header struct.pack(HHII8sI, command, length, session_handle, status, sender_context, options) self.sock.send(encap_header) response self.sock.recv(1024) # 解析响应获取会话句柄 (响应中的第5-8字节) if len(response) 24: resp_session_handle struct.unpack_from(I, response, 4)[0] self.session_handle resp_session_handle print(fSession registered. Handle: 0x{self.session_handle:08X}) else: raise Exception(Failed to register session.) def read_tag(self, tag_name): 读取一个标签假设为DINT类型 # 1. 构建CIP服务路径 (这里是一个简化示例实际路径更复杂) # 假设路径为: 端口 1, 然后标签名 # 实际中需要通过抓包分析确定准确的路径字节 path_segments b # 示例一个可能的路径构造非真实仅示意 # path_segments struct.pack(BB, 0x20, 0x01) # Port 1 segment # path_segments struct.pack(BB, 0x91, len(tag_name)) tag_name.encode(ascii) # Symbolic segment # 这里我们简化用一个从抓包中提取的固定路径字节数组代替 # 假设抓包得到读取MyDINT标签的路径数据是以下字节: # (这需要你从实际抓包中获取并替换) path_data bytes.fromhex(20 06 24 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 91 07 4D 79 44 49 4E 54) # 示意 # 2. 构建CIP服务数据 (Read Tag Service) service_code 0x4C # Read Tag request_path_size len(path_data) // 2 # 路径长度以16-bit字为单位 # 请求数据路径长度(字) 路径数据 request_data struct.pack(H, request_path_size) path_data # 完整的CIP数据项 cip_service struct.pack(B, service_code) request_data cip_service_length len(cip_service) # 3. 构建CIP连接项和数据项 # 连接地址项 (Null) item1_type 0x0000 item1_length 0x0000 item1 struct.pack(HH, item1_type, item1_length) # 连接数据项 (Connected Data) item2_type 0x00B2 item2_length cip_service_length item2 struct.pack(HH, item2_type, item2_length) cip_service # 4. 构建完整的CIP数据部分 interface_handle 0x00000000 timeout 0x0000 item_count 0x0002 cip_data struct.pack(IIHH, interface_handle, timeout, item_count, 0) item1 item2 cip_data_length len(cip_data) # 5. 构建CIP封装头 encap_command 0x6F # SendRRData encap_length cip_data_length encap_status 0x0000 sender_context random.randbytes(8) encap_options 0x00000000 encap_header struct.pack(HHII8sI, encap_command, encap_length, self.session_handle, encap_status, sender_context, encap_options) # 6. 组装并发送完整报文 full_packet encap_header cip_data self.sock.send(full_packet) # 7. 接收并解析响应 response self.sock.recv(4096) # 简化解析跳过封装头找到CIP响应数据 # 响应封装头长度固定24字节 cip_resp_data response[24:] # 解析CIP数据项... # 找到服务响应码 (应该是 0xCC 0x4C0x80) # 检查状态码 # 提取数据部分... # 这里省略详细的字节级解析过程 # 假设我们解析出数据部分是4字节的DINT (小端) data_bytes cip_resp_data[-4:] # 简化实际位置需计算 value struct.unpack(i, data_bytes)[0] # 小端有符号32位整数 return value def close(self): 注销会话并关闭连接 if self.session_handle: # 发送UnRegisterSession命令 (0x66) unreg_packet struct.pack(HHII8sI, 0x66, 0, self.session_handle, 0, random.randbytes(8), 0) self.sock.send(unreg_packet) self.sock.recv(1024) if self.sock: self.sock.close() # 使用示例 if __name__ __main__: client SimpleABClient(192.168.1.10) try: client.connect() tag_value client.read_tag(MyDINT) # 需要替换为真实的路径构造逻辑 print(fTag value read: {tag_value}) except Exception as e: print(fError: {e}) finally: client.close()重要说明上面的path_data是硬编码的示例绝对无法直接运行。你需要用自己抓包分析得到的真实路径字节串替换它。这个代码的价值在于展示了从建立会话、构造封装报文、发送请求到接收响应的完整框架。真正的开发工作大部分都花在如何根据不同的标签名和结构动态生成正确的path_data上。6. 进阶话题与工具推荐当你掌握了基础的单标签读写后可能会需要更高级的功能。6.1 处理数组和结构体数组读取数组需要指定元素数量。在请求中除了标签路径还需要包含请求的元素个数。对于大型数组可能需要进行分片读取Fragmented Read。结构体UDT读取整个结构体可以将其视为一个字节块。读取结构体成员则需要在标签路径后追加成员的偏移量或符号名路径。这通常通过添加额外的路径段来实现例如标签名.成员名。抓包分析是理解其编码规则的不二法门。6.2 写入操作与类型转换写标签Service Code0x4D的报文结构与读标签类似但在请求数据部分需要包含要写入的数据值。你必须确保数据值的字节序列与PLC中标签的数据类型完全匹配包括字节序和填充字节。对于BOOL类型写入单个位可能需要使用Write Tag Service的特定格式。6.3 协议开发辅助工具Wireshark必备配合CIP/EtherNet/IP解析插件默认可能已安装效果更佳。Python库pycomm3一个功能相对丰富的第三方库支持Logix PLC的CIP通信和部分PCCC。阅读其源码是很好的学习材料。cpppo另一个强大的库专注于CIP协议支持更底层的操作。python-snap7虽然主要针对西门子S7协议但其设计思路和异步处理方式值得借鉴。C/C库libplctag一个用C写的、支持多种PLC协议包括AB的库性能很好有各种语言的绑定。研究它的AB协议实现部分受益匪浅。串口/网络调试助手当开发DF1串口协议时串口调试助手是验证帧格式和校验码的利器。6.4 安全性考量工业协议开发安全往往被忽视但至关重要。隔离确保你的协议网关或采集程序运行在独立的、防火墙保护的网络区域。认证新型的AB PLC支持基于角色的访问控制。你的客户端可能需要支持密码认证CIP Security。这涉及到更复杂的密钥交换和报文加密。输入验证防止非法的标签名或数据导致PLC异常。资源管理避免过快的请求频率拖垮PLC的通信处理能力。自己动手开发AB PLC协议是一条充满挑战但回报丰厚的路。它让你摆脱了对商业OPC服务器或特定驱动库的依赖获得了对数据流的完全控制权特别适合嵌入式网关、定制化数据平台或对成本敏感的项目。整个过程的核心在于耐心地抓包分析、严谨地对照文档虽然AB的开放文档不多、以及大胆地编码测试。从最简单的读取一个整数开始逐步扩展到处理数组、结构体、多标签读写和错误重试每一步的突破都会带来巨大的成就感。记住你遇到的绝大多数问题答案都藏在Wireshark捕获的那些绿色和红色的数据包里。
返回列表