简介:面向工控上位机开发者的Modbus通信控件包,兼顾Modbus RTU与Modbus TCP/IP两种链路,适合需要快速读取仪表、PLC及从站设备数据的VC、VB、VC#、Delphi等开发场景。控件围绕线圈、输入状态、保持寄存器、输入寄存器等对象提供读写能力,覆盖01至06、15、16、17、22、23等常用功能码,并附带可直接参考的示例源码,便于理解调用方式、报文组织与异常处理思路。压缩包共60个文件、约478KB,以18个vb源码为主体,辅以dll控件库、sln与vbproj工程文件、chm帮助文档、txt说明及exe工具等,体量轻便但结构较完整。示例中对串口参数、从站地址、寄存器地址及数据类型等配置项有对应演示,可减少从协议理解到代码落地之间的反复试错。已有1770人学习,适合初中级开发者用作Modbus接口联调、功能码测试与二次开发时的查阅材料。
1. 从一台读不到数的流量计说起:Modbus RTU 与 Modbus TCP/IP 控件包到底解决什么问题
现场最尴尬的一幕,是串口助手里能刷出规规矩矩的 Modbus RTU 报文,换成上位机程序就一片超时。带示例源码的 Modbus RTU 与 Modbus TCP/IP 控件包,价值不在“封装了几个函数”,而在把组帧、校验、收发时序、拆帧、异常码这一整条链路固化下来,你只需要关心寄存器映射和业务逻辑。同一套数据模型(线圈、离散输入、保持寄存器、输入寄存器)走两种传输:串口上是地址加 CRC 的二进制帧,网口上是带 MBAP 头、跑在 502 端口的报文。压缩包里的示例源码通常就是最好的“黑匣子说明书”——拿它跟设备手册对一遍,地址偏移和字序问题基本能当场定位。这篇写给要接 PLC、仪表、网关的一线开发和现场调试,从帧结构一路讲到轮询调度和验证方法。
2. 拆开控件包看门道:RTU 帧与 TCP 报文的结构差异
控件包用起来像黑匣子,但真出问题时,能不能徒手把一帧拼出来、能不能对着抓包判断谁错了,决定了你是十分钟收工还是熬夜。这一章先把两种传输的帧结构拆到字节级,后面所有参数配置和排错动作,都建立在这套结构上。先记住一句话:Modbus 的数据模型只有一套,PDU(协议数据单元)在两种传输里是一样的,变的只是外面那层“信封”。
2.1 从站地址、功能码与 PDU:两种传输共用的一套数据模型
PDU 由功能码加数据域组成,读保持寄存器的 0x03 请求体是“起始地址 2 字节 + 寄存器数量 2 字节”,响应体是“字节数 1 字节 + 数据 N 字节”。功能码决定操作对象:0x01 读线圈、0x02 读离散输入、0x03 读保持寄存器、0x04 读输入寄存器、0x05 写单线圈、0x06 写单寄存器、0x0F 写多线圈、0x10 写多寄存器、0x17 读写多寄存器。四个数据区的差别只在语义:线圈和离散输入是位,保持寄存器可读可写,输入寄存器只读。很多新手把 0x03 和 0x04 混着用,遇到从站返回 0x83、异常码 02(非法数据地址)就开始怀疑人生,其实只是设备根本没实现那一类数据区。
异常响应有个统一规律:功能码最高位置 1,后面跟一个异常码。0x03 出错返回 0x83,异常码 01 非法功能、02 非法数据地址、03 非法数据值、04 从站故障、05 确认、06 从站忙、0A 网关路径不可用、0B 网关目标无响应。控件包里判断“成功”不能只看有没有收到字节,必须判断首字节是否等于请求功能码。异常码 06 尤其要区别对待,它表示从站正在处理长任务,这时立刻重发只会把设备压垮,正确做法是退避一段时间再试。
数量上限是硬约束,超了从站直接回异常码 03:读线圈和离散输入一次最多 2000 个,读寄存器一次最多 125 个,写多线圈最多 1968 个,写多寄存器最多 123 个。写 100 个寄存器的正确姿势是拆成 123 以内的若干批,而不是硬塞一帧等它报错。这条限制在控件包里通常没有帮你兜底,得自己在业务层分批。
2.2 RTU 的 CRC16 与 3.5 字符静默:时序为什么是隐形参数
RTU 的一帧是“从站地址 1 字节 + PDU + CRC 2 字节”,CRC 低字节在前。整帧最长 256 字节。CRC 的算法是初值 0xFFFF,多项式 0xA001(0x8005 反序),逐字节异或后右移八次,每次最低位为 1 就和多项式异或。手写一遍比调库更可靠,因为很多“偶发 CRC 错误”其实是发送端把高低字节顺序写反了。
def crc16_modbus(data: bytes) -> bytes: """标准 Modbus RTU CRC16,返回低字节在前的 2 字节""" 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 build_rtu_read(slave: int, start: int, count: int) -> bytes: pdu = bytes([0x03]) + start.to_bytes(2, "big") + count.to_bytes(2, "big") body = bytes([slave]) + pdu return body + crc16_modbus(body)上面这段是拼一帧读保持寄存器的完整逻辑:先按大端拼起始地址和数量,再补从站地址,最后把前 6 字节丢进 CRC 函数。crc16_modbus的返回顺序不能改,改成高字节在前,设备会稳定返回“校验错误”,而且不同设备对 CRC 错误的处理不一样,有的丢帧静默,有的回异常码,排查难度直接翻倍。start这里填的是协议地址,也就是从 0 开始的那个值,不是手册上写的 40001,这个换算放到第 4 章细说。
比 CRC 更隐蔽的是帧间隔。RTU 靠总线静默来划分帧边界,规范要求帧与帧之间至少 3.5 个字符时间。字符时间按 11 位算(1 起始位 + 8 数据位 + 1 校验位 + 1 停止位),9600 波特率下一个字符约 1.146 毫秒,3.5 个字符约 4.01 毫秒。波特率高于 19200 时,规范建议固定用 1.75 毫秒。这个参数在控件包里一般叫“帧间隔”或“字符超时”,默认值常常是 0 或者几毫秒,在慢速从站上就会出问题:上一条响应还没发完,主站已经把它当成新帧的开头,于是“偶发丢包”出现了。我的习惯是把帧间隔设成略大于理论值,比如 9600 下给 5 毫秒,宁可牺牲一点吞吐。
2.3 TCP 的 MBAP 头与 502 端口:事务标识符不是摆设
Modbus TCP 的 ADU 是“MBAP 头 7 字节 + PDU”,没有 CRC。MBAP 四段:事务标识符 2 字节、协议标识符 2 字节(固定 0x0000)、长度 2 字节(后面还有多少字节,包含单元标识符)、单元标识符 1 字节。单元标识符在纯 TCP 设备上通常填 1 或者 0xFF,在“TCP 转 RTU”的网关上它才是真正的从站地址,这一区别决定了网关后面挂多台设备时你怎么寻址。
import struct def build_tcp_read(tid: int, unit: int, start: int, count: int) -> bytes: pdu = struct.pack(">BHH", 0x03, start, count) # MBAP: 事务ID + 协议ID(0) + 长度(单元标识符1字节 + PDU) + 单元标识符 mbap = struct.pack(">HHHB", tid, 0x0000, len(pdu) + 1, unit) return mbap + pdutid每次请求递增,响应回来时要用它跟请求配对。单连接串行请求时它看着多余,一旦你开了并发或者长连接里有迟到的响应,没有 tid 就分不清哪条响应属于哪次请求,这是“数据错位”类故障的头号原因。unit要跟设备手册确认,很多纯 TCP 从站不校验这个字段,随便填都能通,但接到网关上就必须精确对应后端 RTU 从站地址,否则现象是“连接正常、请求有响应、就是异常码 0B”。
协议标识符必须是 0,长度字段必须是“单元标识符 + PDU”的字节数而不是整帧长度——这两个字段写错,抓包工具能解析、设备就是不认。502 端口是约定值,有的设备允许改,改完记得同步防火墙策略。
3. 把控件跑起来:从串口参数到第一个读写请求
拿到一个带示例源码的控件包,别急着把示例跑通就收工。示例能跑通只说明“环境对上了”,离“现场能用”还有一段距离。这一章按串口、读取、网口三步走,每步给出最小可复现的配置和验证方法,让第一个请求真正落地。
3.1 串口侧的最小可用配置:波特率、校验位与收发切换
串口参数错一个,现象就是全超时,而且没有任何报错信息,属于典型的“玄学故障”。下面这张表是现场最常遇到的组合,缺少把握时优先用 8N1,绝大多数设备都支持。
| 参数 | 常用值 | 说明 |
|---|---|---|
| 波特率 | 9600 / 19200 / 38400 / 115200 | 必须与从站一致,距离长时降速更稳 |
| 数据位 | 8 | 几乎无例外 |
| 校验位 | None / Even | 手册写 8E1 就不能用 8N1 |
| 停止位 | 1 / 2 | 少数仪表要求 2 位停止位 |
| 流控 | 无 | RS485 不用硬件流控 |
| 帧间隔 | 3.5 字符时间以上 | 建议留 20% 余量 |
import serial ser = serial.Serial( port="COM3", # Linux 下形如 /dev/ttyUSB0 baudrate=9600, bytesize=serial.EIGHTBITS, parity=serial.PARITY_NONE, # 手册写 8E1 时改成 PARITY_EVEN stopbits=serial.STOPBITS_ONE, timeout=0.5, # 读超时,必须设置,否则线程会永久阻塞 write_timeout=0.5, )timeout是最容易被忽略的参数。不设超时,一次从站掉电就能把整个轮询线程挂死,界面表现为“卡住不动”。write_timeout同样重要,RS485 转换器被拔掉时写操作可能一直等下去。这两个值加起来应该小于你的轮询周期,否则积压会越来越严重。
RS485 半双工还有一层硬件时序:发送完最后一个字节的停止位之后才能切到接收。用 USB 转 485 的成品线,驱动通常自动处理;自己写 RTS 控制的,切换前要等发送寄存器真正空掉,常见的坑是提前一两个字符时间切换,把自己发出的尾部截断,从站看到的就是 CRC 错误。稳妥做法是延时 1 到 2 个字符时间再切。
3.2 用控件封装一次 03/04 功能码读取:参数表与返回值解析
最小验证不要求跑通业务,只要求“发出去、收回来、校验对”。下面这段把发送、接收、CRC 校验、异常码判断串成一条完整链路,可以脱离控件包单独跑,用来判断问题出在设备还是出在封装层。
import serial def read_holding(ser, slave: int, start: int, count: int) -> list: req = build_rtu_read(slave, start, count) ser.reset_input_buffer() # 清掉上一轮的残留字节 ser.write(req) # 先读 3 字节头:地址 + 功能码 + 字节数 head = ser.read(3) if len(head) < 3: raise TimeoutError("响应头超时,检查地址/波特率/接线") if head[0] != slave: raise ValueError(f"从站地址不匹配: {head[0]}") if head[1] & 0x80: code = ser.read(1)[0] if ser.in_waiting else 0 raise RuntimeError(f"从站异常响应,功能码={head[1]:#x} 异常码={code}") byte_count = head[2] payload = ser.read(byte_count + 2) # 数据 + CRC frame = head + payload if crc16_modbus(frame[:-2]) != frame[-2:]: raise ValueError("CRC 校验失败") data = frame[3:-2] return [int.from_bytes(data[i:i + 2], "big") for i in range(0, len(data), 2)]这段的关键在三个判断:地址是否匹配、功能码最高位是否置 1、CRC 是否对得上。很多封装库把这三件事合成一个布尔返回值,出问题时你只知道“失败”,不知道失败在哪一层,等于把黑匣子又套了一层。自己写一遍之后,再去用控件包的封装接口,心里就有对照标准了。
count不要超过 125,start + count不要越过设备的寄存器上界,否则回异常码 02。返回的寄存器列表是大端解析的 16 位无符号整数,浮点和 32 位整型要另外拼,第 4 章专门讲。
3.3 TCP 侧的最小连通性验证:端口、单元标识符与超时
网口调试的第一步永远不是写代码,是先确认链路。用一条命令看端口通不通,比在代码里打断点快得多。
# 确认 502 端口可达,-w 3 表示 3 秒超时 nc -zv -w 3 192.168.1.10 502 # 有 curl 的环境也可以直接发一帧十六进制报文做连通性测试 # 事务ID=0x0001 协议ID=0x0000 长度=0x0006 单元=0x01 功能码=0x03 起始=0x0000 数量=0x000A printf '\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x0a' | nc -w 3 192.168.1.10 502 | xxdnc通了只说明 TCP 层可达,不代表 Modbus 服务在监听,有些设备 Web 管理口开着、502 却是关的。上面那条printf能拿回一串十六进制,才算真正打通。返回报文里第 5、6 字节是长度,第 7 字节是单元标识符,第 8 字节是功能码,从第 10 字节开始才是数据。
import socket, struct sock = socket.create_connection(("192.168.1.10", 502), timeout=2) sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # 关掉 Nagle,避免小包延迟 sock.sendall(build_tcp_read(tid=1, unit=1, start=0, count=10)) head = sock.recv(7) # 先收 MBAP if len(head) < 7: raise TimeoutError("MBAP 头未收全") tid, pid, length, unit = struct.unpack(">HHHB", head) body = b"" while len(body) < length - 1: # 按长度字段收满,不能假设一次 recv 收全 chunk = sock.recv(length - 1 - len(body)) if not chunk: break body += chunkTCP_NODELAY这一行不是可有可无。Modbus 请求报文很短,Nagle 算法会把小包攒着等确认,表现就是“偶发多出几十毫秒延迟”,在轮询周期紧的场景里能把人逼疯。收数据必须按 MBAP 长度字段循环读满,recv一次拿到的可能是半帧,也可能是两帧粘在一起,第 5 章会专门讲这个坑。
4. 寄存器映射与数据解析:地址偏移、字节序与浮点数的血泪经验
通信打通只是开始,数值对不对才是验收标准。这一章三个问题占了现场解析错误的绝大多数:地址到底从几开始、32 位数据的高低位怎么排、一个字节里的 8 个开关怎么拆。每条都给换算方法和可复用的代码。
4.1 40001 还是 0:四种地址写法的换算表
同一台设备的手册可能同时出现四种地址:Modicon 风格的五位编号(40001)、十进制协议地址(0)、十六进制协议地址(0x0000)、以及带数据区前缀的写法(4x0001)。它们描述的是同一个寄存器,只是基准不同。控件包的 API 一般要求协议地址,也就是从 0 开始的那个值,直接把 40001 填进去,读到的就是下一个寄存器,数值自然全错。
| 手册写法 | 数据区 | 协议地址(填给函数) | 换算规则 |
|---|---|---|---|
| 40001 | 保持寄存器 | 0 | 减 40001 |
| 40011 | 保持寄存器 | 10 | 减 40001 |
| 30001 | 输入寄存器 | 0 | 减 30001 |
| 00001 | 线圈 | 0 | 减 00001 |
| 10001 | 离散输入 | 0 | 减 10001 |
| 0x0000 | 保持寄存器 | 0 | 直接使用 |
| 1(十进制,1-based) | 保持寄存器 | 0 | 减 1 |
判断手册用的是哪种基准,有个笨但有效的办法:看它有没有 40001 这一项。有,就是五位编号制;如果地址从 0 开始且最大到几千,多半是协议地址。拿不准的时候,先用 0 读 1 个寄存器,再用 1 读 1 个寄存器,对比哪个值符合工程量程,一般两次就能确定。这个动作我每次都做,比翻手册快。
4.2 32 位整型与浮点的字序:ABCD/CDAB/BADC/DCBA
32 位数据占两个连续寄存器,问题在于这两个寄存器的先后和每个寄存器内部字节的先后,组合出四种常见排布。ABCD 是标准大端;CDAB 是字交换,很多电表和温控仪表用它;BADC 是字节交换;DCBA 是全反。同一个浮点数 25.5,四种排布解析出来的结果天差地别,而设备不会告诉你它用的是哪种。
import struct def parse_f32(regs: list, order: str = "ABCD") -> float: """regs 为按寄存器顺序读到的两个 16 位值""" raw = struct.pack(">HH", regs[0], regs[1]) # 先统一成 ABCD 字节序列 if order == "CDAB": raw = raw[2:4] + raw[0:2] elif order == "BADC": raw = bytes([raw[1], raw[0], raw[3], raw[2]]) elif order == "DCBA": raw = raw[::-1] return struct.unpack(">f", raw)[0] def parse_u32(regs: list, order: str = "ABCD") -> int: raw = struct.pack(">HH", regs[0], regs[1]) if order == "CDAB": raw = raw[2:4] + raw[0:2] return struct.unpack(">I", raw)[0]确定字序的可靠办法不是猜,是让设备输出一个已知值。把某路温度设成 25.5,读回两个寄存器,手工按四种排布各算一次,哪个等于 25.5 就用哪个。没有可写通道的,就找一个量程内的稳定读数,四种排布里只有一种会落在合理区间,其余三种通常是天文数字或者接近 0 的极小值。这个测试做一次记在设备档案里,后面所有点位复用。
浮点数的坑还有一层:有些设备内部是整型,用 0.1 或 0.01 的倍率存,手册上写“单位 0.1℃”,读回来 255 表示 25.5℃。把倍率当成浮点解析,值会小得离谱或大得离谱。解析前先确认数据类型和倍率,比事后调参数省时间。
4.3 位域与线圈打包:一个字节里塞 8 个状态
读线圈返回的是位打包数据,第一个字节的最低位对应起始地址那一路,依次往上排。数量不是 8 的整数倍时,最后一个字节的高位是填充位,必须按请求数量截断,否则会把填充位当成真实状态。
def unpack_bits(payload: bytes, count: int) -> list: """payload 为线圈响应中的数据域,count 为请求的位数""" bits = [] for byte in payload: for i in range(8): bits.append((byte >> i) & 1) # 低位在前 return bits[:count] # 截掉填充位 def pack_bits(values: list) -> bytes: out = bytearray((len(values) + 7) // 8) for i, v in enumerate(values): if v: out[i // 8] |= 1 << (i % 8) return bytes(out)位序写反是最常见的错误,现象是“8 路状态全反”或者“相邻两路互换”。判断方法很简单:只让第 1 路闭合,读回来应该是第 0 位为 1。写多线圈 0x0F 时,请求里要带“字节数”字段,它等于ceil(数量/8),这个字段算错,从站直接回异常码 03。有些设备还会把状态字(比如报警字)放在保持寄存器里,一个寄存器 16 位,解析逻辑同上,只是把它当成 16 位而不是 8 位来处理。
5. 避坑与排查:现场最常见的 5 个翻车现场
这一章按现象、原因、解决三段式记录,全是跑现场时真实撞过的类型。它们共同的特点是“看起来像设备坏了”,实际上都能从报文和参数里找到解释。遇到问题时按顺序过一遍,比盲改代码有效。
5.1 现象:偶发超时与零星 CRC 错误,重启就好一阵
现象:设备平时正常,每隔几分钟到几十分钟出现一次超时或 CRC 错误,重启上位机后恢复,跑久了又复发。
原因:三个方向。一是帧间隔设置过小,从站响应慢的时候主站提前判定帧结束,把响应拆成了碎片;二是 RS485 收发切换过早,最后一两个字节被截断;三是接收缓冲区里的残留字节没清,上一次的尾巴被当成本次的头。第三种最隐蔽,因为它在“请求失败后立即重试”的逻辑里会自我放大,越重试越乱。
解决:把帧间隔调到理论值 1.2 倍以上;切换延时改成等待发送完成中断,或者加 2 个字符时间的保险;每次发请求前清空输入缓冲区(代码里的reset_input_buffer);失败重试前先丢弃缓冲区里的所有字节,不要直接重发。连续三次 CRC 错误就停下来报错,而不是无限重试,否则会把一条坏帧的影响扩散到整轮轮询。
5.2 现象:TCP 一次 recv 读到两帧,数据串位
现象:解析出来的寄存器值偶尔对不上,或者直接解析异常,抓包看设备明明返回了正确数据。
原因:TCP 是字节流,没有消息边界。recv(1024)可能返回半帧,也可能返回两帧粘在一起,尤其是轮询周期短、响应快的时候。如果代码里假设“一次 recv 就是一帧”,迟早出错。UDP 思维写 TCP 程序,这是典型症状。
解决:严格按 MBAP 长度字段切分:先读满 7 字节头,解出length,再读满length - 1字节的 PDU。缓冲区里多出来的字节留给下一次解析,不要丢。请求侧用事务标识符配对,响应 tid 和请求 tid 不一致就丢弃。开了长连接还要考虑服务端主动断开,读到 0 字节要重连并重建缓冲。这层逻辑自己写一遍,比依赖封装库的“自动拆包”更可控。
5.3 现象:数值差 65536 倍,或者变成 1.4e-38 这种怪数
现象:温度显示 -0.0,压力显示天文数字,或者本该是 1000 的值读出来是 65536000。
原因:字序或字节序错了。32 位数据高低字颠倒,等于把数值乘以 65536;浮点数按整型解析,按 IEEE754 规则会解出极小或极大的值。还有一种情况是符号位处理错了,16 位有符号数当无符号读,-1 变成 65535。
解决:用已知值做一次四选一测试,确定字序后写进设备档案;有符号数统一用struct.unpack(">h")而不是>H;浮点数先确认设备是不是用整型加倍率存的。解析函数的入参保持“寄存器原始值列表”,把字序转换放在最外层做一次,不要在每个点位重复写转换逻辑,否则改一处漏一处。
5.4 现象:轮询越跑越慢,最后整轮卡死
现象:刚启动时一轮 200 毫秒,跑几小时后一轮要几秒,界面数据停止刷新,但线程还活着。
原因:串口读超时设得比轮询周期还长,某台从站掉线后每次都等满超时;重试次数没有上限,一台坏设备拖垮整轮;或者用了阻塞式写,转换器异常时写操作永久挂起。多线程共用同一个串口句柄也是常见诱因,两个线程交叉收发,帧就乱了。
解决:串口超时按“单帧理论传输时间 × 2 + 从站处理时间”设置,一般 200 到 500 毫秒够用;单台设备连续失败 N 次(N 取 3)就标记离线,跳过它继续轮询其余设备,隔几轮再探活;串口访问加锁,或者干脆用单线程串行轮询。RTU 半双工本来就是串行的,多线程并发不会更快,只会更乱。
5.5 现象:网关后面挂多台从站,只有第一台能通
现象:直连单台设备正常,接上网关后只有地址为 1 的那台能读到数据,其余全部超时或回异常码 0B。
原因:MBAP 里的单元标识符没有随目标从站变化,一直是 1;或者网关配置的 RTU 参数(波特率、校验位)和后端设备不一致;也可能是网关的从站地址映射表没建,请求根本转发不出去。
解决:确认单元标识符填的是后端 RTU 从站地址,不是网关自己的地址;用网关的配置工具核对串口参数和超时;先用网关自带的调试页面单点测试每台设备,全部通了再回到程序里批量轮询。异常码 0B 表示网关找不到目标,0A 表示网关路径不可用,这两个码基本可以把问题锁定在网关配置而不是你的报文。
6. 上线前先做这三件事:轮询周期估算、异常注入与配置留档
控件包跑通示例只算及格,能不能扛住现场,取决于上线前有没有做验证。我一般先算轮询周期:单次事务耗时约等于“请求帧字节数 + 响应帧字节数”乘以字符时间,再加上从站处理时间和帧间隔。9600 波特率、8N1 下字符时间约 1.15 毫秒,读 10 个寄存器是 8 字节请求加 25 字节响应,共 33 字节约 38 毫秒,加 30 毫秒处理、5 毫秒间隔,单次约 73 毫秒。32 台设备一轮就是 2.3 秒,如果业务要求 1 秒刷新,就得砍点位数、提波特率或者分组轮询。
def estimate_cycle(baud: int, req_len: int, resp_len: int, slave_ms: float = 30, gap_ms: float = 5, slaves: int = 1) -> float: char_ms = 11 * 1000 / baud # 11 位:起始 + 8 数据 + 校验 + 停止 per_slave = (req_len + resp_len) * char_ms + slave_ms + gap_ms return per_slave * slaves # 单位毫秒再做事先的异常注入:拔掉一台从站的线,看轮询会不会卡死;把某台设备的地址改成不存在的值,看离线标记和恢复是否正常;用防火墙挡掉 502 端口,看 TCP 重连是否带退避。这三项测完,现场八成的问题在实验室就暴露了。最后把串口参数、字序、地址基准、倍率写进一份配置表随代码走,换人接手时不用重新猜一遍。
说个我自己的习惯:每次新接一类设备,先花十分钟手写一帧报文,用串口助手或nc发出去,确认返回的每一个字节都对得上,再去写业务代码。这个动作救过我很多次,也让我少熬了不少夜。希望帮到你。
本文还有配套的精品资源,点击获取