
📌 前言
在工业数字孪生、虚拟调试和HMI开发中,Unity与PLC的通信是不可或缺的一环。本文将手把手教你如何在Unity中实现与汇川Easy 521系列PLC的Modbus TCP通信,涵盖X/Y/M/D区域的完整读写操作。
🎯 为什么选择Unity做上位机?
-
3D可视化:构建逼真的数字孪生场景
-
跨平台部署:Windows/Linux/Android/iOS全支持
-
实时交互:60fps的流畅交互体验
-
开发效率:丰富的资源商店和成熟的开发工具
🛠️ 第一步:环境配置
1. 下载Modbus库
测试工具和NModbus4.dll下载链接:
链接:https://pan.baidu.com/s/1kLPesjtCJQOlT9Fq7qDEYA
提取码:rmug
2. DLL导入Unity
将 NModbus4.dll 拖放到Unity项目的以下路径:

如果不存在Plugins文件夹,请手动创建。
3. 解决编译错误(可能会遇到的报错!)
导入NModbus4后可能会遇到SerialPort相关的编译错误,这是因为NModbus4虽然包含了串口通信代码,但是Unity默认使用.NET Standard 2.1,不支持System.IO.Ports命名空间。
解决方法:
-
打开 Unity →
Edit→Project Settings→Player -
找到
Configuration→Api Compatibility Level -
从
.NET Standard 2.1改为.NET Framework -
等待Unity重新编译,错误立即消失!


