news 2026/8/19 23:30:03

Unity3d实时读取Modbus RTU数据

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unity3d实时读取Modbus RTU数据
1.首先要了解Modbus RTU是什么?

Modbus是一种通信协议,用于在电子设备之间传输数据,最早由Modicon(现为施耐德电气的一部分)在1979年提出。它非常简单、开放,并且广泛用于工业自动化系统。

RTU是 Modbus 的一种传输模式,全称是Remote Terminal Unit(远程终端单元)模式

  • 它使用二进制形式(而非 ASCII 字符)传输数据。

  • 数据包紧凑,传输效率高。

  • 常用于串口通信(RS-232、RS-485)。

所以,Modbus RTU就是 Modbus 协议的一种实现方式,通常用于工业设备之间通过串口进行高效通信。

大白话就是:工业版的普通话,各个工业设备想要交流要使用的一种语言。

2.Modbus RTU 功能码

一个标准的 Modbus RTU 数据帧包括以下部分:

部分长度说明
地址(Slave Address)1 字节指定从设备编号(1~247)
功能码(Function Code)1 字节表示操作类型(读/写寄存器/线圈)
数据(Data)N 字节要读写的寄存器地址、数量及实际值
CRC 校验(CRC Check)2 字节错误检测,保证通信可靠性

例如:01 03 02 01 00 01 D5 CA

  • 01:设备地址 1

  • 03:功能码 3(读取保持寄存器)

  • 02 01:寄存器起始地址

  • 00 01:读取数量

  • D5 CA:CRC 校验

3.通信特点

主从模式:一个主机(Master)控制一个或多个从设备(Slave)。

半双工通信(RS-485):只能一端发送,另一端接收。

效率高:RTU 数据压缩,不像 ASCII 模式每个字节用两倍空间传输。

距离远:在 RS-485 上,可靠传输可达 1200 米左右。

