ARTICLE DETAIL

资讯详情

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

C#与汇川PLC通信实战:基于Modbus TCP的完整指南

C#与汇川PLC通信实战:基于Modbus TCP的完整指南 简介一套面向C#开发者的汇川PLC通信示例工程专门服务于工业自动化集成与上位机软件开发场景也适合相关专业学生进行项目实践。示例聚焦通过Modbus TCP协议与汇川PLC进行数据交换覆盖C#网络编程、寄存器读写、报文封装及异常处理等关键环节能够帮助开发者从零搭建可靠的通信链路。压缩包共624个文件大小约214.54MB主要包含C#/C源代码、Visual Studio解决方案与项目文件、可直接运行的exe程序以及docx/pdf说明文档同时附带部分构建缓存及日志文件便于查阅和二次开发。已有574人学习下载。示例中不仅提供了多语言项目实例如ModbusTCPDemo、StandardModbusApi等还展示了基于ModbusApi库的调用方式与界面集成思路可帮助读者快速理解C#与工业设备通信的实现路径减少实战中的踩坑成本。1. 先从需求说起C#和汇川PLC到底怎么搭上线最近在产线改造里又做了一轮C#上位机和汇川PLC的通信对接从H5U到AM系列都过了一遍。今天就把整个通信过程的思路、代码、踩坑点完整梳理一遍给正在做类似项目的朋友一个可以直接“抄作业”的参考。先说清楚这套东西解决什么问题你的设备上有一台汇川PLC在控制电机、气缸、传感器这些现场设备而你的电脑常称上位机上跑着一个C#写的WinForm或WPF程序需要把产线上采集到的数据存到数据库、显示在界面上或者下发工艺参数给PLC。要做到这些第一步就是让C#程序能和PLC“说上话”。C#扫码头、C#与PLC通信这类需求在3C、锂电、汽车零部件这些行业里非常普遍。我这次做的主要是基于以太网的通信协议优先选了Modbus TCP因为汇川的H5U、AM400/AM600、Easy系列都原生支持不需要额外买通信扩展卡也不需要装厂商私有的SDK一套标准Modbus TCP指令就能搞定。这篇笔记适合谁刚接触上位机开发的工程师做过PLC但没写过C#通信的电气工程师以及想搞清楚“上位机和PLC到底怎么交互”的入门者。如果你已经用厂商SDK通信过也可以看看用标准Modbus TCP能简化多少事。2. 通信方案选型为什么最后选了Modbus TCP2.1 三种常见通信方式对比汇川PLC对外通信大体有三条路串口Modbus RTU、以太网Modbus TCP、厂商私有协议如InoProShop内部协议、EtherCAT等。我实际对比过三者的差别通信方式硬件需求传输速度开发难度适用场景Modbus RTURS232/RS485接口9600~115200bps较慢低数据帧简单老设备、近距离、数据量小Modbus TCP网口RJ45百兆/千兆以太网毫秒级响应低Socket编程即可大部分中大型产线、多站远程厂商私有协议网口、专用DLL快但依赖厂家SDK中高需读厂家协议文档特殊功能、私有寄存区访问我最常用的方案是Modbus TCP。原因很直接一是通用只要PLC支持标准Modbus换品牌也能用同一套C#代码二是调试方便网线一插Wireshark抓包就能看到报文内容三是C#的Socket库或第三方库如NModbus封装得已经很成熟代码量可以压得很低。如果你用的是汇川H5U或者AM系列PLC本体默认支持Modbus TCP从站功能参数都不用额外调给一个端口号就能接收请求。2.2 标准Modbus TCP报文结构我拿一个实际请求举例C#作为Modbus客户端主站读取PLC的保持寄存器报文拆开看是这样的事务处理标识符Transaction Identifier2字节区分同一连接上的不同请求随便填个自增整数就行响应里会原样返回。协议标识符Protocol Identifier2字节固定0x0000表示是Modbus协议。长度Length2字节后面还有多少个字节。单元标识符Unit Identifier1字节标识从站设备号默认0xFF或0x01。功能码Function Code1字节0x03表示读保持寄存器0x06写单个寄存器0x10写多个寄存器。起始地址Starting Address2字节寄存器地址。寄存器数量Quantity2字节本次读多少个寄存器。理解了这一段你就拿到了Modbus TCP通信的“钥匙”剩下的工作其实就是按规则填充这些字节再解析返回的数据。坐标类、温度类、产量类参数无非就是地址不同、数据格式不同。3. 核心前置准备PLC侧需要做什么不少C#工程师很熟悉Socket但一到现场就卡壳C#代码写好了PLC那边却怎么都不响应。绝大多数问题不是协议的问题而是PLC侧寄存器没弄对。所以在写C#之前先把汇川PLC这边的准备工作过一遍。3.1 汇川PLC的Modbus TCP配置我以H5U为例用AutoShop软件操作。先确认PLC固件支持Modbus TCP从站然后在软件里做两步打开“系统参数”或“通信配置”找到以太网设置给PLC设置一个固定IP假设192.168.1.10子网掩码255.255.255.0。确认Modbus TCP服务使能默认端口一般是502部分型号可自定义。PC这边顺便把网卡IP设为同网段比如192.168.1.100。这一步最简单但漏掉的人真不少——IP不在一个网段连不上是必然的。注意很多现场PLC已经接入了工厂生产网络IP冲突会引发严重通信中断。建议事先检查交换机下是否有其他设备占用目标IP再执行ping操作确认在线状态。3.2 寄存器地址映射写给C#程序员看的PLC工程师写程序时通常用“%MW100”这种方式表示保持寄存器地址。以H5U为例Modbus地址映射规则如下线圈0区%QX0.0按位访问对应Modbus功能码0x01/0x05C#里起始地址从0开始。离散输入1区%IX0.0只读对应功能码0x02。输入寄存器3区%IW100只读对应功能码0x04多用于读取从站模拟量数据。保持寄存器4区%QW100或%MW100可读可写对应功能码0x03/0x06/0x10这是最常用的区域。我遇到的坑是PLC里标注的%QW100对应Modbus TCP报文里的地址是99十六进制0x0063因为Modbus的地址编号从0开始而PLC的%QW从1开始编号实际是偏移量。这个“差1”的坑几乎每个新人都踩一遍。简单说你在C#代码里写“读取寄存器地址100”实际访问的是PLC里的%QW101如果PLC侧的从0开始或%QW100如果从1开始。最好的办法是先用PLC软件查一下对应寄存器的Modbus地址再填到C#里。3.3 日期时间寄存器是个什么概念搜索词里出现了“汇川PLC日期时间寄存器是什么”这个问题。简单解释部分汇川PLC如AM系列、H5U内部有系统时钟区可以通过特定寄存器地址读取或校准PLC的本地时间常用于配方追溯、报文时间戳等场景。具体地址要看该系列的手册不同型号不一样通常是若干连续的保持寄存器分别存年、月、日、时、分、秒。读出来的是整数C#这边需要用DateTime组装一下再显示。例如读取到年2025月06日18时14分30秒25组装代码就是DateTime plcTime new DateTime(2025, 6, 18, 14, 30, 25);这种寄存器一般建议以“读连续多个寄存器再拆字段”的方式处理而不是逐个读取效率高很多。4. C#通信核心代码从零手写一个Modbus TCP客户端4.1 极简版Socket读写先跑通再优化很多教程一上来就推荐NModbus之类的第三方库。我并不是反对用库但在项目前期我建议至少手写一次Socket版通信这样你对报文结构的理解才扎实后面遇到库解决不了的场景比如自定义超时、批量读写优化时心里有底。先看最核心的读取函数我把代码压缩到最小可运行状态using System; using System.Net; using System.Net.Sockets; using System.Text; public class ModbusTcpClient { private TcpClient _tcpClient; private NetworkStream _stream; private int _transactionId 0; public bool Connect(string ip, int port 502) { _tcpClient new TcpClient(); _tcpClient.Connect(IPAddress.Parse(ip), port); _stream _tcpClient.GetStream(); return _tcpClient.Connected; } public void Close() { _stream?.Close(); _tcpClient?.Close(); } public ushort[] ReadHoldRegisters(byte unitId, ushort startAddress, ushort quantity) { // 1. 组装请求报文 byte[] request new byte[12]; _transactionId; byte[] transId BitConverter.GetBytes((ushort)_transactionId); // Modbus TCP是大端序需要反转 request[0] transId[1]; request[1] transId[0]; request[2] 0x00; // 协议标识符高字节 request[3] 0x00; // 协议标识符低字节 request[4] 0x00; // 长度高字节 request[5] 0x06; // 长度低字节后面有6个字节 request[6] unitId; // 单元标识符 request[7] 0x03; // 功能码读保持寄存器 request[8] (byte)(startAddress 8); request[9] (byte)(startAddress 0xFF); request[10] (byte)(quantity 8); request[11] (byte)(quantity 0xFF); // 2. 发送并接收响应 _stream.Write(request, 0, request.Length); byte[] response new byte[9 quantity * 2]; int readCount 0; while (readCount response.Length) { int n _stream.Read(response, readCount, response.Length - readCount); if (n 0) throw new TimeoutException(连接关闭); readCount n; } // 3. 解析数据 ushort[] data new ushort[quantity]; for (int i 0; i quantity; i) { data[i] (ushort)((response[9 i * 2] 8) | response[10 i * 2]); } return data; } }这里解释三个细节。字节序Modbus TCP是大端序高字节在前。C#的BitConverter在小端环境下总是反的所以组装报文时我就手动手工指定高字节在前解析数据时同样手动组合。这一步最容易错一旦错了读出来的数会变成“大数乱跳”比如读到65535这种。读满整包_stream.Read()不保证一次把所有字节读完所以要用循环等完整响应。这也是新手常见的Bug只读了一次就开始解析结果数据错乱。一个简单的经验是响应长度通常是9字节MBAP头2×寄存器数量字节。超时处理默认TcpClient不设ReceiveTimeout的话一旦PLC断线程序可能卡死很久。建议在Connect后加上_tcpClient.ReceiveTimeout 2000; _tcpClient.SendTimeout 2000;这样2秒收不到响应就直接抛异常方便你做重连和告警。4.2 写寄存器修改单个和批量下发再上一个写单个寄存器的示例适合改速度、改产量目标这种单个参数的场景public void WriteSingleRegister(byte unitId, ushort startAddress, ushort value) { byte[] request new byte[12]; _transactionId; byte[] transId BitConverter.GetBytes((ushort)_transactionId); request[0] transId[1]; request[1] transId[0]; request[2] 0x00; request[3] 0x00; request[4] 0x00; request[5] 0x06; request[6] unitId; request[7] 0x06; // 写单个寄存器 request[8] (byte)(startAddress 8); request[9] (byte)(startAddress 0xFF); request[10] (byte)(value 8); request[11] (byte)(value 0xFF); _stream.Write(request, 0, request.Length); byte[] response new byte[12]; int readCount 0; while (readCount response.Length) { int n _stream.Read(response, readCount, response.Length - readCount); if (n 0) throw new TimeoutException(连接关闭); readCount n; } }批量写的功能码是0x10报文长度会变化结构是“写入起始地址寄存器数量字节数各寄存器值”。批量下发的好处是只需要一次收发多个参数可以原子性地写入PLC比如一次下发整条工单参数速度、温度、时间效率比循环单写高很多也能避免PLC参数更新到一半的情况。4.3 用第三方库还是自己写我的建议没有特殊需求的话我建议用NModbus库直接干活因为它把异常处理、超时重试、字节序都处理好了。写法大致是using Modbus.Device; TcpClient tcpClient new TcpClient(192.168.1.10, 502); var master ModbusIpMaster.CreateIp(tcpClient); ushort[] data master.ReadHoldingRegisters(0xFF, 100, 10); master.WriteSingleRegister(0xFF, 110, 50);注意NModbus里的ReadHoldingRegisters第一个参数是从站地址unitId这个要和PLC侧设置一致通常是1或255。但我不建议完全依赖库上面手写版的思路必须懂因为现场一旦出现库版本和.NET版本不兼容、功能码不支持等怪问题时手写版就是你的保底方案。5. 进阶场景扫码枪触发事件、异步通信和UI刷新5.1 扫码枪和PLC通信怎么结合很多产线结构是扫码枪串口或网络扫码→上位机收到条码→上位机把条码信息写入PLC对应寄存器→PLC根据条码判断工装夹具动作或调出配方。以C#接串口扫码枪为例using System.IO.Ports; SerialPort _serialPort new SerialPort(COM3, 115200, Parity.None, 8, StopBits.One); _serialPort.DataReceived new SerialDataReceivedEventHandler(DataReceivedHandler); private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { SerialPort sp (SerialPort)sender; string data sp.ReadExisting(); // 扫码枪数据通常以回车结尾去除首尾空白 string barcode data.Trim(); if (!string.IsNullOrEmpty(barcode)) { // 注意这里不能直接更新UI控件必须Invoke this.Invoke(new Action(() { txtBarcode.Text barcode; })); // 将条码写入PLC建议放到线程池或Task中避免串口事件线程阻塞 Task.Run(() { WriteStringToPlc(barcode); }); } }这个例子里最容易踩的坑是DataReceived事件运行在后台线程直接操作TextBox会抛跨线程异常必须用Invoke。另外条码写入PLC别放在串口事件里同步执行否则扫码枪下一帧数据来了没线程处理容易丢码。5.2 条码字符串如何写入PLC寄存器PLC的保持寄存器只能存16位整数一条条码字符串“ABC12345”怎么存常用做法是把ASCII字符按两个一组塞到一个寄存器里高字节放一个字符低字节放一个字符。示例代码public void WriteStringToPlc(string data, ushort startAddress, byte unitId 0xFF) { // 确保偶数长度 if (data.Length % 2 1) data ; int registerCount data.Length / 2; ushort[] values new ushort[registerCount]; for (int i 0; i registerCount; i) { byte high (byte)data[i * 2]; byte low (byte)data[i * 2 1]; values[i] (ushort)((high 8) | low); } // 调用批量写功能码0x10 WriteMultiRegisters(unitId, startAddress, values); }PLC侧你需要在程序中定义相同长度的字符串变量比如定义为STRING(24)然后在通信映射中把它关联到起始寄存器。两边约定好编码格式ASCII还是GBK和长度这是联调时最容易出分歧的点一定要写进通信协议文档。5.3 定时轮询还是事件触发数据刷新策略和PLC通信最常见模型就是“上位机定时读到PLC能存”。PLC周期性把状态写到指定的寄存器区域上位机开启一个后台循环每100ms~500ms批量读取一次。我用的是System.Timers.Timer加信号量避免上一次读取还没返回下一次又发出请求导致数据交错private Timer _timer; private SemaphoreSlim _commLock new SemaphoreSlim(1, 1); public void StartPolling(int intervalMs 200) { _timer new Timer(intervalMs); _timer.Elapsed async (s, e) { if (await _commLock.WaitAsync(0)) // 不排队拿不到锁就跳过本次 { try { ushort[] data await Task.Run(() ReadHoldRegisters(0xFF, 100, 20)); UpdateUI(data); } finally { _commLock.Release(); } } }; _timer.Start(); }WaitAsync(0)意味着如果上一次通信还在进行中直接跳过本次采集宁可丢一拍也不要产生乱序。这个细节能避免大量时序相关的诡异问题。5.4 异步等待还是阻塞等待写参数时的选择现场工程师按了按钮后上位机要下发一组参数给PLC。如果直接用同步阻塞方式一旦网络卡顿界面就卡死三五秒。我习惯的做法是“异步下发超时提示”private async void btnSend_Click(object sender, EventArgs e) { try { btnSend.Enabled false; bool ok await Task.Run(() { return WriteParametersAndConfirm(); // 内部写多个寄存器并校验 }); MessageBox.Show(ok ? 下发成功 : 下发失败请检查通信); } catch (Exception ex) { MessageBox.Show(异常 ex.Message); } finally { btnSend.Enabled true; } }关键点写参数一定要带回读校验不能只发不认。发完过200ms再读一遍确认PLC里的值确实是目标值再做“成功”提醒。这种习惯在产线上能少很多“以为写了实际没写”的纠纷。6. 实操中的坑与排查实录6.1 连接失败ping不通ip最经典的问题先查物理链路。网线没插好、交换机口坏了、电脑开了多个网卡导致路由混乱、防火墙拦截502端口都是高频原因。排查步骤用ping 192.168.1.10看通不通。通但连不上检查Windows防火墙是否放行502端口或者直接在代码里禁用防火墙测试。多个网卡时用route print看默认路由确保发往PLC的报文走的是正确网卡。用网线流量工具Wireshark抓包看TCP握手有没有黄色RST标志有就是被对端拒绝。6.2 能连上但读到的寄存器值不对这个概率也很大八成是地址偏移或字节序问题。我整理成一个速查表现象可能原因解决办法读出来的值一直65535或0地址配错、越界用PLC软件在线监控确认寄存器实际地址数值对但720/1440这种倍数关系字节序高低位反了解析时换一下高低字节组合顺序写入后PLC值没变化写入的地址不对或未触发PLC程序处理回读校验检查PLC侧程序有无地址映射只有先读一次才能写成功部分PLC从站状态机要求先建立会话连接后先读一个无关寄存器再执行写操作偶尔超时、偶发重连网络质量问题抓包看是否有TCP重传检查交换机端口双工模式频率高时丢数据轮询周期太短/报文太长加长间隔开启串行队列按块读取而非单寄存器读6.3 通信正常但UI显示卡顿很多C#窗体程序刚接PLC时一开轮询界面就卡。原因基本都是在UI线程里做了Socket读取或者每次读取后大量刷新控件。解决办法是用后台线程或async/await处理通信UI只负责Invoke更新。如果数据量很大比如几十个坐标点连续刷新建议把数据对象先缓存后批量刷新到界面而不是每个点单独更新一个控件。6.4 断开重连机制现场网线被踢掉、PLC断电重启是常态。我认为上位机必须做到通信异常后自动重连恢复后自动恢复采集不能要操作员去重启软件。写法套路是检测到异常超时、IOException→ 关闭旧连接 → 标记状态离线 → 定时尝试Connect → 成功后将状态置回在线 → 重新开始轮询。界面放一个“通信状态”指示灯绿/红切换重连期间不要狂弹MessageBox骚扰操作员。7. 最后一个实用技巧通信前先做寄存器表项目做了几年回头看最大的经验就一句话写代码前先和PLC工程师把寄存器表敲定比任何技术细节都重要。这份寄存器表至少要包含信号名、PLC地址、Modbus地址、数据类型16位无符号/32位浮点/字符串、读写属性、单位、换算系数。比如温度值PLC里存的是0.1°C为单位寄存器值是235那上位机显示就是23.5°C电机转速寄存器值1000表示100.0rpm。这类换算系数早定清楚联调能省一半时间。另外一个实操体会刚开始联调时先用最简单的“写固定值单寄存器”验证通路上每一个环节通不通比如写100到寄存器看PLC监控里有没有变成100。通路通了再上复杂的字符串、批量读写、浮点解析功能。不要一上来就跑全流程出问题时根本没法定位是哪一环的问题。C#与汇川PLC通信这件事本身并不难难的是把通信里的各种细节风险提前设计掉断线重连、字节序、地址映射、线程安全、回读校验。把这些考虑进去产线上跑几个月不碰它都是有可能的。希望这篇笔记能帮你在下一个项目里少走点弯路。本文还有配套的精品资源点击获取
返回列表