前言 在工业自动化领域,上位机与 PLC 的通信是核心环节之一。随着 C# 在工业软件开发中的广泛应用,如何实现 C# 与汇川 PLC 的高效稳定通信成为众多开发关注的问题。 本文将详细介绍两种主流实现方案:基于标准 Modbus TCP 协议的通信方式,以及使用汇川官方 API 的专用通信方式。 两种方案各有优劣,开发者可根据项目实际需求选择最适合的方案。 方案 | 实现方式 | 适用场景 | 使用标准 Modbus TCP 协议 | Modbus TCP 协议 | 快速开发,对稳定性要求高的项目 | 使用汇川官方 API | ModbusTcpAPI.dll;StandardModbusApi.dll | 深度控制通信过程的项目 |
一、方案一:使用标准 Modbus TCP 协议 Modbus TCP 是一种广泛应用的工业通信协议,具有跨品牌兼容性强的特点,适用于多品牌 PLC 混合使用的场景。 1.1 Modbus TCP 通讯帧格式说明 1.2 常用功能码详解 命令码 0x01/0x02:读线圈 请求帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x01/0x02 + 线圈起始地址 + 线圈数量 序号 | 数据 (字节) | 意义 | 字节数量 | 说明 | 1 | 事务元标识符 | MODBUS 请求/响应事务处理的识别码 | 2 个字节 | - | 2 | 协议标识符 | 0=MODBUS 协议 | 2 个字节 | - | 3 | 长度 | 以下字节的数量 | 2 个字节 | - | 4 | 单元标识符 | 主站请求标识符 | 1 个字节 | - | 5 | 0x01/0x02(命令码) | 读线圈 | 1 个字节 | - | 6 | 线圈起始地址 | 高位在前,低位在后 | 2 个字节 | 见线圈编址 | 7 | 线圈数量 | 高位在前,低位在后(N) | 2 个字节 | N 最大为 2000 |
响应帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x01/0x02 + 字节数 + 线圈状态 序号 | 数据 (字节) | 意义 | 字节数量 | 说明 | 1 | 事务元标识符 | MODBUS 请求/响应事务处理的识别码 | 2 个字节 | - | 2 | 协议标识符 | 0=MODBUS 协议 | 2 个字节 | - | 3 | 长度 | 以下字节的数量 | 2 个字节 | - | 4 | 单元标识符 | 复制主站请求标识符 | 1 个字节 | - | 5 | 0x01/0x02(命令码) | 读线圈 | 1 个字节 | - | 6 | 字节数 | 值:[(N+7)/8] | 1 个字节 | - | 7 | 线圈状态 | [(N+7)/8]个字节 | 可变 | 每 8 个线圈合为一个字节 |
命令码 0x03/0x04:读寄存器 请求帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x03/0x04 + 寄存器起始地址 + 寄存器数量 响应帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x03/0x04 + 字节数 + 寄存器值 其他常用命令码 命令码 | 功能 | 请求帧格式 | 响应帧格式 | 0x05 | 写单线圈 | 事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x05 + 线圈地址 + 线圈状态 | 同请求帧 | 0x06 | 写单个寄存器 | 事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x06 + 寄存器地址 + 寄存器值 | 同请求帧 | 0x0f | 写多个线圈 | 事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x0f + 线圈起始地址 + 线圈数量 + 字节数 + 线圈状态 | 事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x0f + 线圈起始地址 + 线圈数 | 0x10 | 写多个寄存器 | 从机地址 + 0x10 + 寄存器起始地址 + 寄存器数量 + 字节数 + 寄存器值 + CRC 检验 | 从机地址 + 0x10 + 线圈起始地址 + 线圈数量 + CRC 检验 |
报文示例:写多个寄存器 请求报文:2F 52 00 00 00 09 01 10 00 01 00 01 02 00 64 响应报文:2F 52 00 00 00 06 01 10 00 01 00 01 1.3 协议实现代码 internal classModbusProtocol : IDisposable{ private Socket _socket; privatereadonlyobject _lock = newobject(); privateushort _transactionId = 0; privatebool _disposed = false; // Pre-allocated buffers privatereadonlybyte[] _sendBuffer = newbyte[512]; privatereadonlybyte[] _recvBuffer = newbyte[4096]; public IPAddress IpAddress { get; privateset; } publicint Port { get; privateset; } publicint Timeout { get; set; } = 1000; publicbool IsConnected => _socket?.Connected ?? false; // Expected lengths for current operation privateushort _writeLen; privateushort _readLen; public ModbusProtocol(string ip, int port, int timeout = 1000) { if (!IPAddress.TryParse(ip, outvar address)) thrownew ArgumentException("Invalid IP address", nameof(ip)); IpAddress = address; Port = port; Timeout = timeout; } #region Connection public bool Connect() { lock (_lock) { try { if (_socket?.Connected == true) returntrue; _socket?.Dispose(); _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp) { ReceiveTimeout = Timeout, SendTimeout = Timeout, NoDelay = true }; var result = _socket.BeginConnect(new IPEndPoint(IpAddress, Port), null, null); bool success = result.AsyncWaitHandle.WaitOne(Timeout, true); if (success && _socket.Connected) { _socket.EndConnect(result); returntrue; } _socket.Close(); returnfalse; } catch { returnfalse; } } } public bool Disconnect() { lock (_lock) { try { if (_socket != null) { if (_socket.Connected) _socket.Shutdown(SocketShutdown.Both); _socket.Close(); _socket.Dispose(); _socket = null; } returntrue; } catch { returnfalse; } } } #endregion #region Modbus Functions /// <summary> /// Read Coils - Function 01 (mbtcpfcn01) /// </summary> public int ReadCoils(int address, int count, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildReadRequest(ModbusCmd.ModbusCmd_Read_Coil_01, address, count); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Read Discrete Inputs - Function 02 (mbtcpfcn02) /// </summary> public int ReadDiscreteInputs(int address, int count, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildReadRequest(ModbusCmd.ModbusCmd_Read_Coil_02, address, count); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Read Holding Registers - Function 03 (mbtcpfcn03) /// </summary> public int ReadHoldingRegisters(int address, int count, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildReadRequest(ModbusCmd.ModbusCmd_Read_Regs_03, address, count); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Read Input Registers - Function 04 (mbtcpfcn04) /// </summary> public int ReadInputRegisters(int address, int count, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildReadRequest(ModbusCmd.ModbusCmd_Read_Regs_04, address, count); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Write Single Coil - Function 05 (mbtcpfcn05) /// </summary> public int WriteSingleCoil(int address, int value, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildWriteSingleRequest(ModbusCmd.ModbusCmd_Write_Coil, address, value); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Write Single Register - Function 06 (mbtcpfcn06) /// </summary> public int WriteSingleRegister(int address, int value, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildWriteSingleRequest(ModbusCmd.ModbusCmd_Write_Regs, address, value); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Write Multiple Coils - Function 15 (mbtcpfcn15) /// </summary> public int WriteMultipleCoils(int address, int count, byte[] txdBuffer, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildWriteMultipleRequest(ModbusCmd.ModbusCmd_Write_MutlCoils, address, count, txdBuffer); return DataExchange(rxdBuffer, ref rxdLength); } } /// <summary> /// Write Multiple Registers - Function 16 (mbtcpfcn16) /// </summary> public int WriteMultipleRegisters(int address, int count, byte[] txdBuffer, byte[] rxdBuffer, ref int rxdLength) { lock (_lock) { if (!IsConnected) return0; BuildWriteMultipleRequest(ModbusCmd.ModbusCmd_Write_MutlRegs, address, count, txdBuffer); return DataExchange(rxdBuffer, ref rxdLength); } } #endregion #region Request Building private void BuildReadRequest(ModbusCmd functionCode, int address, int count) { ushort transId = _transactionId++; // MBAP Header _sendBuffer[0] = (byte)(transId >> 8); _sendBuffer[1] = (byte)(transId & 0xFF); _sendBuffer[2] = 0; // Protocol ID _sendBuffer[3] = 0; _sendBuffer[4] = 0; // Length (high) _sendBuffer[5] = 6; // Length (low) - Unit ID + FC + Addr + Count // PDU _sendBuffer[6] = 0xFF; _sendBuffer[7] = (byte)functionCode; _sendBuffer[8] = (byte)(address >> 8); _sendBuffer[9] = (byte)(address & 0xFF); _sendBuffer[10] = (byte)(count >> 8); _sendBuffer[11] = (byte)(count & 0xFF); _writeLen = 12; // Calculate expected response length if (functionCode == ModbusCmd.ModbusCmd_Read_Coil_01 || functionCode == ModbusCmd.ModbusCmd_Read_Coil_02) { // Coils: MBAP(6) + UnitID(1) + FC(1) + ByteCount(1) + Data((count+7)/8) _readLen = (ushort)(9 + ((count + 7) >> 3)); } else { // Registers: MBAP(6) + UnitID(1) + FC(1) + ByteCount(1) + Data(count*2) _readLen = (ushort)(9 + count * 2); } } private void BuildWriteSingleRequest(ModbusCmd functionCode, int address, int value) { ushort transId = _transactionId++; _sendBuffer[0] = (byte)(transId >> 8); _sendBuffer[1] = (byte)(transId & 0xFF); _sendBuffer[2] = 0; _sendBuffer[3] = 0; _sendBuffer[4] = 0; _sendBuffer[5] = 6; _sendBuffer[6] = 0xFF; _sendBuffer[7] = (byte)functionCode; _sendBuffer[8] = (byte)(address >> 8); _sendBuffer[9] = (byte)(address & 0xFF); if (functionCode == ModbusCmd.ModbusCmd_Write_Coil) { // Coil: 0xFF00 = ON, 0x0000 = OFF ushort coilValue = (value & 1) == 1 ? (ushort)0xFF00 : (ushort)0x0000; _sendBuffer[10] = (byte)(coilValue >> 8); _sendBuffer[11] = (byte)(coilValue & 0xFF); } else { // Register: big-endian _sendBuffer[10] = (byte)(value >> 8); _sendBuffer[11] = (byte)(value & 0xFF); } _writeLen = 12; _readLen = 12; // Echo response } private void BuildWriteMultipleRequest(ModbusCmd functionCode, int address, int count, byte[] txdBuffer) { ushort transId = _transactionId++; _sendBuffer[0] = (byte)(transId >> 8); _sendBuffer[1] = (byte)(transId & 0xFF); _sendBuffer[2] = 0; _sendBuffer[3] = 0; int offset = 6; _sendBuffer[offset++] = 0xFF; _sendBuffer[offset++] = (byte)functionCode; _sendBuffer[offset++] = (byte)(address >> 8); _sendBuffer[offset++] = (byte)(address & 0xFF); _sendBuffer[offset++] = (byte)(count >> 8); _sendBuffer[offset++] = (byte)(count & 0xFF); int byteCount; if (functionCode == ModbusCmd.ModbusCmd_Write_MutlCoils) { // Coils: byte count = (count + 7) / 8 byteCount = (count + 7) >> 3; _sendBuffer[offset++] = (byte)byteCount; // Copy packed bits directly for (int i = 0; i < byteCount; i++) { _sendBuffer[offset++] = txdBuffer; } } else// Write Multiple Registers { // Registers: byte count = count * 2 byteCount = count * 2; _sendBuffer[offset++] = (byte)byteCount; // Copy data with byte swap (little-endian to big-endian) //WriteFilterOfMutlWrite swap bytes for (int i = 0; i < count; i++) { int srcOffset = i * 2; // Swap bytes: host order (little-endian) to network order (big-endian) _sendBuffer[offset++] = txdBuffer[srcOffset + 1]; // High byte _sendBuffer[offset++] = txdBuffer[srcOffset]; // Low byte } } ushort pduLen = (ushort)(7 + byteCount); _sendBuffer[4] = (byte)(pduLen >> 8); _sendBuffer[5] = (byte)(pduLen & 0xFF); _writeLen = (ushort)(6 + pduLen); _readLen = 12; // Standard response for write multiple } #endregion #region Data Exchange /// <summary> /// Send request and receive response /// </summary> private int DataExchange(byte[] rxdBuffer, ref int rxdLength) { // Send with retry int iRet = -1; for (int i = 0; i < 2; i++) { try { iRet = _socket.Send(_sendBuffer, _writeLen, SocketFlags.None); if (iRet > 0) break; } catch { Disconnect(); Thread.Sleep(200); } } if (iRet <= 0) return0; // FALSE // Receive with retry int totalReceived = 0; for (int i = 0; i < 10 && totalReceived < _readLen; i++) { try { int received = _socket.Receive( _recvBuffer, totalReceived, _readLen - totalReceived, SocketFlags.None); if (received > 0) { totalReceived += received; } else { Thread.Sleep(1); } } catch { Thread.Sleep(1); } } // Additional receive loop if partial data if (totalReceived > 0 && totalReceived < _readLen) { int retryCount = 0; while (totalReceived < _readLen && retryCount < 3) { try { int received = _socket.Receive( _recvBuffer, totalReceived, _readLen - totalReceived, SocketFlags.None); if (received > 0) totalReceived += received; } catch { } Thread.Sleep(5); retryCount++; } } rxdLength = totalReceived; if (totalReceived <= 0) return0; // FALSE // Copy to output buffer Array.Copy(_recvBuffer, rxdBuffer, Math.Min(totalReceived, rxdBuffer.Length)); // Verify response if (totalReceived == _readLen && _sendBuffer[0] == _recvBuffer[0] && _sendBuffer[1] == _recvBuffer[1]) { return1; // TRUE - Success } return0; // FALSE } #endregion #region Response Parsing /// <summary> /// Parse Modbus response /// </summary> public static int ParseReadResponse(byte[] pData, out byte[] pReturnData, int nCount) { int nOffset = 7; // Skip MBAP header (6 bytes) + Unit ID (1 byte) byte byteMode = pData[nOffset]; // Function code nOffset++; byte byteLen = pData[nOffset]; // Byte count from response nOffset++; switch (byteMode) { case0x01: // Read Coils case0x02: // Read Discrete Inputs { // Output: 1 byte per coil (0 or 1) pReturnData = newbyte[nCount]; int nIndex = 0; for (int i = 0; i < nCount; i++) { byte byData = pData[nOffset]; pReturnData = (byte)((byData >> nIndex) & 0x01); nIndex++; if (nIndex >= 8) { nIndex = 0; nOffset++; } } } break; case0x03: // Read Holding Registers case0x04: // Read Input Registers { // Output: Raw bytes with byte order swapped pReturnData = newbyte[byteLen]; for (int i = 0; i < byteLen; i += 2) { // Swap bytes: network order (big-endian) to host order (little-endian) pReturnData = pData[nOffset + 1]; // Low byte pReturnData[i + 1] = pData[nOffset]; // High byte nOffset += 2; } } break; default: pReturnData = newbyte[byteLen]; Array.Copy(pData, nOffset, pReturnData, 0, byteLen); break; } return byteLen; } #endregion #region Utility Functions /// <summary> /// Convert byte array (each byte 0 or 1) to packed bits /// </summary> public static bool Byte2Bit(byte[] pSrcData, byte[] pDesData, int nCount) { int nBitIndex = 0; int desIndex = 0; pDesData[0] = 0; for (int i = 0; i < nCount; i++) { if (nBitIndex >= 8) { nBitIndex = 0; desIndex++; pDesData[desIndex] = 0; } if (pSrcData == 1) { pDesData[desIndex] |= (byte)(1 << nBitIndex); } nBitIndex++; } returntrue; } /// <summary> /// Convert octal to decimal /// </summary> public static int Oct2Int(int nOctData) { int nRtnData = 0; int nData = nOctData; int power = 0; while (nData > 0) { int digit = nData % 10; nRtnData += digit * (int)Math.Pow(8, power); nData /= 10; power++; } return nRtnData; } /// <summary> /// Check if number is valid octal - matches /// </summary> public static bool IsOct(int num) { if (num == 0) returntrue; while (num > 0) { int digit = num % 10; if (digit > 7) returnfalse; num /= 10; } returntrue; } #endregion #region IDisposable public void Dispose() { if (!_disposed) { Disconnect(); _disposed = true; } } #endregion} 提示:目前网上已有许多封装完善的 Modbus TCP 协议库,如 NModbus、HslCommunication 等,可直接引用使用。但了解底层通信原理对提升技术能力大有裨益。 二、方案二:汇川官方 API 2.1 准备工作 将以下两个动态链接库文件复制到创建的项目目录下: 资源下载:Modbus Api (2021-03-19).zip链接:https://pan.baidu.com/s/13zAyIXmJ5nOd_pLH87-I1Q提取码:ijkl 2.2 实现代码 汇川官方 API 提供了更直接的控制方式,支持访问 PLC 的特殊寄存器。 public classInovancePLC{ [DllImport("StandardModbusApi.dll")] public static extern bool Init_ETH_String(string ip, int netId = 0, int port = 502); [DllImport("StandardModbusApi.dll")] public static extern int H5u_Read_Soft_Elem(SoftElemType type, int startAddr, int count, byte[] buffer, int netId = 0); [DllImport("StandardModbusApi.dll")] public static extern int H5u_Write_Soft_Elem(SoftElemType type, int startAddr, int count, byte[] buffer, int netId = 0); publicenum SoftElemType { REGI_H5U_Y = 0x30, // 输出继电器 REGI_H5U_X = 0x31, // 输入继电器 REGI_H5U_M = 0x33, // 内部继电器 REGI_H5U_D = 0x35 // 数据寄存器 } public static void Main() { // 初始化连接 if (!Init_ETH_String("192.168.1.100")) { Console.WriteLine("PLC 连接失败"); return; } // 读取 D 寄存器示例 byte[] buffer = newbyte[4]; int result = H5u_Read_Soft_Elem(SoftElemType.REGI_H5U_D, 0, 2, buffer); if (result == 0) { ushortvalue = BitConverter.ToUInt16(buffer, 0); Console.WriteLine($"D0 值:{value}"); } // 写入 M 寄存器示例 ushort writeValue = 1; byte[] writeBuffer = BitConverter.GetBytes(writeValue); H5u_Write_Soft_Elem(SoftElemType.REGI_H5U_M, 0, 1, writeBuffer); }}三、方案对比分析 对比项 | Modbus TCP 方案 | 汇川 API 方案 | 兼容性 | 高(不限品牌) | 仅限汇川 PLC | 性能 | 一般 | 优 | 功能支持 | 标准功能 | 特殊寄存器访问 | 实现复杂度 | 低 | 中 |
总结 完成程序开发后,建议先使用 Modbus 调试工具进行通信测试,确保基础通信正常。推荐使用 Modbus Poll 或 Modbus Slave 等工具。 使用 Modbus Slave 软件时,建议开启 Log 记录功能(路径:Display → Communication Traffic → Log),这样发送和接收的报文会保存在 Log 文件中,便于后续排查问题和协议分析。 方案选择建议: 若项目需要兼容多品牌 PLC,优先选择 Modbus TCP 标准协议 若项目仅使用汇川 PLC 且需要深度控制,建议选择 汇川官方 API 对于快速原型开发,可直接使用成熟的第三方库(如 HslCommunication) 理解底层通信原理有助于在遇到问题时快速定位和解决,建议开发者在引用第三方库的同时,也要掌握基础协议的实现方式。 关键词 最后 如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长! 作者:DotNET探索求知 出处:mp.weixin.qq.com/s/TNXT-vTDqU7FJtrF8k85hQ 声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢! 方便大家交流、资源共享和共同成长 纯技术交流群,需要加入的小伙伴请扫码,并备注【加群】
推荐阅读 觉得有收获?不妨分享让更多人受益 关注「DotNet技术匠」,共同提升技术实力 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |