ARTICLE DETAIL

资讯详情

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

温控器MODBUS-RTU串口抓包实战:从报文解析到Wireshark调试全流程

温控器MODBUS-RTU串口抓包实战:从报文解析到Wireshark调试全流程 1. 抓包前先把RTU帧结构吃透温控器报文的五段式骨架1.1 一段最典型的读温度报文逐字节拆给你看做温控器的上位机通讯调试绕不开MODBUS-RTU。我最早接手的那个冷库项目上位机每隔几秒报一次温度数据异常现场工程师一口咬定通讯没问题可数据就是不稳定。后来我把串口上的字节流完整抓下来发现从站返回的帧里CRC一到特定寄存器地址就错位问题立刻浮出水面。所以比起在业务代码里瞎猜直接看报文才是最有效的手段。MODBUS-RTU的一帧报文其实非常简单整帧由四部分组成从站地址、功能码、数据区、CRC16校验帧与帧之间还要有至少3.5个字符时间的静默间隔。以温控器最常见的读保持寄存器请求为例01 03 00 00 00 01 84 0A逐字节看01从站地址也就是温控器在总线上的编号。常见的温控器拨码地址从1到247地址0是广播地址正常通信中很少出现。03功能码代表读保持寄存器。温控器的当前温度、设定温度、PID参数大多存放在保持寄存器里所以03是使用频率最高的功能码。00 00起始寄存器地址这里是第0号寄存器。注意MODBUS寄存器地址是16位无符号整数用的是大端字节序也就是高字节在前。00 01要读的寄存器数量。温控器一般一个寄存器存一个16位数值读1个就是读2字节数据。84 0ACRC16校验值由前面6个字节计算得来。注意MODBUS-RTU的CRC发送顺序是低字节在前所以0x0A84先发84再发0A这个坑后面详细说。这里有一个非常关键的细节MODBUS寄存器是16位字但RTU的数据帧是按字节传输的。所以你在抓包工具里看到的所有两个一组的十六进制数基本都是高字节在前。很多新手第一次看到00 00会以为这是地址的ASCII码其实是两个字节拼接成的一个16位寄存器地址。1.2 温控器通信里最高频的功能码一张表说清楚抓包解析前先把温控器领域最常见的几个功能码记住。我整理了下面这个表现场调试时对照着看基本够用功能码含义请求数据区应答数据区温控器场景0x03读保持寄存器起始地址(2字节)数量(2字节)字节数(1字节)寄存器值(N×2字节)读当前温度、设定温度、PID值0x04读输入寄存器起始地址(2字节)数量(2字节)字节数(1字节)寄存器值(N×2字节)读温度传感器原始值、故障码0x06写单个寄存器寄存器地址(2字节)写入值(2字节)原请求原样返回修改设定温度0x10写多个寄存器起始地址(2字节)数量(2字节)字节数(1字节)数据区起始地址(2字节)数量(2字节)整定PID参数、配置报警阈值其中03和06这两个功能码是温控器与上位机通讯的主体。读取温度用03下发设定温度用06。如果温控器支持PID自整定一般会用到10功能码一次写多个寄存器。需要特别强调的是异常响应帧。当温控器收到一个无法处理的请求它会返回一个功能码0x80的帧并在后面附带一个1字节的异常码。比如请求里的寄存器地址越界温控器不会沉默而是返回01 83 02地址01功能码0x830x030x80异常码0x02代表非法数据地址。抓包时如果看到功能码高位多了0x80直接查异常码含义就行后面第五章我会专门讲怎么快速判断。1.3 CRC16的两种算法和那个最容易踩的字节序坑CRC16校验是MODBUS-RTU通信的安全员。它的计算参数是固定的多项式0x8005初始值0xFFFF输入输出都需要按位反射结果异或值0x0000。用代码实现时最常用的是查表法和位运算法两种。位运算法代码简洁逻辑直白适合理解原理def crc16_modbus(data: bytes) - bytes: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 # MODBUS规定发送时低字节在前 return bytes([crc 0xFF, (crc 8) 0xFF])这里0xA001是0x8005按位反射后的结果做底层算法的朋友应该能看懂这一步。查表法比位运算法更快适合在资源受限的嵌入式设备上跑但生成256项CRC表的代码会占一些篇幅。我在后面的模拟脚本里会直接用位运算法因为Python环境下性能不是瓶颈代码短、好维护才是关键。字节序这个问题我吃过一次大亏。按照MODBUS-RTU标准CRC发送顺序是低字节在前也就是上面代码返回的[crc 0xFF, crc 8]。但有些温控器厂商的协议手册偏偏把CRC写成高字节在前。如果你上位机严格按照手册实现对端也严格按照手册实现两边反而能通因为数据经过了两层反转。最怕的是手册和实际设备不一致或者你换了一台不同批次的设备CRC整体错位Wireshark里就会看到一堆[incorrect]标记。判断方法很简单把抓到的帧用Python脚本计算一遍哪个字节序算出0x0000哪个就是对的。2. 串口抓流量的三套实战接线方案与工具选型2.1 为什么Wireshark不能直接选COM口很多第一次抓串口的人打开Wireshark后到处找串口选项找了半天发现根本没有。原因在于Wireshark底层走的是npcap/winpcap这套网络抓包驱动它面向的是网卡、回环接口这样的网络设备天然不认识COM口。换句话说Wireshark是一个网络协议分析器不是一个串口监视器。所以要抓温控器的MODBUS-RTU报文必须有一个桥要么用硬件把总线上的电平信号引出来要么用软件把串口字节流重新封装成Wireshark能读懂的数据包。下面三套方案我都在实际项目里用过适用场景不太一样。2.2 方案一USB转485并联监听最贴近真实温控器现场温控器大多是RS485两线总线主站PLC、上位机轮询多个温控器从站。想抓总线上的通信内容最直接的办法是并接一个USB转485转换器到总线上。接线注意三点A接A、B接B别接反。RS485是差分信号A/B线一旦接反你抓到的每个字节CRC都会错但设备之间反而正常通信这个坑第六部分细说。信号地要共地。USB转485的GND引脚尽量和温控器电源的GND接在一起否则在长线传输时容易因为电势差导致抓包数据偶发错位。不要额外启用USB转485上的终端电阻。如果转换器本身有120欧终端电阻开关一定要关掉。温控器总线两端本来就有终端电阻你再并一个上去相当于把总线阻抗拉低波形会畸变。这种方案的优点是接入即用不需要拆设备缺点是只能抓一条总线上的数据如果总线上有多个从站你需要用从站地址去区分报文归属。另外USB转485的质量很影响结果我建议选带隔离的方案有些工业现场的共模电压会直接烧掉转换器。2.3 方案二TTL电平旁路监听嵌入式工程师最常用的定向打法如果你负责的是温控器设备本身的开发手头大概率有USB转TTL而没有USB转485。这时候可以打开温控器外壳找到RS485收发芯片常见型号MAX485、SP3485把USB转TTL的RX引脚接到这颗芯片的RO引脚上GND共地就能单向监听到这个节点从总线上收到的所有字节。这个方案的绝妙之处在于RO引脚是接收数据输出脚它输出的TTL电平就是总线上的字节流不需要你额外处理差分信号。而且因为是旁路接线基本不影响原总线通信。但要注意这种接法只能监听单方向。RS485是半双工总线主站发送和从站应答都在同一对差分线上而RO脚反映的是本节点收到的信号如果你想同时看主站请求和从站应答需要在主站侧的收发芯片RO脚也接一路或者直接并接在总线上。我实际项目里遇到这种需求会用两路USB转TTL分别监听主站和从站然后在Wireshark里按时间戳合并。2.4 方案三逻辑分析仪硬抓UART波形最硬核也最稳妥当波特率不清楚、串口参数不确定或者设备通讯已经乱到软件层面根本抓不到完整字节时逻辑分析仪是最后的底牌。把逻辑分析仪的通道接在TTL电平的TX/RX引脚上采样率设置为波特率的16倍以上比如9600波特率就设2MHz以上抓完波形后用软件自带的UART解码器解出字节流。采样率这个参数很关键。设太低波形边缘失真解码出来全是乱码设太高抓包时间窗口变短。我一般习惯设1MHz采样率既能覆盖到115200以下的常用波特率又能连续抓几秒钟数据。逻辑分析仪的缺点是波形文件不能直接拖进Wireshark需要把解码出来的字节流导成文本再用后面的脚本封进pcap文件。不过排查乱码和波特率问题时花这几分钟很值。3. 把RTU字节流送进Wireshark桥接脚本与Decode As全流程3.1 思路用Python把串口字节封成pcap文件抓到了串口字节流接下来就是这篇文章的核心技巧让Wireshark能打开并解析它。我在实践中用的是一个Python脚本把串口收到的每一帧RTU报文按时间戳写进pcap文件再用Wireshark的Decode As功能把它强制识别成MODBUS协议。pcap文件格式本身不复杂就是全局文件头加若干数据包记录。这里我用了一个小技巧把数据链路类型设为DLT_USER0编号147这是Wireshark专门留给用户自定义协议的类型不会和网卡抓包的Ethernet格式冲突。import serial import struct import time PCAP_GLOBAL_HEADER struct.pack( IHHiIII, 0xA1B2C3D4, # pcap魔术字小端序 2, 4, # 主版本号2次版本号4 0, 0, # 时区修正、时间戳精度一般填0 65535, # 最大捕获长度 147 # DLT_USER0方便后续强制解析 ) def write_pcap_frame(f, frame: bytes): ts time.time() ts_sec int(ts) ts_usec int((ts - ts_sec) * 1_000_000) f.write(struct.pack(IIII, ts_sec, ts_usec, len(frame), len(frame))) f.write(frame) def main(): ser serial.Serial(COM3, 9600, timeout0.02) with open(modbus_rtu.pcap, wb) as f: f.write(PCAP_GLOBAL_HEADER) while True: data ser.read(ser.in_waiting or 1) if not data: continue # 等一小段静默时间认为一帧结束 frame bytearray(data) while ser.in_waiting: frame.extend(ser.read(ser.in_waiting)) write_pcap_frame(f, bytes(frame)) print(捕获:, bytes(frame).hex( ).upper()) if __name__ __main__: main()这段脚本的核心逻辑是串口每次读到数据就放进缓冲区然后不断检查串口接收缓冲区是否还有残留数据一旦缓冲区空了就认为这一帧报文已经完整到达写入pcap文件。timeout设为0.02秒相当于20毫秒静默时间足够区分9600波特率下的帧间隔。Linux环境下设备名改成/dev/ttyUSB0或/dev/ttyS0即可。macOS则是/dev/tty.usbserial-*。3.2 Wireshark端配置Decode As把DLT_USER0解析成Modbus打开Wireshark加载生成的pcap文件你会看到所有数据包都被标记成DLT_USER0。此时点击菜单栏的Analyze → Decode As在弹出的窗口左侧找到DLT_USER0这一行右侧下拉框选择MODBUS点击OK保存。完成这个操作后Wireshark会自动重新解析所有报文。你会发现数据包现在是这样的结构ModbusAddress: 1 (0x01)Function Code: Read Holding Registers (0x03)Starting Address: 0x0000Quantity of Registers: 1CRC: 0x0A84 [correct]功能码、寄存器地址、寄存器数量全部以字段形式展开再也不用对着十六进制数一个个数了。这个方法强烈推荐因为它保留了完整的RTU帧包括CRC校验值。有些方案会把RTU帧拆成Modbus TCP格式来喂给Wireshark虽然也能解析但CRC字段就看不到了。3.3 实时方案转发UDP强制解码省去保存文件的中间步骤pcap文件方案适合事后分析但如果你需要在现场长时间观察温控器通信每次都保存文件再加载很麻烦。我后来改进了一下把串口收到的完整RTU帧实时封装成UDP包发到本机的127.0.0.1:1502端口Wireshark直接监听回环接口就能实时看到MODBUS解析结果。注意UDP包要封装完整的一帧不能一个字节一个包。我在代码里用同样的帧切分逻辑收集到一帧完整数据后统一发送import socket import serial UDP_IP 127.0.0.1 UDP_PORT 1502 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ser serial.Serial(COM3, 9600, timeout0.02) while True: data ser.read(ser.in_waiting or 1) if not data: continue frame bytearray(data) while ser.in_waiting: frame.extend(ser.read(ser.in_waiting)) sock.sendto(bytes(frame), (UDP_IP, UDP_PORT))然后在Wireshark里做两步操作第一步打开Edit → Preferences → Protocols → Modbus在RTU over TCP/UDP相关的端口配置里加上1502第二步如果没有自动生效选中一个UDP包右键Decode As把UDP 1502端口强制指定为Modbus。这样Wireshark就能实时把UDP载荷按RTU帧解析。这里要解释一下为什么Wireshark的Modbus解析器能处理RTU帧Wireshark的Modbus解析器本身就同时支持Modbus/TCP带MBAP头和Modbus RTU over TCP/UDP两种形态关键是要让解析器知道这个端口承载的是RTU帧而不是TCP报文。3.4 验证解析是否正确的几个标志解析成不成功看三个标志就够数据包摘要里出现了MODBUS字样而不是Data。展开Modbus字段能看到清晰的Function Code、Register Address、Register Values分层。如果Wireshark算出了CRC校验会有[correct]或[incorrect]标记。如果解析后看到的是乱码字段大概率是Wireshark把它当成了Modbus/TCP来解把RTU帧的地址字节误当成了事务ID。回到Modbus协议设置里确认1502端口被配置成了RTU承载端口即可。4. Python模拟温控器从站与主站没有真实设备也能完整跑一遍抓包流程4.1 为什么我先搭模拟环境再上真机说实话直接拿真实温控器抓包当然最理想但真实设备有很多限制寄存器地址表不能随意改、异常响应不好构造、CRC错误场景更难复现。所以我建议先搭一套Python模拟环境把温控器的行为用软件模拟出来再用前面的桥接方案抓自己的模拟报文。等整个流程跑通了再切换到真实设备你对报文的判断力完全不一样。模拟环境只需要两个脚本一个模拟温控器从站一个模拟上位机主站。中间用虚拟串口对连接Windows下用com0comLinux下用socat。Windows安装com0com后打开它的Setup工具配置一对关联串口比如把COM5和COM6做成交互联。往COM5写的数据会从COM6读出来反之亦然。注意虚拟串口的波特率设置要和脚本一致虽然虚拟串口不校验实际波特率但保持一致性可以避免混淆。Linux下更简单一条命令创建两个伪终端socat -d -d pty,raw,echo0 pty,raw,echo0运行后终端会输出两个/dev/pts/N编号从站脚本用其中一个主站脚本用另一个。4.2 从站脚本一个能处理03和06请求的温控器模型下面这个从站脚本我用温控器开发中最典型的三个寄存器做模型0x0000当前温度只读初始值0x15E0按0.01℃分辨率就是56.00℃。0x0001设定温度可写初始值0x0BB8即30.00℃。0x0002PID比例带可写初始值100。import serial import struct def crc16_modbus(data: bytes) - bytes: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return bytes([crc 0xFF, (crc 8) 0xFF]) class TempController: def __init__(self, address0x01): self.address address self.registers { 0x0000: 0x15E0, # 当前温度 56.00℃ 0x0001: 0x0BB8, # 设定温度 30.00℃ 0x0002: 0x0064, # PID比例带 100 } def handle_frame(self, frame: bytes) - bytes: if len(frame) 8: return b addr, func frame[0], frame[1] data frame[2:-2] # CRC校验失败从站按MODBUS规范保持静默 if crc16_modbus(frame[:-2]) ! bytes(frame[-2:]): return b if addr ! self.address: return b # 读保持寄存器(0x03) if func 0x03: start_addr struct.unpack(H, data[0:2])[0] quantity struct.unpack(H, data[2:4])[0] if start_addr quantity 0xFFFF or start_addr quantity len(self.registers) start_addr - start_addr: # 简化处理只要读的寄存器超出已知范围就报错 if any((start_addr i) not in self.registers for i in range(quantity)): return self._exception_response(func, 0x02) resp bytes([self.address, 0x03, quantity * 2]) for i in range(quantity): resp struct.pack(H, self.registers[start_addr i]) return resp crc16_modbus(resp) # 写单个寄存器(0x06) if func 0x06: reg_addr struct.unpack(H, data[0:2])[0] reg_value struct.unpack(H, data[2:4])[0] if reg_addr not in self.registers: return self._exception_response(func, 0x02) # 0x0000是当前温度只读写它返回非法数据地址 if reg_addr 0x0000: return self._exception_response(func, 0x02) self.registers[reg_addr] reg_value resp bytes([self.address, 0x06]) data return resp crc16_modbus(resp) # 其他功能码不支持 return self._exception_response(func, 0x01) def _exception_response(self, func: int, code: int) - bytes: resp bytes([self.address, func | 0x80, code]) return resp crc16_modbus(resp) if __name__ __main__: import serial.tools.list_ports print(可用串口:) for p in serial.tools.list_ports.comports(): print( , p.device, p.description) ser serial.Serial(COM6, 9600, timeout0.2) controller TempController(address0x01) print(温控器从站模拟程序已启动等待主站请求...) while True: # 等待至少8字节最短帧长度 frame ser.read(8) if len(frame) 8: continue # 继续读完剩余字节 while ser.in_waiting: frame ser.read(ser.in_waiting) response controller.handle_frame(frame) if response: print(收到请求:, frame.hex( ).upper()) print(回复应答:, response.hex( ).upper()) ser.write(response)代码里值得注意的点一是CRC校验失败直接静默返回。这是MODBUS-RTU的标准行为从站收到CRC错误的帧不会回复任何内容主站只能靠超时处理。二是_exception_response方法里func | 0x80这个操作就是前面说的异常标志位。03变8306变86主站一看高位是1就知道请求失败了。三是读寄存器越界判断写得不复杂但够用只要请求读的地址里有一个不在已知寄存器表中就返回0x02非法数据地址。实际温控器固件里一般会按寄存器块范围判断这里用简单字典查找已经能模拟出最常见的异常场景。4.3 主站脚本模拟上位机发读温度请求并解析应答主站脚本相对简单我把它设计成命令行交互式方便每次发不同请求import serial import struct def crc16_modbus(data: bytes) - bytes: crc 0xFFFF for byte in data: crc ^ byte for _ in range(8): if crc 0x0001: crc (crc 1) ^ 0xA001 else: crc 1 return bytes([crc 0xFF, (crc 8) 0xFF]) def send_raw(ser, frame_no_crc: bytes): frame frame_no_crc crc16_modbus(frame_no_crc) print(发送:, frame.hex( ).upper()) ser.write(frame) resp ser.read(100) if resp: print(接收:, resp.hex( ).upper()) else: print(接收: 超时从站无响应) return resp def main(): ser serial.Serial(COM5, 9600, timeout1.0) ser.reset_input_buffer() print( 读取当前温度(寄存器0x0000) ) resp send_raw(ser, bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x01])) if len(resp) 5 and resp[1] 0x03: raw_temp struct.unpack(H, resp[3:5])[0] print(f当前温度: {raw_temp / 100.0:.2f} ℃) print( 读取设定温度(寄存器0x0001) ) resp send_raw(ser, bytes([0x01, 0x03, 0x00, 0x01, 0x00, 0x01])) if len(resp) 5 and resp[1] 0x03: raw_set struct.unpack(H, resp[3:5])[0] print(f设定温度: {raw_set / 100.0:.2f} ℃) print( 修改设定温度为35.00℃ ) resp send_raw(ser, bytes([0x01, 0x06, 0x00, 0x01, 0x0D, 0xAC])) print( 读写操作完成 ) if __name__ __main__: main()注意0x0DAC就是3500对应35.00℃。不同温控器分辨率不一样有些是0.1℃分辨率那就是350了所以寄存器值到实际温度的换算一定要看温控器手册这是现场最容易搞错的地方。4.4 跑通后你会看到什么把从站脚本挂在COM6主站脚本挂在COM5同时把第三章的UDP转发脚本也挂在COM6上注意不要同时占用同一个COM口转发脚本可以和从站脚本串行执行或者干脆在主站侧转发我推荐监听主站侧因为请求和应答都在主站的收发串口上经过。运行主站脚本你应该在控制台看到类似这样的输出发送: 01 03 00 00 00 01 84 0A 接收: 01 03 02 15 E0 B7 5C 当前温度: 56.00 ℃ 发送: 01 03 00 01 00 01 CA 0A 接收: 01 03 02 0B B8 FD 4F 设定温度: 30.00 ℃这个流程跑通后Wireshark里实时看到的报文和真实温控器几乎一样你就可以放心用它验证解析规则和排查思路了。5. 实际报文拆解正常读温度与异常响应的判断套路5.1 读当前温度的请求应答逐字节拆解我拿模拟环境里抓到的这对报文给你拆一遍。请求帧是01 03 00 00 00 01 84 0A01从站地址1总线上的温控器编号。03读保持寄存器。00 00起始寄存器地址0也就是当前温度寄存器的地址。00 01读1个寄存器。84 0ACRC16校验。应答帧是01 03 02 15 E0 B7 5C01从站地址1。03功能码正常应答会原样返回功能码。02数据区字节数因为1个寄存器占2字节。15 E0寄存器值十六进制0x15E0等于十进制5600。B7 5CCRC16校验。5600怎么变成56.00℃这就是温控器手册里定义的分辨率。常见的温控器温度值分辨率是0.01℃也就是寄存器值除以100也有的是0.1℃或0.001℃。如果厂商手册没写清楚最直观的办法是把温控器显示面板的温度值和寄存器值对照一下一比就出来了。Wireshark解析之后15 E0会直接按寄存器值显示部分场景下还会换算成十进制但不会自动给你显示成温度。所以Wireshark能帮你省去数十六进制字节的功夫但温度换算还是得靠人。5.2 写设定温度流程拆解06功能码的请求与应答镜像关系写单个寄存器的请求和应答很有意思它不像03功能码那样请求一段、应答另一段而是应答帧完全镜像请求帧。比如把设定温度改成35.00℃请求帧01 06 00 01 0D AC ?? ??01从站地址1。06写单个寄存器。00 01寄存器地址1也就是设定温度寄存器。0D AC要写入的值十进制3500代表35.00℃。?? ??CRC。应答帧01 06 00 01 0D AC ?? ??和请求一模一样。温控器把原请求原样返回表示写操作成功。所以解析06功能码时你只需要看请求帧应答帧基本不用解析除非你想要确认从站确实收到了完整且CRC正确的帧。抓包时如果看到应答应答帧和请求帧不一致或者CRC不正确那说明要么从站收到了错误帧后静默但主站收到的其实是另一个设备的响应要么总线上存在干扰。这类问题在真实现场排查中占了相当大一部分比例。5.3 异常响应识别看到功能码高位加0x80直接查表MODBUS的异常响应非常好认功能码的最高位被置1。理论上这是协议层的一个巧妙设计因为正常功能码最大也就用到0x10高位永远是0一旦变成了0x83、0x86这种必然是异常响应。我用一个现场案例说明。某次调试中上位机一直报写入失败我用抓包工具一看主站发的是01 06 00 02 00 64 ?? ??从站返回01 86 02 ?? ??86就是0x06 | 0x80意味着写寄存器失败。后面的02是异常码。MODBUS标准定义了这几个常见异常码异常码含义温控器场景0x01非法功能用了不支持的读/写命令0x02非法数据地址寄存器地址不存在或越界0x03非法数据值地址存在但写入值越界比如温度超过量程0x04从站设备故障温控器内部异常无法响应0x06从站忙温控器正在执行PID计算暂时无法处理新请求在这个案例里0x02说明上位机写了一个不存在的寄存器地址我对照温控器手册检查后发现是起始地址算错了偏移了一位。这种问题不抓包靠肉眼盯寄存器表效率会低很多。Wireshark里快速过滤异常响应也有技巧。在过滤栏输入modbus.func_code 0x80就能筛出所有异常响应。如果再配合modbus.exception_code字段可以直接定位到具体的异常码。6. 这5个坑我在抓包现场都踩过提前帮你排掉6.1 终端电阻导致波形畸变抓包结果时好时坏第一次并接USB转485抓温控器总线时我抓到的数据一会正常一会乱码。排查了很久最后发现是USB转485转换器上的120欧终端电阻开关被拨到了ON位置。RS485总线在两端各需要一个终端电阻中间设备不能再加。多加的这个电阻把总线上的信号电平拉低距离较远的设备发出的信号到了抓包器这里就已经衰减到临界状态。判断方法很简单抓包时如果某个从站地址的报文总是CRC错误但其他从站正常基本可以怀疑是物理层信号问题。关掉转换器的终端电阻开关后所有报文恢复正常。6.2 AB线接反设备之间通信正常抓包器却全是CRC错误这个坑非常反直觉。AB线接反的情况下温控器和PLC之间反而正常工作因为两个设备都接反了差分信号依然能被正确解析。但你并接的抓包器如果A/B和总线反了它读到的就是反相位的差分信号解出来的字节全部按位取反CRC校验必然全错。所以一旦抓包器显示所有设备的CRC都错先检查A/B极性而不是怀疑温控器坏了。我甚至遇到过一种情况总线本身A/B接反但两个设备已经形成默契正常工作抓包器按另一套标准接反而什么也抓不到。这种时候用万用表量一下总线静态电压就能定位A线对GND的电压应该比B线高0.2V以上在终端电阻存在的情况下。6.3 串口参数不匹配9600、8、N、1是默认值但不是全部MODBUS-RTU最常见的串口参数组合是9600波特率、8位数据位、无校验位、1位停止位。但温控器厂商不一定按这个默认值来有些型号默认19200有些默认偶校验。串口参数不匹配时抓包器会看到大量乱码或者帧长度异常。现场判断方法是看帧的起始字节如果出现连续的0x00或0xFF极有可能是停止位/校验位不匹配。另外一个技巧是先用逻辑分析仪抓一下波形从波形宽度反推波特率9600波特率下每位约104微秒19200波特率下每位约52微秒量几个位宽就出来了。6.4 Wireshark把一帧拆成两帧导致Modbus解析失败RTU over TCP/UDP转发方案里如果转发脚本把一帧数据拆成了多个UDP包发送Wireshark的Modbus解析器就可能看到不完整的RTU帧直接把它当普通Data显示。我早期的转发脚本就是每收到一个字节就发一个UDP包结果Wireshark里全是零碎的Data包根本没法看。解决办法是我在第三章里写的等到静默时间再打包发送逻辑。但要注意如果温控器自身发送的帧间隔特别大超过了脚本的静默判定时间一帧还是可能被拆开。这时候可以适当调大timeout参数比如从20毫秒调到50毫秒。不过时间调太大也有副作用两帧相邻的报文会被合并成一个UDP包解析器反而分不清边界。MODBUS规范建议的帧间隔是3.5个字符时间在9600波特率下约4毫秒所以20毫秒的静默判定已经很宽松了。6.5 抓包器自身的干扰别把抓包引入的噪声当成设备故障这是所有串口抓包方案的通病抓包器并接在总线上多少会给总线带来额外负载。USB转485转换器的输入阻抗不够高时可能会拉低总线信号质量尤其是在长线传输的末端。最典型的现象是抓包器接上后温控器通信反而开始报错拔掉抓包器通信恢复正常。如果出现这种情况优先换一个带隔离的USB转485或者改用TTL旁路监听方案直接并接在收发芯片的RO脚上对总线的负载影响最小。我后来在部署长期的温控器监控系统时干脆在总线设计阶段就预留了一个监听分支用高阻抗缓冲器把总线信号引出来再接抓包工具彻底避免抓包器影响正常通信。6.6 抓包脚本本身也可能写错CRC最后一个坑有点黑色幽默你写来验证别人的主站脚本可能自己就是错的。我在crc16_modbus函数里返回的是低字节在前的bytes对象而主站发送时直接把frame_no_crc crc16_modbus(frame_no_crc)拼接成帧这里如果函数返回顺序写反了整个模拟环境的报文就全部CRC错误。所以建议在跑模拟环境前先用已知正确报文做一次自检01 03 00 00 00 01的CRC应该算出84 0A对不上就是函数实现有问题。7. 一些经验收尾抓包这件事做到最后比拼的不是工具熟不熟练而是对协议本身的理解深度。MODBUS-RTU看起来只有四段结构但真正到现场波特率、校验位、CRC字节序、寄存器分辨率、功能码异常机制任何一个环节出问题都能让你折腾一整天。把这套Python模拟脚本跑熟你会对这些环节建立非常直观的体感。我个人现在的习惯是任何温控器项目无论现场问题多紧急先花半小时在模拟环境复现一遍再把抓包工具接到真实总线上。模拟环境能帮你排除掉协议理解层面的错误真实环境才能暴露物理层和厂商实现的问题两者结合排查效率比直接拿真机瞎试快得多。这套脚本和接线方法沉淀到团队工具库后连硬件工程师调试温控器固件时都会来借用。如果你手头正好有温控器或者类似MODBUS设备在现场出问题不妨按这个流程走一遍剥开报文看真相远比猜来猜去靠谱。
返回列表