ARTICLE DETAIL

资讯详情

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

倍福ADS协议深度拆解:从数据包结构到现场排障

倍福ADS协议深度拆解:从数据包结构到现场排障 上周有个现场把我拉过去上位机读倍福PLC数据偶发超时有时重启软件就好过几个小时又犯。这类问题我在倍福ADS通讯协议相关的项目里见过太多次表面是“网络不稳定”实际是协议栈里的握手时序、路由表和报文缓冲区都在互相打架。这篇文章就直接从协议层往下拆把倍福ADS通讯协议从连接建立、身份确认到数据包字段、请求响应的完整旅程讲透。读完你会掌握三件事ADS连接建立的本质是什么一个数据包从发送到返回经过哪些层、每个字节起什么作用以及当连接异常、错误码出现时正确的排查顺序。适合正在用TwinCAT做上位机、准备对接倍福控制器的嵌入式工程师也适合那些已经开始用pyads或ADS.NET但遇到问题只能靠猜的同学。1. 起底ADS在倍福体系里到底负责什么1.1 你天天在用的“通讯”其实隔了三层先说结论ADS全称Automation Device Specification它是倍福TwinCAT系统里所有自动化组件之间交换数据的统一协议。你可以把它理解成倍福世界里的“HTTP”而TwinCAT Router就是那个“DNS 网关 路由器”的复合体。一个典型场景是C#上位机要读PLC里的一个INT标签。代码写得再简单背后也要走三层逻辑第一层是物理传输通常是Ethernet也可以是本地共享内存。第二层是AMSAutomation Message System它负责给消息寻址。就像快递在路上的分拣中心它只看AmsNetId和AMS Port不关心你寄的是衣服还是文件。第三层才是ADS服务本身它定义“读变量”“写变量”“注册通知”这些具体操作。大部分时候你用pyads或者ADS.NET根本感觉不到这三层的存在因为库把细节都封装好了。但当你遇到“连接超时”“返回错误码4132”或者要在非Windows设备上自研一个ADS网关时三层结构就是你定位问题的唯一地图。1.2 什么时候才需要关心协议细节我个人的判断标准是看你的工作是否停留在“调用API”层面。如果只是写十个八个变量那封装库够用但如果你要做下面任何一件事协议细节就是硬通货排查偶发性掉线、响应慢、请求堆积把TwinCAT数据接到OPC UA、MQTT或数据库在Linux、嵌入式或自定义硬件上实现ADS客户端分析现场抓包定位是哪一端在丢数据或拖时间。尤其是做网关的场景你没法依赖Windows上的TwinCAT Router必须自己处理端口48898上的裸数据包。这时候你对手里那个字节流的理解深度直接决定项目能不能按时上线。2. “握手”不是你以为的那种握手ADS连接建立的真相2.1 上位机和PLC之间先要各自拿到“身份证”ADS通讯里没有“用户名密码”它认的是AmsNetId和AMS Port。AmsNetId长6个字节写成十进制通常是四段加两段比如192.168.0.1.1.1。前4字节常常和IP地址相似但千万别当成IP用它是独立的地址体系。后面两字节一般是路由器实例标识。每个通信节点还有一个AMS Port相当于进程里的端口号。TwinCAT 3里PLC Runtime 1的端口通常是851Runtime 2是852Windows系统服务也有自己的固定端口。你发请求时必须同时告诉对方“我的AmsNetId和AMS Port”以及“我要访问的AmsNetId和AMS Port”两边地址对不上请求就静默丢弃或者返回错误。在TwinCAT工程里这套对应关系写进路由文件。手动添加路由时填的那张表其实就是让两边的Router互相认识。很多“连不上”的问题根因就是AmsNetId写错了一个数字或者对方路由表里没有你这台机器的条目。2.2 从建连到可用的完整时序看网上讨论“三次握手四次挥手”的时候容易把TCP握手和ADS应用层握手混在一起。实际ADS连接建立分两个层次TCP层先做经典的三次握手客户端SYN目标设备SYNACK客户端ACK把端口48898上的TCP连接建立起来。这一步保证“网络通道能通”。应用层并没有一个专门的“ADS握手帧”ADS的“握手”实际上是第一条请求/响应成功往返。你发一个Read Device Info或者Read State请求目标返回“0错误码”这个事件就是握手成功的标志。这里有个细节值得注意Router到Router的TCP连接是长连接建立之后不会每条ADS请求都重新握手。所以你在Wireshark里看到的现象是TCP SYN只有一次后面全是承载ADS帧的连续TCP报文。这是好事吞吐高坏处是TCP连接一旦被防火墙或NAT踢掉上层应用不一定立刻感知直到下一条请求超时。2.3 和TCP三次握手的对照别被名字带偏对比项TCP三次握手ADS连接建立目的确认双方序列号建立可靠字节流确认AMS地址可达、目标端口有服务核心动作SYN/SYN-ACK/ACK建立TCP连接 第一条ADS请求/响应身份标识IP TCP端口AmsNetId AMS Port失败表现连接超时、拒绝连接请求无响应、错误码返回、路由器日志报错是否每次请求都要不是不是请求靠InvokeId匹配把这两层分开看排错时就少走弯路。TCP通不代表ADS通TCP是“路修好了”ADS是“门牌号对上了、屋里的人愿意理你”。我们见过太多现场TCP ping得通但应用层一直报错就是因为路通、门没开。3. 逐字节解剖一个ADS数据包的完整骨架3.1 从以太网帧到AMS头你要找的字段就在这当ADS帧走TCP封装时TCP payload就是完整的AMS头加数据区。AMS头一共32字节这是整个协议最核心的骨架。字段顺序和长度如下偏移字段长度说明0目标AmsNetId6字节目标设备的AMS地址6目标AMS Port2字节目标上的服务端口小端8源AmsNetId6字节发起方的AMS地址14源AMS Port2字节发起方的端口小端16Command ID2字节ADS服务命令号小端18StateFlags2字节请求/响应状态标志20Data Length4字节数据区长度小端24Error Code4字节服务返回的错误码28Invoke ID4字节调用匹配号请求与响应一一对应四个AmsNetId和Port字段决定了“谁发给谁”Command ID决定了“你要干什么”Data Length告诉接收方后面要读多少字节Error Code是命令执行结果Invoke ID用来把响应和请求对上号。新手最容易栽在两个地方一是字节序。整个ADS协议栈从头到尾都是小端struct.pack默认也要显式用H、I二是Invoke ID同步调用时很多库让它自增异步并发时这是匹配响应的唯一凭据乱填会导致响应张冠李戴。3.2 常用服务命令与IndexGroup/IndexOffsetCommand ID有多种但日常打交道的就那么几个Command ID服务名作用0x0001Read Device Info读设备版本、名称等基本信息0x0002Read按组号和偏移读一块数据0x0003Write按组号和偏移写一块数据0x0004Read Write一次进出双向数据0x0007Add Device Notification注册变量变化通知0x0009Device Notification设备主动推送通知数据Read和Write请求都要带IndexGroup和IndexOffset两个四字节字段。这两个字段本质上就是“变量在目标地址空间里的门牌号”。在TwinCAT里不同内存区域用不同的IndexGroup区分偏移再精确到具体位置。比如符号表相关操作有固定组号直接按名字访问变量时会先通过符号解析服务拿到句柄再用句柄去读写。知道这个机制后你就明白为什么有时读一个变量要先“多打一个招呼”——那不是多余是ADS的寻址方式决定的。3.3 用原始Socket发一帧把抽象拉回现实光看表还是不直观我用一个最小Python示例演示如何通过原始TCP套接字向端口48898发送Read Device Info请求。这里没有用任何ADS封装库为的是让你看清字节怎么排布。import socket import struct TCP_PORT 48898 target_netid bytes([192, 168, 0, 1, 1, 1]) # 目标AmsNetId target_port 851 # PLC Runtime 1 source_netid bytes([127, 0, 0, 1, 1, 1]) # 本机AmsNetId source_port 30000 # 本机AMS端口 invoke_id 1 command_id 0x0001 # Read Device Info state_flags 0 data_length 0 error_code 0 header ( target_netid struct.pack(H, target_port) source_netid struct.pack(H, source_port) struct.pack(H, command_id) struct.pack(H, state_flags) struct.pack(I, data_length) struct.pack(I, error_code) struct.pack(I, invoke_id) ) def read_frame(sock): header_buf b while len(header_buf) 32: chunk sock.recv(32 - len(header_buf)) if not chunk: raise ConnectionError(连接被断开) header_buf chunk (data_len,) struct.unpack_from(I, header_buf, 20) data_buf b while len(data_buf) data_len: chunk sock.recv(data_len - len(data_buf)) if not chunk: raise ConnectionError(连接被断开) data_buf chunk return header_buf, data_buf sock socket.create_connection((192.168.0.100, TCP_PORT), timeout3) sock.sendall(header) resp_header, resp_data read_frame(sock) (recv_err,) struct.unpack_from(I, resp_header, 24) print(错误码:, recv_err) print(设备信息原始字节:, resp_data.hex()) sock.close()这段代码对一个正常连接的PLC发送后如果错误码是0数据区里就是设备名和版本信息。注意我写的read_frame不是一把梭直接recv(1024)因为TCP是流式传输你没法保证一次recv恰好拿全一帧。必须按照AMS头里的Data Length去循环读这就是我常说的“TCP粘包/半包”在ADS场景里的真实体现。4. 数据包的往返旅程从变量读取到控制任务更新4.1 请求怎么走进PLC的任务周期现在把视野从字节拉回业务链路。一次变量读取实际路径是这样上位机应用构造ADS Read请求目标AmsNetId指向PLC目标AMS Port指向PLC Runtime服务。ADS库把请求交给本机RouterRouter查路由表判断目标是本机还是远程设备。如果是远程设备Router把AMS帧封装进TCP连接发到对方IP的48898端口。目标Router收到后根据目标AMS Port找到对应服务进程把数据区交给PLC Runtime处理。Runtime按IndexGroup和IndexOffset找到变量读出值原路返回。这里有个反直觉的点AMS路由不一定走TCP。如果上位机软件跑在控制器本机Router走的是共享内存或本地消息队列根本不碰网卡。所以你在本地调试一切正常、换到远程就出问题别急着怀疑协议本身先想清楚你的数据是不是真的“上了网线”。4.2 同步请求、异步请求和通知机制怎么选读变量有两种思路轮询和通知。轮询就是上位机每隔固定周期主动发Read请求逻辑简单但会产生大量请求帧而且响应时间取决于轮询周期。通知机制是上位机先发AddNotification注册“我对某个变量感兴趣”之后PLC在变量变化或周期触发时主动推数据过来。实操中我建议按数据特性选低速变量、频率低于10Hz轮询足够别引入通知机制增加复杂度。高频变量、比如驱动轴状态用Notification并合理配置CycleTime和MaxDelay让PLC按固定节拍推送。大批量IO点优先用Read Write一次批量读写而不是一个点一个点请求。4.3 高频数据下的“背压”问题通知机制用多了会遇到一个有意思的问题数据推送太快上位机处理不过来。这让我想起数字电路里的valid/ready握手和stall背压逻辑发送端说“我有数据”接收端说“我还没准备好”双方必须握上手数据才算真正被消费。ADS通知并没有这么优雅的逐包背压它更像是“PLC按自己的节奏往通道里塞数据上位机来不及读消息就在缓冲区里排队排满了就丢”。遇到这种情况第一反应不要是调高接收线程优先级而是从源头降频。把通知的CycleTime调大或者把多个变量合并成结构体一次推送。另一个常见经验是让PLC端在任务里自己累加或缓存数据上位机每100毫秒批量拉一次快照效果比高频通知稳定得多。5. 动手搭一个最小ADS客户端把协议跑通5.1 选型逻辑什么时候用库、什么时候裸写关于ADS客户端你得诚实评估自己的场景。生产环境优先用官方ADS库Windows上的ADS.NET、TcAdsDll稳定且支持全功能跨平台脚本验证用pyads效率很高到了网关开发或者做协议分析才轮到裸socket。裸写Socket的最大价值不是替代库而是培养“协议感”。当你亲手拼过一次32字节的AMS头再回去看封装库的日志、错误码和超时参数你会突然明白很多原来不理解的行为。5.2 用pyads完整跑通读写变量Linux或Windows上装了Python后pyads是验证协议最快的方式。pip install pyads一个最小读写实例大概长这样import pyads # 本机与PLC都配好路由后 plc pyads.Connection( 192.168.0.1.1.1, # PLC的AmsNetId 851, # PLC Runtime 1的AMS Port 192.168.0.100 # PLC的IP地址 ) plc.open() # 如果符号名是GVL.bEnable plc.write_by_name(GVL.bEnable, True, pyads.PLCTYPE_BOOL) val plc.read_by_name(GVL.bEnable, pyads.PLCTYPE_BOOL) print(GVL.bEnable , val) plc.close()第一次跑通后建议故意改错AmsNetId里的一个数字再观察报错信息。你会发现错误不是在TCP层而是在AMS层返回超时或路由不可达。这个体验非常重要它能帮你把“连接失败”这个概念从网络层挪到协议层。5.3 嵌入式工程师最需要绕开的两个思维定式我在带嵌入式团队对接倍福时发现大家普遍带着串口通信的思维来搞ADS最容易踩两个坑。第一个坑是用处理串口帧的姿势处理TCP。STM32上做USART接收数据包靠DMA加空闲中断一帧一帧拿到了ADS的TCP通道还想着等“一整帧”到达再解析结果要么recv缓冲不满一直等要么一次性收到好几帧处理不过来。正确的姿势是“循环读头、按长度字段再读体”这正是前面read_frame函数做的事情。第二个坑是把自己写的板子当成“唯一客户端”。PLC侧可能是多客户端同时访问而AMS Router默认允许并发连接。嵌入式设备频繁重连时如果没有正确释放源端口或通知句柄Router端会积累一堆僵尸会话最终出现莫名其妙的超时。别小看这个问题现场好几起“隔几天就断一次”的故障最后查出来是客户端没有清理通知注册。6. 连接异常和错误码排障实战6.1 “连不上”的排查顺序ADS连接故障的排查顺序非常固定乱序会浪费大量时间。我建议按下面这张表逐层推进排查层检查内容方法网络层IP通不通、防火墙是否放行48898ping、telnet IP 48898TCP层TCP三次握手是否成功Wireshark过滤tcp.port 48898路由层双方路由表是否互认TwinCAT Router查看远程设备状态地址层AmsNetId和AMS Port是否写对核对工程实际配置服务层目标服务是否在运行查看TwinCAT系统状态、PLC是否处于RUN命令层请求字段是否合法观察响应Error Code很多人一上来就盯着错误码看那是第6层的事。底层没通上层错误码再清楚也解决不了问题。我处理现场问题经常是先打开Wireshark确认SYN有没有成功再谈别的。6.2 错误码别急着背先看上下文网上经常有人问“倍福报错4132是好事还是坏事”这类问题。我的看法是错误码本身没有好坏它只是AMS头里Error Code字段的一个数字反映的是“这条命令在目标端执行得怎么样”。同一个错误码出现在不同阶段含义完全不同。比如出现在PLC还没进入RUN时往往只是目标服务未就绪属于时序问题出现在长时间运行后可能是句柄泄漏、连接数超限或路由表变化出现在高负载时更可能是任务周期抖动导致响应超时。所以面对错误码第一步不是查表而是把上下文记录下来什么命令、什么时间、什么状态、频率如何。把这些信息整理清楚再对照TwinCAT的系统日志和ADS官方错误码表定位会快很多。6.3 抓包工具和过滤条件最后推荐大家养成抓包习惯。Wireshark对48898端口上的ADS流量通常能解析出AMS头和ADS命令打开后直接过滤tcp.port 48898即可。重点看三类信息第一条请求与响应是否在合理时间内返回Response里Error Code是不是0请求帧的Data Length和实际传输数据是否一致。抓包时还要注意如果你连的是PLC上的TwinCAT 3有些通信走本地Router不经过网口只有在远程访问时网卡上才看得到包。不要因为抓不到包就以为数据没发先确认数据路径真的经过了这块网卡。我之前排查过一台现场设备上位机一直报错但Wireshark在服务器网卡上什么都抓不到。最后发现那台电脑上装了两块网卡上位机配置的PLC地址走的是另一条物理链路。把抓包网卡换过来真相立刻浮现。这类问题用协议栈思考比背错误码有效得多。
返回列表