简介:面向C#开发者与工业自动化工程技术人员,这份资源提供了一套完整的汇川H5U PLC Modbus-TCP通信示例,适用于设备数据采集、远程监控、产线调试等工业联网场景。压缩包内共61个文件,其中源码文件多达26个,以C#语言实现,辅以XAML界面、解决方案、配置与缓存等文件,整个包仅164KB,轻量易用,在Visual Studio中可直接打开运行。项目结构清晰,源码中PLC控制类与数据类分工明确,基于NModbus库及TcpClient实现TCP连接、读写保持寄存器和线圈,并配有交互界面以便观察通信结果。读者能借此深入理解Modbus-TCP协议在502端口的工作机制,掌握从工程搭建、协议封装到功能调试的完整编码流程,代码稍作修改即可迁移至其他支持Modbus的PLC设备。目前已有1561人学习使用,是快速上手工业通信开发的优质实用参考。
1. 不只是写个TCP客户端:C#对接汇川H5U的Modbus-TCP,真正卡住你的是寄存器映射和字节序
做上位机跟PLC通讯,最容易产生「这不就是发几个字节嘛」的错觉。等你自己用C#写出来,连上汇川H5U,发一条读D0的命令,收到的数据要么全是0,要么数值大到离谱,才知道Modbus-TCP这条链路里,TCP连接只是第一步,后面每一帧报文的拼法、PLC侧的地址映射、数据字节序,甚至PLC里的Modbus使能开关没打开,都能让你卡上半天。这篇文章不是讲Modbus协议基础,而是直接按汇川H5U这个具体从站设备,讲C#客户端怎么把报文拼对、把地址对上、把数据正确解析出来,并把我实际调试中遇到过的坑和参数设置一并写下。适合正在写上位机、需要对接汇川H5U的工程师参考,也适合刚接触C# Modbus通讯的人用来快速落地。
2. 汇川H5U的Modbus-TCP从站行为:先搞清寄存器地址映射,再谈C#代码
很多人一上来就找C#的Modbus库,其实最大的坑在PLC侧。H5U作为Modbus-TCP从站,它的内部软元件地址(D、M、Y、X、SM等)跟Modbus协议里的地址区间是有映射关系的。如果映射错了,后面所有代码都白写。
2.1 H5U的Modbus地址怎么对到D区、M区、SM区
汇川H5U的内置以太网口支持Modbus-TCP从站功能。常见做法是把D区(数据寄存器)映射到Modbus的保持寄存器,功能码03/06/16就是操作这一区的;M区和Y区映射到线圈,功能码01/05/15操作;X区映射到离散输入,功能码02;模拟量通道映射到输入寄存器,功能码04。注意,这只是最典型的一种映射,具体用的哪个Modbus地址偏移量,不同固件版本、不同型号可能会差一个基址。
我一般会先在H5U的编程软件AutoShop里检查PLC程序的系统参数,找到「内置以太网」或「Modbus从站配置」页。里面会列出一张表,比如D区起始地址对应Modbus保持寄存器40001,或者D0对应40001。这里的「对应」指的是:你要读D0,Modbus请求里的寄存器地址要填0(因为40001的协议地址是0,即地址寄存器值为0x0000);要读D100,地址就填100(0x0064)。有增强版固件会多一个偏移量,比如D100在协议地址里是100,但有些上位机习惯直接发100,有些要加1,以PLC侧配置页显示为准。
如果PLC侧把Modbus从站功能默认关闭了,你C#连上TCP端口(默认502)也不会收到正确响应,甚至连接直接被拒。很多第一次调试的人,代码没问题,就是PLC没开这个开关,卡了一下午。
2.2 C#里建立TCP连接前的两个前置检查
在写通讯代码之前,先用网络工具把这两件事确认了,能省不少时间。
第一,PLC的IP地址要能ping通。H5U默认IP可能与电脑不在同一网段,比如PLC是192.168.1.10,电脑是192.168.2.x,那就需要修改电脑网卡IP或在PLC侧把IP改过来。这一步属于基础网络配置,但实际项目中经常被忽视,尤其是有多个网卡、虚拟机的开发机。我习惯在命令行先ping一下PLC的IP,ping通了再继续。
第二,确认PLC的Modbus-TCP服务端口没有被防火墙或其他设备挡掉。H5U默认端口是502。如果ping通但TCP连接失败,就要检查PLC所在的防火墙策略,以及电脑的杀毒软件是否拦截了外连。用C#写TcpClient连接超时控制在2秒以内,连不上就报错,比较利于定位。
前置检查做完了,再往下走代码才稳妥。不要把时间浪费在怀疑自己的代码上,先确认底层链路。
3. 用C#写一个能跑的Modbus-TCP客户端:从读线圈到写寄存器
这里给出一个不依赖第三方库的C#实现思路。Modbus-TCP报文结构本身很简单:事务标识符(2字节)、协议标识符(2字节)、长度(2字节)、单元标识符(1字节)、功能码(1字节),后面跟数据。我们按这个结构把字节拼出来,发给PLC,然后读响应。
3.1 报文结构:事务ID、协议标识、长度、单元ID、功能码怎么填
Modbus-TCP的报文头部固定7字节(MBAP头),具体如下:
- 事务标识符(Transaction ID):2字节,每次请求递增,用来匹配请求与响应。同一时刻可以发多条请求,用事务ID区分。
- 协议标识符(Protocol ID):2字节,Modbus协议为0。
- 长度字段(Length):2字节,表示后续字节数,即从单元标识符开始到数据末尾的总字节数。比如一条读命令,单元ID(1字节)+功能码(1字节)+起始地址(2字节)+寄存器数量(2字节),共6字节,长度字段就填6。
- 单元标识符(Unit ID):1字节,一般填0或1,取决于PLC的从站地址配置。H5U通常填0或1都能通,但最好在PLC的从站配置里确认,默认一般是1。
功能码:
- FC01:读线圈(读M或Y区)
- FC02:读离散输入(读X区)
- FC03:读保持寄存器(读D区)
- FC04:读输入寄存器(读模拟量)
- FC05:写单个线圈(写Y区或M区)
- FC06:写单个寄存器(写D区单字)
- FC15:写多个线圈(写M区连续位)
- FC16:写多个寄存器(写D区连续字)
有了这个表,拼报文就有依据了。
3.2 读H5U的D区数据:读保持寄存器(功能码03)的C#实现
读D区,最常用的是FC03。下面是一段最小可用的C#代码,把报文拼好、发送、接收、解析。
public byte[] ReadHoldRegisters(string ip, ushort startAddr, ushort count) { // 1. 构造MBAP头 byte[] buffer = new byte[12]; // 头部6字节 + 请求数据6字节 ushort transId = 0x0001; // 事务ID,实际使用时应递增 ushort protocolId = 0x0000; // Modbus协议 ushort length = 6; // 后续字节数:单元ID+功能码+起始地址+数量 = 1+1+2+2 = 6 byte unitId = 0x01; // H5U从站地址,按PLC配置 // 2. 填入MBAP头 buffer[0] = (byte)(transId >> 8); buffer[1] = (byte)(transId & 0xFF); buffer[2] = (byte)(protocolId >> 8); buffer[3] = (byte)(protocolId & 0xFF); buffer[4] = (byte)(length >> 8); buffer[5] = (byte)(length & 0xFF); // 3. 填入请求数据 buffer[6] = unitId; buffer[7] = 0x03; // 功能码FC03 buffer[8] = (byte)(startAddr >> 8); buffer[9] = (byte)(startAddr & 0xFF); buffer[10] = (byte)(count >> 8); buffer[11] = (byte)(count & 0xFF); // 4. 发送数据并接收响应 using TcpClient client = new TcpClient(); client.Connect(ip, 502); client.Client.SendTimeout = 2000; client.Client.ReceiveTimeout = 2000; NetworkStream stream = client.GetStream(); stream.Write(buffer, 0, buffer.Length); // 读取响应头,先读7字节的MBAP头 byte[] head = new byte[7]; int read = stream.Read(head, 0, 7); if (read < 7) throw new Exception("响应头不完整"); // 根据长度字段读取响应数据 int dataLen = (head[4] << 8) | head[5]; dataLen -= 1; // 去掉单元ID字节 byte[] data = new byte[dataLen]; int offset = 0; while (offset < dataLen) { int n = stream.Read(data, offset, dataLen - offset); if (n == 0) throw new Exception("连接已关闭"); offset += n; } // data[0]是功能码,data[1]是字节数,后面是寄存器值 return data.Skip(2).ToArray(); }这段代码的逻辑说明:先把事务ID、协议ID、长度字段按大端模式填入数组,再把单元ID、功能码、起始地址、数量同样按大端填入。Modbus-TCP的头部字段都是大端(高字节在前),所以用右移8位取高字节。发送后,响应的MBAP头里也带长度字段,根据长度读取剩余数据。返回的byte数组去掉了功能码和字节计数字段,直接是寄存器值。
参数说明:startAddr是协议地址,不是PLC上的D区编号。如果PLC地址表里写的是D0对应协议地址0,那读D0就传startAddr=0;如果PLC配置里写的是D0对应协议地址1,那就要传1。count是要读的寄存器个数,注意一次不要读太多,H5U作为从站一般限制一次最多读125个寄存器,超过会返回异常码。事务ID在每次请求时应该累加,不能用固定1,否则在多发并发请求时容易乱。
3.3 写H5U的Y输出:写单个线圈(功能码05)与写多个寄存器(功能码16)
写单线圈,比如控制Y0输出,用FC05。报文数据区是:功能码(1字节)+线圈地址(2字节)+输出值(2字节)。输出值0xFF00表示ON,0x0000表示OFF。下面是一个直接构造写线圈请求的方法。
public void WriteSingleCoil(string ip, ushort coilAddr, bool value) { // 写单线圈,用功能码05 byte[] buffer = new byte[12]; ushort transId = 0x0002; buffer[0] = (byte)(transId >> 8); buffer[1] = (byte)(transId & 0xFF); buffer[4] = 0; buffer[5] = 6; // 长度:单元ID+功能码+地址+值 = 1+1+2+2 = 6 buffer[6] = 0x01; buffer[7] = 0x05; buffer[8] = (byte)(coilAddr >> 8); buffer[9] = (byte)(coilAddr & 0xFF); buffer[10] = value ? 0xFF : 0x00; buffer[11] = value ? 0x00 : 0x00; // 高字节是0xFF,低字节是0x00 using TcpClient client = new TcpClient(); client.Connect(ip, 502); client.Client.SendTimeout = 2000; client.Client.ReceiveTimeout = 2000; NetworkStream stream = client.GetStream(); stream.Write(buffer, 0, buffer.Length); // 读取响应并检查功能码是否与请求一致 byte[] resp = new byte[12]; int n = stream.Read(resp, 0, resp.Length); if (n < 12) throw new Exception("写线圈响应不完整"); if (resp[7] != 0x05) throw new Exception("写线圈失败,功能码异常"); }这里的关键点在于:H5U的Y区是位寄存器,写Y0对应线圈地址是什么,需要查PLC侧Modbus映射表。有些型号的Y0地址是0,Y1是1;也有按十六进制偏移的。我的经验是,用AutoShop里的「系统设置→Modbus从站」查看,不要凭猜。同时,Y区是PLC输出,直接写Y可能会和PLC程序里对Y的赋值冲突,PLC扫描周期一到,Y值会被程序覆盖。所以实际项目中控制设备动作,更稳妥的做法是写M区中间变量,再由PLC程序把M区映射到Y区。这个坑后面会详细讲。
写多个寄存器,比如一次写D100到D105连续6个字,用FC16。报文数据区为:功能码+起始地址(2字节)+寄存器数量(2字节)+字节数(1字节)+数据值(每寄存器2字节)。下面代码演示了怎么拼写多个寄存器的报文。
public void WriteMultiRegisters(string ip, ushort startAddr, ushort[] values) { // 功能码16写多个寄存器 int count = values.Length; byte[] buffer = new byte[13 + count * 2]; // 7头 + 1单元 + 1功能码 + 2地址 + 2数量 + 1字节数 + 数据 ushort transId = 0x0003; ushort length = (ushort)(6 + 1 + count * 2); // 单元+功能码+起始地址+数量+字节数+数据字节数 buffer[0] = (byte)(transId >> 8); buffer[1] = (byte)(transId & 0xFF); buffer[4] = (byte)(length >> 8); buffer[5] = (byte)(length & 0xFF); buffer[6] = 0x01; // 单元ID buffer[7] = 0x10; // FC16 buffer[8] = (byte)(startAddr >> 8); buffer[9] = (byte)(startAddr & 0xFF); buffer[10] = (byte)(count >> 8); buffer[11] = (byte)(count & 0xFF); buffer[12] = (byte)(count * 2); for (int i = 0; i < count; i++) { buffer[13 + i * 2] = (byte)(values[i] >> 8); buffer[14 + i * 2] = (byte)(values[i] & 0xFF); } // 发送与读取响应,代码略 }参数说明:values是ushort数组,每个元素对应一个寄存器的16位值。注意Modbus寄存器传输是高字节在前,所以要做和读命令一样的位移处理。如果values里的数值是从C#的Int32转过来的,比如一个32位浮点数,那就不是简单地拆成两个ushort了,还要考虑字节序和组合方式,这在下节说。
4. 把这套通讯跑成上位机轮询:超时、重连、字节序和并发请求的取舍
单条命令通了,不等于系统能稳定跑。实际业务里,上位机是周期轮询PLC的D区、M区数据的,怎么设计这个轮询,参数怎么设,会直接影响整个产线的节奏。
4.1 轮询周期与超时参数:100ms还是500ms?看PLC扫描周期
H5U的扫描周期通常在毫秒级,比如1ms到10ms,取决于程序量。Modbus-TCP请求从发送到PLC响应,这个时间包括网络传输时间 + PLC扫描周期内对Modbus服务的处理时间。我一般把轮询周期定在100ms到200ms之间。如果低于50ms,PLC可能每5个周期才处理一次Modbus请求,容易在服务端形成消息堆积,尤其在写操作比较频繁时会挤占PLC的通讯处理时间。
超时参数也要匹配轮询周期。如果轮询周期是100ms,超时设为500ms即可,不要设置成10秒。否则一旦PLC侧通讯卡住,你下一次查询要等10秒才报错,上位机界面会假死,产线上看起来就像设备停了。建议超时 = 轮询周期 × 3 ~ 5,并且小于上位机界面更新的节拍。
重连策略上,我倾向于主动断开 + 定时重连,而不是依赖TCP KeepAlive。因为H5U的TCP栈在很多固件下KeepAlive行为不太一致,你明明发现链路断了,Socket却还显示Connected。常见做法是:每次请求前检测连接状态,如果发送或接收超时,主动把TcpClient Dispose掉,然后每3秒重连一次。重连成功后先读一个固定地址确认通讯正常,再进行业务轮询。
4.2 字节序陷阱:汇川H5U是低字节在前,C#的BitConverter要反转
这是整个通讯中最容易被坑的一点。Modbus协议规定寄存器数据是高字节在前,但PLC内部存储D区数据时,可能按照低字节在前(小端)。具体到汇川H5U,很多工程师实测发现,直接读D区出来,把两个字节按高字节在前拼成ushort,数值是反的。比如D0里存的是十进制100(0x0064),Modbus响应回来的字节却是0x00 0x64,但如果你按大小端转换,C#的BitConverter.ToInt16默认按系统的小端解析,得到0x6400,就是25600。所以你必须手动做字节序转换。
下面是我常用的转换函数,把Modbus返回的两个字节按PLC实际字节序解析成C#的ushort。
public static ushort ModbusBytesToUShort(byte[] data, int startIndex) { if (data.Length < startIndex + 2) throw new ArgumentException("数据长度不足"); // H5U实测:高字节在前(Modbus标准),所以直接按大端解析 return (ushort)((data[startIndex] << 8) | data[startIndex + 1]); }注意,这里说的「大端解析」是指Modbus报文本身的大端。但H5U内部D区如果存的是小端数据,比如32位float,情况就更复杂了:一个32位浮点数占用两个寄存器,编码顺序是「高字在前还是低字在前」?汇川的常见排列是:低字节在前,即第一个寄存器存浮点数的低16位,第二个寄存器存高16位。C#里要将两个寄存器拼成float,需要先交换寄存器顺序,再调用BitConverter。
public static float ModbusRegistersToFloat(ushort regLow, ushort regHigh) { // 将两个寄存器按高字在前,低字在后拼成32位 byte[] bytes = new byte[4]; byte[] low = BitConverter.GetBytes(regLow); byte[] high = BitConverter.GetBytes(regHigh); // 小端系统下,低地址放低字节,所以先放low的低字节,再放low的高字节,再放high... bytes[0] = low[0]; bytes[1] = low[1]; bytes[2] = high[0]; bytes[3] = high[1]; return BitConverter.ToSingle(bytes, 0); }但这种转换不一定适用于所有固件版本。我的建议是,用PLC侧写入一个已知浮点数,比如1.5,然后读回来,看看怎样排列才能得到1.5。把这一步做成调试函数,放在通讯模块的初始化里自动校验,比靠手册猜测稳得多。字节序处理错了,数据就是黑匣子,怎么看都不对。
4.3 断线重连与心跳:自己实现还是依赖TCP KeepAlive
我在实际项目里不信任TCP KeepAlive,因为它的默认探测周期太长(Linux下默认7200秒),且H5U作为从站时,它的系统不一定会按照你的期望维持连接。常见做法是自己写心跳:每隔一定时间(比如1秒)用功能码03读一个固定寄存器,如果连续3次没有正常响应,就判定链路异常,主动断开,走重连流程。
重连不能太频繁,给PLC一点恢复时间。可以设计成指数退避,第一次重连延迟1秒,第二次2秒,第三次5秒,最多间隔10秒。这样不会把PLC的Modbus服务打崩,也不会在上位机上产生大量超时日志。
5. 汇川H5U Modbus-TCP通讯避坑:5个让我翻过车的现场问题
这里列几个在实际调试中反复出现的问题,每一条都是「现象→原因→解决」的完整记录。这些坑有的我一开始就撞上,有的则是设备跑了快半年才暴露出来。
5.1 现象:连上了但读到的全是0或异常大数
原因:寄存器地址偏移没对上。你按D0读,但H5U的Modbus地址表里D0不在协议地址0的偏移上。可能PLC侧配置了偏移量(比如从地址0开始,但中间间隔了其他区域),或者你读的地址区域本身不是D区。还有一种可能:PLC程序根本没有往D0里写数据,而D区默认值是0。排除方法:先用手动方式在PLC里给D区赋一个已知值(比如123),再读出来对比。
解决:打开AutoShop的Modbus从站配置表,把D区对应的协议地址起始值抄出来,修改C#代码里的startAddr。另外,读回来的数据在转换为int时,注意是有符号还是无符号。H5U的D区默认是带符号整数,但有时上位的业务数据是使用无符号格式,解析方式错了数值就完全不对。
5.2 现象:写寄存器成功,但PLC里的D区没变
原因:你写的是PLC程序正在使用的D区,PLC扫描周期又定时把旧值刷新回来了。比如你的上位机写了D10,但PLC梯形图里每个扫描周期都把另一个值赋给D10,那Modbus写入的结果瞬间就被覆盖。这个场景在写Y区时更常见,Y区本来就是输出映像区,PLC每一轮扫描会根据程序逻辑刷新Y,你从外部写Y等于和PLC程序抢控制权,基本抢不赢。
解决:不要直接往Y区写。在PLC程序里划分一片M区或D区作为上位机命令缓冲区,把上位机要控制的输出逻辑用梯形图把M区映射到Y区。上位机只写M区,PLC程序再根据M区状态去驱动Y。这样更安全,也方便在PLC内做联锁保护。
5.3 现象:PLC忙时过来读会超时,但TCP连接没断
原因:H5U作为从站,在它忙着处理其他通讯请求(比如程序里用了多个Modbus连接,或者工程调试工具也连着一台H5U)时,对新的请求响应会延迟,甚至丢掉报文。你的C#客户端如果只发一次请求,超时设置又短,很容易触发误报。
解决:在同一时刻只发起一个Modbus TCP连接,不要多个客户端同时连接同一个H5U,除非H5U明确支持多连接(一般最多支持16个,但总处理能力有限)。C#侧做串行轮询,一次请求完成后再发下一次,中间留1~2ms小延时。在PLC侧也检查一下AutoShop的在线监控是否开着,那个工具也会占Modbus连接。
5.4 现象:多个上位机同时读写,PLC偶发复位
原因:有可能不是通讯造成的,而是你写的是PLC保持继电器或特殊寄存器,比如向SM区写入了不该写的值,导致PLC系统状态改变。另一个原因是写多个寄存器时数量超过了H5U允许的范围,PLC产生异常响应,虽然没有复位,但通讯模块重启了。也可能是上位机发送了非法功能码,PLC会返回异常码,但不至于复位。
解决:严格控制写入范围,不要跨区域写。写多个寄存器时,一次最多125个。PLC程序里对关键M区做初始化保护,防止上位机在PLC刚从STOP切到RUN时立刻写入,造成程序逻辑错误。还有,Modbus事务ID如果一直不递增,某些固件会认为是重复包,处理异常。
5.5 现象:H5U断电重启后,TCP连接不再恢复
原因:H5U重启后,它的TCP连接表里还残留着旧连接的信息,而你的C#客户端那个Socket可能还没被操作系统回收,导致双方都等另一方超时。TCP KeepAlive在上位机侧默认是关闭的,连接断了半天,Socket读不出异常。
解决:C#侧实现断线检测逻辑比较直接。方法是在读取响应时,给ReceiveTimeout设置2秒。在这个超时期间如果读不到数据,就认为PLC侧已经掉线,主动Close掉Socket,并进行重连。重连成功后,一定要重新发送一次事务ID递增后的请求,不要沿用旧Socket。
6. 落地到真实项目:H5U通讯参数模板与自检步骤
前面说了原理也给了代码,最后这一章给你一套可以直接拿去用的参数表和自检流程,照着做基本不会翻车。
6.1 一张参数表搞定从PLC侧到C#侧
| 项目 | 推荐值 | 说明 |
|---|---|---|
| PLC侧Modbus从站开关 | 开启 | 在AutoShop系统设置里打开,并设置单元ID为1 |
| PLC侧IP地址 | 192.168.1.10(按项目) | 与上位机必须在同一网段 |
| PLC端口 | 502 | 默认 |
| 轮询周期 | 100ms~200ms | 别低于50ms |
| 单次读寄存器数 | 最多120 | 留余量,不要卡着125的上限 |
| 单次写寄存器数 | 最多100 | 写多了容易触发PLC异常 |
| 超时时间 | 500ms | 响应慢时先查PLC负载 |
| 重连间隔 | 3s起步,最大10s | 指数退避 |
| 字节序 | 高字节在前(Modbus报文),浮点数需实测 | 用已知值验证 |
这套参数是我在不同项目上逐步试出来的。如果你的设备通讯量特别大,比如一次要读200个寄存器,我建议拆成两次读,每次100个,不要贪多。
6.2 用Modbus Poll或自带调试工具先验证,再上C#
不要上来就写代码。我第一次做H5U上位机时,直接用C#客户端调试,报错了也分不清是协议问题还是地址问题。后来学乖了,先用Modbus Poll这类万用Modbus调试工具连PLC,读同一个地址,确认数据和PLC里一致,再回到C#去实现。C#代码排错时,也可以先用调试工具把原始报文抓出来,对比C#发出的报文,很快就能找出拼包错误的地方。H5U的AutoShop软件里通常自带Modbus从站监视功能,可以看PLC收到了什么功能码,回发了什么异常码,是排查通讯故障的利器。
6.3 日志记录:每次收发都留十六进制原文,才能快速定位
你的C#上位机一定要把收发双方的完整报文记录成日志,包括事务ID、功能码、内容。不要把数据转换成int就完事了,原始hex才是定位问题的根本。比如当你发现读到的值不对,回看日志里响应报文的hex,如果是0x00 0x64,解析出来是100,那字节序没有错;如果是0x64 0x00,那就是PLC侧字节序特殊,必须反转。没有hex日志,你只能靠猜,那是最浪费时间的。
日志里还要记录PLC的响应时间,即从发送到收到响应的间隔。如果这个时间波形突然变大,说明PLC负载高了或者网络有波动,可以提前预警生产线的通讯健康状态。我现在写的通讯模块里,这个间隔是必记录字段,排查问题时直接看折线图,一眼就能发现异常点。
最后说一个我自己的习惯:在C#通讯模块里留一个「写入验证」方法,每次写一个寄存器或线圈后,立即回读一遍,确认写入生效。这在调试阶段能省下大量排查时间,因为在PLC侧误写导致设备动作异常时,你能从日志上明确区分是写入失败还是PLC程序后续覆盖了。这种回读逻辑只在调试阶段启用,正式运行时去掉,避免每写一次就多一轮网络交互,拖慢轮询节奏。希望这套从寄存器映射到字节序再到重连策略的完整思路能帮到你,照着这个框架去落地,H5U的Modbus-TCP通讯应该不会再是什么难题。
本文还有配套的精品资源,点击获取