汇川PLC-Unity3d与汇川easy521plc进行Modbustcp通讯

📌 前言

在工业数字孪生、虚拟调试和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命名空间。

解决方法

  1. 打开 Unity → EditProject SettingsPlayer

  2. 找到 ConfigurationApi Compatibility Level

  3. .NET Standard 2.1 改为 .NET Framework

  4. 等待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. 创建通信脚本

  1. 在Unity场景中创建一个空物体,命名为PLC_Controller

  2. 将上述脚本挂载到该物体上

  3. 在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监控应用。如有问题,欢迎在评论区交流!

相关推荐
small-pudding2 小时前
Unity URP + Compute Shader 路径追踪器实战:从可用到可优化
unity·游戏引擎
weixin_423995002 小时前
unity 物体转向鼠标点击方向2d和3d
unity·计算机外设·游戏引擎
游乐码3 小时前
C#List
开发语言·c#·list
mxwin3 小时前
Unity URP 下 Shader 变体 (Variants):multi_compile 与 shader_feature的关键字管理及变体爆炸防控策略
unity·游戏引擎
RReality4 小时前
【Unity Shader URP】全息扫描线(Hologram Scanline)源码+脚本控制
ui·unity·游戏引擎·图形渲染
Paine Zeng5 小时前
C# + SolidWorks 二次开发 -监听退出草图事件并自动执行逻辑
c#·solidworks二次开发·solidworks api
游乐码5 小时前
C#Dicitionary
算法·c#
SunnyDays10115 小时前
C# 实战:如何高效地将 HTML 转换为可编辑 Word 文档
c#·html转word
渔民小镇5 小时前
一次编写到处对接 —— 为 Godot/Unity/React 生成统一交互接口
java·分布式·游戏·unity·godot