📊 第二步:理解汇川EASY 521的地址映射
这是整个通信方案的核心!汇川EASY 521系列PLC的Modbus TCP地址映射有特定规律:
| PLC元件 | Modbus类型 | 起始地址(Hex) | 起始地址(Dec) | 功能码 | 地址格式 |
|---|---|---|---|---|---|
| Y | 线圈 | 0xFC00 | 64512 | 01/05/15 | ⚠️ 八进制 |
| X | 线圈 | 0xF800 | 63488 | 01/05/15 | ⚠️ 八进制 |
| M | 线圈 | 0x0000 | 0 | 01/05/15 | 十进制 |
| D | 保持寄存器 | 0x0000 | 0 | 03/06/16 | 十进制 |
⚠️ 八进制地址说明(非常重要!)
X和Y元件使用八进制地址,这意味着:
-
Y0→ Modbus地址 64512 -
Y1→ Modbus地址 64513 -
Y7→ Modbus地址 64519 -
Y10→ Modbus地址 64520(八进制10 = 十进制8!) -
Y17→ Modbus地址 64527(八进制17 = 十进制15!)
💻 第三步:完整代码实现
cs
using System;
using System.Net.Sockets;
using UnityEngine;
/// <summary>
/// 汇川EASY521系列PLC - Modbus TCP通信脚本
/// 支持X/Y/M区(线圈)读写,D区(保持寄存器)读写
/// </summary>
public class Inovance521_Modbus : MonoBehaviour
{
// PLC连接参数
public string ipAddress = "192.168.1.100"; // PLC的IP地址
public int port = 502; // Modbus TCP默认端口
public byte slaveId = 1; // 从站ID,通常为1
private TcpClient tcpClient;
private NetworkStream stream;
private bool isConnected = false;
// 事务标识符,每次请求累加
private ushort transactionId = 1;
/// <summary>
/// 连接到PLC
/// </summary>
public bool Connect()
{
try
{
tcpClient = new TcpClient();
tcpClient.Connect(ipAddress, port);
stream = tcpClient.GetStream();
isConnected = true;
Debug.Log($"[Modbus] 成功连接到PLC {ipAddress}:{port}");
return true;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 连接失败: {e.Message}");
return false;
}
}
/// <summary>
/// 断开连接
/// </summary>
public void Disconnect()
{
if (stream != null) stream.Close();
if (tcpClient != null) tcpClient.Close();
isConnected = false;
Debug.Log("[Modbus] 已断开连接");
}
// ==================== 线圈读写 (X / Y / M 区域) ====================
// 说明:汇川EASY系列PLC的X/Y/M均映射到Modbus线圈区(功能码01/05/15)
// 地址转换规则:
// - X区:八进制地址,X0 = 63488(0xF800), X1 = 63489, ... X7 = 63495, X10 = 63496(八进制10=十进制8)
// - Y区:八进制地址,Y0 = 64512(0xFC00), Y1 = 64513, ... Y7 = 64519, Y10 = 64520
// - M区:十进制地址,M0 = 0, M100 = 100, M7999 = 7999
// 线圈功能码常量
private const byte FC_READ_COILS = 0x01; // 读线圈
private const byte FC_WRITE_SINGLE_COIL = 0x05; // 写单线圈
private const byte FC_WRITE_MULTI_COILS = 0x0F; // 写多线圈
/// <summary>
/// X区地址转换(八进制 -> 十进制Modbus地址)
/// 例如:X0=0, X1=1, X7=7, X10=8 (八进制10 = 十进制8)
/// Modbus基地址 = 0xF800 (63488)
/// </summary>
private ushort GetXAddress(int octalAddress)
{
int decimalValue = Convert.ToInt32(octalAddress.ToString(), 8);
return (ushort)(0xF800 + decimalValue);
}
/// <summary>
/// Y区地址转换(八进制 -> 十进制Modbus地址)
/// Modbus基地址 = 0xFC00 (64512)
/// </summary>
private ushort GetYAddress(int octalAddress)
{
int decimalValue = Convert.ToInt32(octalAddress.ToString(), 8);
return (ushort)(0xFC00 + decimalValue);
}
/// <summary>
/// M区地址转换(十进制,直接返回)
/// Modbus基地址 = 0x0000 (0)
/// </summary>
private ushort GetMAddress(int decimalAddress)
{
return (ushort)decimalAddress;
}
/// <summary>
/// 写入单个线圈(X/Y/M通用)
/// </summary>
/// <param name="coilAddress">Modbus线圈地址</param>
/// <param name="value">true=ON(0xFF00), false=OFF(0x0000)</param>
private bool WriteSingleCoil(ushort coilAddress, bool value)
{
if (!isConnected) return false;
try
{
// MBAP报文头(7字节) + 功能码(1) + 地址(2) + 数据(2) = 12字节
byte[] frame = new byte[12];
// MBAP头
frame[0] = (byte)(transactionId >> 8); // 事务标识符高字节
frame[1] = (byte)(transactionId & 0xFF); // 事务标识符低字节
frame[2] = 0x00; // 协议标识符(Modbus=0)
frame[3] = 0x00;
frame[4] = 0x00; // 后续字节长度高字节
frame[5] = 0x06; // 后续字节长度低字节(功能码+地址+数据=6)
frame[6] = slaveId; // 单元标识符(从站ID)
// PDU数据
frame[7] = FC_WRITE_SINGLE_COIL; // 功能码0x05
frame[8] = (byte)(coilAddress >> 8); // 线圈地址高字节
frame[9] = (byte)(coilAddress & 0xFF); // 线圈地址低字节
frame[10] = (byte)(value ? 0xFF : 0x00); // ON=0xFF00, OFF=0x0000
frame[11] = 0x00;
stream.Write(frame, 0, frame.Length);
// 读取响应
byte[] response = new byte[12];
int bytesRead = stream.Read(response, 0, response.Length);
transactionId++;
return bytesRead == 12 && response[7] == FC_WRITE_SINGLE_COIL;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 写线圈失败: {e.Message}");
return false;
}
}
/// <summary>
/// 写入Y线圈(八进制地址)
/// 示例: WriteY(0, true) => Y0 = true
/// </summary>
public bool WriteY(int octalAddress, bool value)
{
ushort addr = GetYAddress(octalAddress);
Debug.Log($"[Modbus] 写入 Y{octalAddress} (Modbus地址:{addr}) = {value}");
return WriteSingleCoil(addr, value);
}
/// <summary>
/// 写入M线圈(十进制地址)
/// 示例: WriteM(100, true) => M100 = true
/// </summary>
public bool WriteM(int decimalAddress, bool value)
{
ushort addr = GetMAddress(decimalAddress);
Debug.Log($"[Modbus] 写入 M{decimalAddress} (Modbus地址:{addr}) = {value}");
return WriteSingleCoil(addr, value);
}
/// <summary>
/// 写入X线圈(八进制地址)- 注意:X通常是输入点,实际写入可能无效
/// </summary>
public bool WriteX(int octalAddress, bool value)
{
ushort addr = GetXAddress(octalAddress);
Debug.Log($"[Modbus] 写入 X{octalAddress} (Modbus地址:{addr}) = {value}");
return WriteSingleCoil(addr, value);
}
/// <summary>
/// 读取单个线圈状态
/// </summary>
public bool ReadCoil(ushort coilAddress, out bool value)
{
value = false;
if (!isConnected) return false;
try
{
byte[] frame = new byte[12];
frame[0] = (byte)(transactionId >> 8);
frame[1] = (byte)(transactionId & 0xFF);
frame[2] = 0x00; frame[3] = 0x00;
frame[4] = 0x00; frame[5] = 0x06;
frame[6] = slaveId;
frame[7] = FC_READ_COILS; // 功能码0x01
frame[8] = (byte)(coilAddress >> 8);
frame[9] = (byte)(coilAddress & 0xFF);
frame[10] = 0x00; // 读取数量高字节
frame[11] = 0x01; // 读取数量低字节(1个)
stream.Write(frame, 0, frame.Length);
byte[] response = new byte[11]; // 响应: MBAP(7) + 功能码(1) + 字节数(1) + 数据(1) + 可能填充
int bytesRead = stream.Read(response, 0, response.Length);
transactionId++;
if (bytesRead >= 10 && response[7] == FC_READ_COILS)
{
value = (response[9] & 0x01) != 0;
return true;
}
return false;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 读线圈失败: {e.Message}");
return false;
}
}
/// <summary>
/// 读取Y线圈
/// </summary>
public bool ReadY(int octalAddress, out bool value)
{
return ReadCoil(GetYAddress(octalAddress), out value);
}
/// <summary>
/// 读取M线圈
/// </summary>
public bool ReadM(int decimalAddress, out bool value)
{
return ReadCoil(GetMAddress(decimalAddress), out value);
}
/// <summary>
/// 读取X线圈
/// </summary>
public bool ReadX(int octalAddress, out bool value)
{
return ReadCoil(GetXAddress(octalAddress), out value);
}
// ==================== 保持寄存器读写 (D 区域) ====================
// 说明:D区映射到保持寄存器区(功能码03/06/16)
// 地址转换:D0 = 0, D100 = 100, D7999 = 7999
// 寄存器功能码常量
private const byte FC_READ_HOLDING_REGISTERS = 0x03; // 读保持寄存器
private const byte FC_WRITE_SINGLE_REGISTER = 0x06; // 写单寄存器
private const byte FC_WRITE_MULTI_REGISTERS = 0x10; // 写多寄存器
/// <summary>
/// D区地址转换(十进制,直接返回)
/// </summary>
private ushort GetDAddress(int decimalAddress)
{
return (ushort)decimalAddress;
}
/// <summary>
/// 写入单个D寄存器(16位整数)
/// 示例: WriteD(100, 2) => D100 = 2
/// </summary>
public bool WriteD(int decimalAddress, ushort value)
{
if (!isConnected) return false;
try
{
ushort regAddress = GetDAddress(decimalAddress);
byte[] frame = new byte[12];
frame[0] = (byte)(transactionId >> 8);
frame[1] = (byte)(transactionId & 0xFF);
frame[2] = 0x00; frame[3] = 0x00;
frame[4] = 0x00; frame[5] = 0x06;
frame[6] = slaveId;
frame[7] = FC_WRITE_SINGLE_REGISTER; // 功能码0x06
frame[8] = (byte)(regAddress >> 8);
frame[9] = (byte)(regAddress & 0xFF);
frame[10] = (byte)(value >> 8); // 寄存器值高字节
frame[11] = (byte)(value & 0xFF); // 寄存器值低字节
stream.Write(frame, 0, frame.Length);
byte[] response = new byte[12];
int bytesRead = stream.Read(response, 0, response.Length);
transactionId++;
Debug.Log($"[Modbus] 写入 D{decimalAddress} = {value}");
return bytesRead == 12 && response[7] == FC_WRITE_SINGLE_REGISTER;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 写D寄存器失败: {e.Message}");
return false;
}
}
/// <summary>
/// 写入D寄存器(32位整数,占用2个寄存器)
/// </summary>
public bool WriteD32(int decimalAddress, int value)
{
if (!isConnected) return false;
try
{
ushort regAddress = GetDAddress(decimalAddress);
byte[] bytes = BitConverter.GetBytes(value);
// 注意:Modbus标准是高字节在前,需要根据PLC实际配置调整
ushort reg1 = (ushort)((bytes[1] << 8) | bytes[0]); // 低16位
ushort reg2 = (ushort)((bytes[3] << 8) | bytes[2]); // 高16位
byte[] frame = new byte[17];
frame[0] = (byte)(transactionId >> 8);
frame[1] = (byte)(transactionId & 0xFF);
frame[2] = 0x00; frame[3] = 0x00;
frame[4] = 0x00; frame[5] = 0x0B; // 后续11字节
frame[6] = slaveId;
frame[7] = FC_WRITE_MULTI_REGISTERS; // 功能码0x10
frame[8] = (byte)(regAddress >> 8);
frame[9] = (byte)(regAddress & 0xFF);
frame[10] = 0x00; // 寄存器数量高字节
frame[11] = 0x02; // 寄存器数量低字节(2个)
frame[12] = 0x04; // 字节数(2个寄存器=4字节)
frame[13] = (byte)(reg1 >> 8);
frame[14] = (byte)(reg1 & 0xFF);
frame[15] = (byte)(reg2 >> 8);
frame[16] = (byte)(reg2 & 0xFF);
stream.Write(frame, 0, frame.Length);
byte[] response = new byte[12];
int bytesRead = stream.Read(response, 0, response.Length);
transactionId++;
return bytesRead == 12 && response[7] == FC_WRITE_MULTI_REGISTERS;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 写D32寄存器失败: {e.Message}");
return false;
}
}
/// <summary>
/// 读取单个D寄存器
/// </summary>
public bool ReadD(int decimalAddress, out ushort value)
{
value = 0;
if (!isConnected) return false;
try
{
ushort regAddress = GetDAddress(decimalAddress);
byte[] frame = new byte[12];
frame[0] = (byte)(transactionId >> 8);
frame[1] = (byte)(transactionId & 0xFF);
frame[2] = 0x00; frame[3] = 0x00;
frame[4] = 0x00; frame[5] = 0x06;
frame[6] = slaveId;
frame[7] = FC_READ_HOLDING_REGISTERS; // 功能码0x03
frame[8] = (byte)(regAddress >> 8);
frame[9] = (byte)(regAddress & 0xFF);
frame[10] = 0x00; // 寄存器数量高字节
frame[11] = 0x01; // 寄存器数量低字节(1个)
stream.Write(frame, 0, frame.Length);
byte[] response = new byte[11];
int bytesRead = stream.Read(response, 0, response.Length);
transactionId++;
if (bytesRead >= 10 && response[7] == FC_READ_HOLDING_REGISTERS)
{
value = (ushort)((response[9] << 8) | response[10]);
return true;
}
return false;
}
catch (Exception e)
{
Debug.LogError($"[Modbus] 读D寄存器失败: {e.Message}");
return false;
}
}
// ==================== 示例 ====================
void Start()
{
// 连接PLC
if (Connect())
{
// 写入示例
WriteY(0, true); // Y0 = true
WriteM(100, true); // M100 = true
WriteD(100, 2); // D100 = 2
}
}
void OnDestroy()
{
Disconnect();
}
// 可选的自动重连检查
void Update()
{
if (!isConnected)
{
// 可在此实现自动重连逻辑
}
}
}
🎮 第四步:在Unity中使用
1. 创建通信脚本
-
在Unity场景中创建一个空物体,命名为
PLC_Controller -
将上述脚本挂载到该物体上
-
在Inspector中配置PLC的IP地址(如
192.168.1.88)
如下为autoshop界面:

📝 总结
本文详细介绍了如何在Unity中使用Modbus TCP协议与汇川Easy 521 PLC通信,核心要点总结如下:
| 要点 | 说明 |
|---|---|
| API兼容性 | Unity需使用.NET Framework而非.NET Standard |
| 八进制地址 | X/Y地址是八进制,需用Convert.ToInt32("10", 8)转换 |
| 地址映射 | X:0xF800, Y:0xFC00, M:0x0000, D:0x0000 |
| 字节序 | 16位用大端序,32位需根据PLC配置调整 |
| 性能 | 使用批量读写,避免频繁单次操作 |
通过这套方案,你可以轻松构建工业数字孪生系统、虚拟调试平台或HMI监控应用。如有问题,欢迎在评论区交流!