说白了就是你需要主动发送数据请求,它才会回应数据,它不会主动回应给你数据的,比如你说现在温度是多少(发送::01 03 02 01 00 01 D4 72),它回答现在23℃(读取到数据::01 03 0201 F0B9 90即:(0x01F0,496)
它不会自己跑出来告诉你“我现在是 25.3℃”,除非你问它。

如果你需要实时获取温度或者距离,你就要不停的发送,不停的读取。

一般频率是0.02s-2s看具体手册和个人需求。

4.Unity3d获取数据:

Player Settings -> Api Compatibility Level -> .NET Standard 2.0 或 .NET Framework
启用 Allow 'unsafe' Code(部分串口库需要)
仅支持 Windows Standalone(Android/iOS 需原生插件,WebGL 不支持)

完整脚本如下:根据自己的文档适当调整发送数据

using UnityEngine; using UnityEngine.UI; using System.IO.Ports; using System; using System.Text; using System.Threading; public class TestModbusRTU : MonoBehaviour { // ========== INSPECTOR 配置区(关键参数全暴露)========== [Header("🔌 串口配置")] [Tooltip("设备管理器中查看的COM端口")] public string portName = "COM3"; [Tooltip("波特率:9600/19200/38400/115200")] public int baudRate = 9600; [Tooltip("校验位:与设备说明书严格一致!")] public Parity parity = Parity.None; [Tooltip("停止位")] public StopBits stopBits = StopBits.One; [Tooltip("读取超时(毫秒),建议≥2000")] public int readTimeout = 3000; [Tooltip("RS485设备必开:控制收发方向")] public bool enableRTS = false; [Tooltip("部分设备需开启")] public bool enableDTR = true; [Header("🤖 Modbus 参数")] [Tooltip("从站地址(十进制)")] public byte slaveAddress = 1; [Tooltip("寄存器地址(十进制),示例:513=0x0201")] public ushort registerAddress = 513; [Tooltip("固定读1个寄存器(返回2字节)")] public ushort registerCount = 1; [Header("🖥️ UI 绑定")] public Text statusText; // 状态提示 public Text valueText; // 数据显示 public Text debugLogText; // 诊断日志(可选) public Button readButton; // 读取按钮 // ========== 内部状态 ========== private SerialPort serialPort; private bool isReading = false; private StringBuilder logBuffer = new StringBuilder(); private const int RESPONSE_LENGTH = 7; // 1+1+1+2+2 // ========== UNITY LIFECYCLE ========== void Start() { InitializeUI(); InitializeSerialPort(); if (readButton != null) readButton.onClick.AddListener(OnReadButtonClicked); else Debug.LogWarning("⚠️ ReadButton 未绑定,需手动调用 StartRead()"); } void OnDestroy() { CloseSerialPort(); } // ========== 初始化 ========== void InitializeUI() { if (statusText != null) statusText.text = "⏳ 初始化中..."; if (valueText != null) valueText.text = "N/A"; if (debugLogText != null) debugLogText.text = "诊断日志:\n"; AppendLog($"[SYS] 脚本启动 | 目标设备: {slaveAddress} | 寄存器: {registerAddress}"); } void InitializeSerialPort() { try { // 关闭旧端口(避免重复打开) if (serialPort != null && serialPort.IsOpen) CloseSerialPort(); serialPort = new SerialPort( portName, baudRate, parity, 8, // 数据位固定为8(Modbus RTU标准) stopBits ) { ReadTimeout = readTimeout, WriteTimeout = 2000, DtrEnable = enableDTR, RtsEnable = enableRTS, Handshake = Handshake.None }; serialPort.Open(); Thread.Sleep(100); // 等待驱动稳定 // 清空缓冲区 if (serialPort.IsOpen) { serialPort.DiscardInBuffer(); serialPort.DiscardOutBuffer(); } string configStr = $"波特率:{baudRate} | {parity} | 8数据位 | {stopBits} | DTR:{enableDTR} | RTS:{enableRTS}"; UpdateStatus($"✅ 串口 {portName} 已打开\n{configStr}", Color.green); AppendLog($"[串口] {portName} 打开成功 | {configStr}"); AppendLog($"[诊断] 初始缓冲区: In={serialPort.BytesToRead} Out={serialPort.BytesToWrite}"); } catch (Exception e) { string errorMsg = $"❌ 串口打开失败: {e.Message}\n请检查:\n1. 端口是否被占用\n2. 驱动是否安装\n3. 以管理员身份运行Unity"; UpdateStatus(errorMsg, Color.red); AppendLog($"[错误] {errorMsg}"); Debug.LogError($"[串口初始化] {e}"); } } void CloseSerialPort() { if (serialPort != null) { if (serialPort.IsOpen) serialPort.Close(); serialPort.Dispose(); serialPort = null; AppendLog("[串口] 已安全关闭"); } } // ========== 外部触发 ========== public void OnReadButtonClicked() { if (isReading) { AppendLog("[警告] 读取中,请稍后再试"); return; } if (serialPort == null || !serialPort.IsOpen) { UpdateStatus("⚠️ 串口未打开!点击重试或检查配置", Color.yellow); InitializeSerialPort(); // 尝试重连 return; } StartCoroutine(ReadModbusData()); } // ========== 核心通信流程(含超时修复)========== System.Collections.IEnumerator ReadModbusData() { isReading = true; UpdateStatus("📡 构建请求帧...", Color.cyan); AppendLog($"\n[请求] 地址:{slaveAddress} | 寄存器:{registerAddress} | 计数:{registerCount}"); // 1. 清空缓冲区 + 帧间隔等待(Modbus RTU关键!) serialPort.DiscardInBuffer(); serialPort.DiscardOutBuffer(); yield return new WaitForSecondsRealtime(0.05f); // ≥3.5字符时间(9600波特率≈4ms) // 2. 构建请求帧 byte[] requestFrame = BuildRequestFrame(); if (requestFrame == null) { UpdateStatus("❌ 帧构建失败", Color.red); isReading = false; yield break; } LogFrame("📤 发送帧", requestFrame); // 3. 发送数据 try { serialPort.Write(requestFrame, 0, requestFrame.Length); AppendLog($"[发送] 已写入 {requestFrame.Length} 字节"); } catch (Exception e) { UpdateStatus($"❌ 发送失败: {e.Message}", Color.red); AppendLog($"[错误] 发送异常: {e.Message}"); isReading = false; yield break; } // 4. ⚠️ 关键:等待设备响应时间(RTU要求) yield return new WaitForSecondsRealtime(0.15f); // 根据波特率调整:9600≈150ms // 5. 读取响应 byte[] response = new byte[RESPONSE_LENGTH]; int totalRead = 0; DateTime startTime = DateTime.Now; AppendLog($"[接收] 开始读取 (超时:{readTimeout}ms)"); try { while (totalRead < RESPONSE_LENGTH && (DateTime.Now - startTime).TotalMilliseconds < readTimeout) { if (serialPort.BytesToRead > 0) { int bytesRead = serialPort.Read(response, totalRead, RESPONSE_LENGTH - totalRead); totalRead += bytesRead; AppendLog($"[接收] 累计读取 {totalRead}/{RESPONSE_LENGTH} 字节"); } //yield return null; // 释放主线程 } if (totalRead != RESPONSE_LENGTH) { // 尝试读取残余数据用于诊断 int remaining = serialPort.BytesToRead; if (remaining > 0) { byte[] partial = new byte[remaining]; serialPort.Read(partial, 0, remaining); LogFrame($"⚠️ 响应不完整! 仅收到 {totalRead}+{remaining} 字节", partial); } throw new TimeoutException($"期望{RESPONSE_LENGTH}字节,实际{totalRead}"); } LogFrame("📥 完整响应", response); } catch (TimeoutException te) { string msg = $"⏱️ 读取超时! (收到 {totalRead}/{RESPONSE_LENGTH} 字节)"; UpdateStatus(msg, Color.red); AppendLog($"[超时] {te.Message} | 缓冲区剩余: {serialPort.BytesToRead} 字节"); AppendLog("[诊断建议]\n1. 检查设备地址/波特率/校验位\n2. 用Modbus Poll测试硬件\n3. RS485确认A/B线接反?"); isReading = false; yield break; } catch (Exception e) { UpdateStatus($"❌ 读取异常: {e.Message}", Color.red); AppendLog($"[异常] {e.GetType().Name}: {e.Message}"); isReading = false; yield break; } // 6. 验证响应 if (!ValidateResponse(response)) { isReading = false; yield break; } // 7. 解析数据(大端序) ushort value = (ushort)((response[3] << 8) | response[4]); valueText.text = $"{value} (0x{value:X4})"; UpdateStatus($"✅ 读取成功 | 值: {value}", Color.green); AppendLog($"[解析] 寄存器[{registerAddress}] = {value} (十进制) | 0x{value:X4} (十六进制)"); isReading = false; } // ========== 帧构建与验证 ========== byte[] BuildRequestFrame() { try { byte[] baseFrame = new byte[] { slaveAddress, 0x03, // 功能码:读保持寄存器 (byte)(registerAddress >> 8), (byte)(registerAddress & 0xFF), (byte)(registerCount >> 8), (byte)(registerCount & 0xFF) }; ushort crc = CalcCRC16(baseFrame, baseFrame.Length); byte[] fullFrame = new byte[baseFrame.Length + 2]; Buffer.BlockCopy(baseFrame, 0, fullFrame, 0, baseFrame.Length); fullFrame[baseFrame.Length] = (byte)(crc & 0xFF); // 低字节 fullFrame[baseFrame.Length + 1] = (byte)(crc >> 8); // 高字节 return fullFrame; } catch (Exception e) { AppendLog($"[帧构建] 错误: {e.Message}"); return null; } } bool ValidateResponse(byte[] resp) { // 检查长度 if (resp.Length < RESPONSE_LENGTH) { AppendLog($"[验证] 响应长度错误: {resp.Length} < {RESPONSE_LENGTH}"); return false; } // 检查从站地址 if (resp[0] != slaveAddress) { AppendLog($"[验证] 从站地址不匹配! 期望:{slaveAddress:X2}, 实际:{resp[0]:X2}"); return false; } // 检查功能码(异常响应:0x83) if (resp[1] == 0x83) { AppendLog($"[验证] 设备返回异常! 错误码: 0x{resp[2]:X2} (参考Modbus异常码)"); return false; } if (resp[1] != 0x03) { AppendLog($"[验证] 功能码错误! 期望:03, 实际:{resp[1]:X2}"); return false; } // 检查字节数 if (resp[2] != registerCount * 2) { AppendLog($"[验证] 数据长度错误! 期望:{registerCount * 2}, 实际:{resp[2]}"); return false; } // CRC校验 if (!ValidateCRC(resp, RESPONSE_LENGTH)) { AppendLog("[验证] CRC校验失败!"); return false; } AppendLog("[验证] 响应帧校验通过 ✓"); return true; } // ========== CRC16 (Modbus标准) ========== ushort CalcCRC16(byte[] data, int length) { ushort crc = 0xFFFF; for (int i = 0; i < length; i++) { crc ^= data[i]; for (int j = 0; j < 8; j++) { if ((crc & 1) == 1) crc = (ushort)((crc >> 1) ^ 0xA001); else crc >>= 1; } } return crc; } bool ValidateCRC(byte[] data, int length) { if (length < 2) return false; ushort calc = CalcCRC16(data, length - 2); ushort recv = (ushort)((data[length - 1] << 8) | data[length - 2]); bool valid = calc == recv; if (!valid) AppendLog($"[CRC] 计算:0x{calc:X4} | 接收:0x{recv:X4}"); return valid; } // ========== UI 与日志辅助 ========== void UpdateStatus(string message, Color color) { if (statusText != null) { statusText.text = message; // 如需动态改色,需确保Text组件支持(使用Outline或自定义Shader) } } void LogFrame(string prefix, byte[] data) { if (data == null || data.Length == 0) return; string hex = BitConverter.ToString(data).Replace("-", " "); string log = $"{prefix}: {hex}"; AppendLog(log); Debug.Log($"[Modbus] {log}"); } void AppendLog(string message) { logBuffer.AppendLine(message); if (debugLogText != null) { // 限制日志长度(防内存溢出) if (logBuffer.Length > 2000) logBuffer.Remove(0, logBuffer.Length - 2000); debugLogText.text = logBuffer.ToString(); } } }
5.这是本人使用的距离传感器获取数据经验,有需要的可以随时交流讨论

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 23:29:53

计算有功功率和无功功率

微电网&#xff0c;下垂控制&#xff0c;两并联虚拟阻抗下垂控制&#xff0c;实现了有功功率共享&#xff0c;效果好&#xff0c;有参考文献。微电网技术近年来发展迅速&#xff0c;成为分布式能源系统的重要组成部分。然而&#xff0c;微电网中的多个发电单元如何协调运行&…

作者头像 李华
网站建设 2026/7/14 16:25:48

Hil-SERL真机复现思路及框架

项目介绍项目采用UC伯克利罗剑岚博士发表的HIL-SERL及SERL框架尝试通过“仿真经验 真实经验联合回放”的方式提升学习效率。项目地址&#xff1a;SERL&#xff08;项目&#xff09;&#xff1a;https://serl-robot.github.io/SERL&#xff08;代码&#xff09;&#xff1a;htt…

作者头像 李华
网站建设 2026/7/14 16:25:52

制造业如传统车企如何数字化转型实践?

曾今有幸做过宁波某车企二级供应商的数字化转型整体解决方案&#xff08;包含企业精益生产管理以及数字化系统WMS\MES\APS三个系统的集成&#xff09;我想我应该很有发言权&#xff01; 是2022年接手项目&#xff0c;一开始面临质量追溯严苛、信息孤岛、效率瓶颈等行业共性挑战…

作者头像 李华
网站建设 2026/7/14 16:25:51

Agent如何自我进化?Autoresearch用“最小可控”给出答案

曾几何时,前沿AI研究是由血肉计算机(指人脑)在吃饭、睡觉、娱乐之余,偶尔通过声波互联在“小组会议”的仪式中同步完成的。那个时代早已远去。如今,研究完全由自主AI代理集群主导,它们运行于天空中的计算集群巨型结构之上。这些代理声称,代码库现已演进至第10,205代;无…

作者头像 李华
网站建设 2026/7/14 16:25:49

[算法][力扣3]无重复字符的最长子串

给定一个字符串 s &#xff0c;请你找出其中不含有重复字符的 最长 子串 的长度。示例 1:输入: s "abcabcbb"输出: 3 解释: 因为无重复字符的最长子串是 "abc"&#xff0c;所以其长度为 3。注意 "bca" 和 "cab" 也是正确答案。示例 2…

作者头像 李华
网站建设 2026/7/14 16:25:51

矿用本安型全景雷达物位扫描仪的应用

唐山大方汇中仪表研发的GUL系列矿用本安型全景雷达物位扫描仪&#xff08;3D雷达物位扫描仪&#xff09;为非接触计量级测量仪表&#xff0c;具有数字信号、图像输出功能。3D雷达物位扫描仪基于雷达信号时间行程原理对多个测量点进行测距&#xff0c;其内部的扫描机构可对料仓内…

作者头像 李华