C# .NET 7.0中含结构体数组的PLC结构ADS封送问题
问题:C#通过ADS读取TwinCAT3复杂结构体数据异常
问题背景
使用C#通过ADS协议读取Beckhoff TwinCAT3 PLC中的结构体数据,简单结构读取正常,但处理包含固定大小数组的复杂结构时,封送后的值完全错误。核心问题在于ProtocolTable结构体无法正确读取固定大小为1001个元素的Values数组,PLC中结构体的pack_mod已设置为1。
初始结构体定义
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct TableDefinition { public short Unitpointer; public short Textpointer; public short dataType; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct Values { public float realValue; [MarshalAs(UnmanagedType.I1)] public bool boolValue; } [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ProtocolTable { public TableDefinition TableDefinition; [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1001)] public Values[] Values; }
初始读取数据代码
static void Main(string[] args) { AdsClient tcClient = new (); tcClient.Connect(851); int arraySize = 2; uint handle = tcClient.CreateVariableHandle("MAIN.ascProtocolTable"); ProtocolTable[] protocolTable = (ProtocolTable[])tcClient.ReadAny(handle, typeof(ProtocolTable[]), new int[] { arraySize }); tcClient.DeleteVariableHandle(handle); }
尝试过的错误方案
曾尝试使用指定SubType的SafeArray,但触发AccessViolationException,对应的结构体定义如下:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ProtocolTable { public TableDefinition TableDefinition; [MarshalAs(UnmanagedType.SafeArray, SafeArrayUserDefinedSubType = typeof(Values))] public Values[] Values; }
最终解决方案:自定义封送器
自行编写自定义封送器后,所有功能恢复正常,还能读取Values结构体中的字符串。以下是更新后的结构体及封送器代码:
更新后的结构体定义
/// <summary> /// 表示PLC数据中使用的值结构 /// </summary> public struct Values { public const int STRING_SIZE = 256; public float RealValue; public bool BoolValue; public string StringValue; /// <summary> /// 计算Values结构的字节大小 /// </summary> /// <returns>Values结构的字节大小</returns> public static int SizeOf() { return Marshal.SizeOf(typeof(float)) + Marshal.SizeOf(typeof(bool)) + STRING_SIZE; } } /// <summary> /// 表示PLC数据中的表定义结构 /// </summary> public struct TableDefinition { public short Unitpointer; public short Textpointer; public DataType DataType; public TableDefinition(short unitPointer, short textPointer, DataType dataType) { this.Unitpointer = unitPointer; this.Textpointer = textPointer; this.DataType = dataType; } } /// <summary> /// 表示PLC数据中的PSetTable结构 /// </summary> public struct PSetTable { public TableDefinition TableDefinition; public Values PSetValues; /// <summary> /// 计算PSetTable结构的字节大小 /// </summary> /// <returns>PSetTable结构的字节大小</returns> public static int SizeOf() { return Marshal.SizeOf(typeof(TableDefinition)) + Values.SizeOf(); } } /// <summary> /// 表示PLC数据中的ProtocolTable结构 /// </summary> public struct ProtocolTable { public TableDefinition TableDefinition; public Values[] Values; /// <summary> /// 计算ProtocolTable结构的字节大小 /// </summary> /// <param name="valuesCount">表中值的数量</param> /// <returns>ProtocolTable结构的字节大小</returns> public static int SizeOf(int valuesCount) { return Marshal.SizeOf(typeof(TableDefinition)) + (PlcStructs.Values.SizeOf() * valuesCount); } }
自定义封送器代码
/// <summary> /// 提供将二进制数据封送为结构化类型的方法 /// </summary> internal class Marshaller { private const int STRING_SIZE = 256; /// <summary> /// 将二进制数据封送为结构化类型数组 /// </summary> /// <typeparam name="T">要封送的结构化数据类型</typeparam> /// <param name="bytes">要封送的二进制数据</param> /// <param name="readStruct">从BinaryReader读取单个结构化类型的函数</param> /// <param name="structSizeProvider">提供单个结构化类型字节大小的函数</param> /// <returns>从二进制数据封送得到的结构化类型数组</returns> public static T[] MarshalStructs<T>(byte[] bytes, Func<BinaryReader, T> readStruct, Func<int> structSizeProvider) { int structSize = structSizeProvider(); int arrayLength = bytes.Length / structSize; T[] result = new T[arrayLength]; using (MemoryStream stream = new(bytes)) using (BinaryReader reader = new(stream)) { for (int i = 0; i < arrayLength; i++) { result[i] = readStruct(reader); } } return result; } /// <summary> /// 将二进制数据封送为PSetTable结构化类型 /// </summary> /// <param name="reader">读取二进制数据的BinaryReader</param> /// <returns>封送后的PSetTable结构化类型</returns> public static PlcStructs.PSetTable MarshalPSetTable(BinaryReader reader) { PlcStructs.PSetTable pSetTable = new() { TableDefinition = MarshalTableDefinition(reader) }; pSetTable.PSetValues.RealValue = reader.ReadSingle(); pSetTable.PSetValues.BoolValue = reader.ReadBoolean(); byte[] stringBytes = reader.ReadBytes(STRING_SIZE); pSetTable.PSetValues.StringValue = ExtractString(stringBytes); return pSetTable; } /// <summary> /// 将二进制数据封送为ProtocolTable结构化类型 /// </summary> /// <param name="reader">读取二进制数据的BinaryReader</param> /// <param name="valuesCount">ProtocolTable中的值数量</param> /// <returns>封送后的ProtocolTable结构化类型</returns> public static PlcStructs.ProtocolTable MarshalProtocolTable(BinaryReader reader, int valuesCount) { PlcStructs.ProtocolTable protocolTable = new() { TableDefinition = MarshalTableDefinition(reader), Values = new PlcStructs.Values[valuesCount] }; for (int j = 0; j < valuesCount; j++) { protocolTable.Values[j] = new PlcStructs.Values { RealValue = reader.ReadSingle(), BoolValue = reader.ReadBoolean() }; byte[] stringBytes = reader.ReadBytes(STRING_SIZE); protocolTable.Values[j].StringValue = ExtractString(stringBytes); } return protocolTable; } /// <summary> /// 从提供的BinaryReader中封送TableDefinition /// </summary> /// <param name="reader">包含要封送字节的BinaryReader</param> /// <returns>封送后的TableDefinition</returns> private static PlcStructs.TableDefinition MarshalTableDefinition(BinaryReader reader) { // 从读取器中读取表定义字节,并在内存中固定字节数组 // 确保在需要IntPtr的操作期间其地址保持稳定 byte[] tableDefinitionBytes = reader.ReadBytes(Marshal.SizeOf(typeof(PlcStructs.TableDefinition))); GCHandle handle = GCHandle.Alloc(tableDefinitionBytes, GCHandleType.Pinned); // 获取固定对象的地址以获取IntPtr供后续使用 IntPtr pinnedObjectPtr = handle.AddrOfPinnedObject(); PlcStructs.TableDefinition tableDefinition; if (pinnedObjectPtr != IntPtr.Zero) { tableDefinition = (PlcStructs.TableDefinition)Marshal.PtrToStructure(pinnedObjectPtr, typeof(PlcStructs.TableDefinition))!; } else { throw new Exception("无法为TableDefinition分配内存。"); } handle.Free(); return tableDefinition; } /// <summary> /// 从字节数组中提取字符串 /// </summary> /// <param name="bytes">包含字符串的字节数组</param> /// <returns>提取的字符串</returns> private static string ExtractString(byte[] bytes) { #pragma warning disable SYSLIB0001 // 在字节数组中查找第一个空(0x00)字节的位置 int nullIndex = Array.IndexOf(bytes, (byte)0); if (nullIndex >= 0) { byte[] relevantBytes = new byte[nullIndex]; Array.Copy(bytes, relevantBytes, nullIndex); string stringValue = Encoding.UTF7.GetString(relevantBytes); return stringValue; } else { string stringValue = Encoding.UTF7.GetString(bytes); return stringValue; } #pragma warning restore SYSLIB0001 } }
内容的提问来源于stack exchange,提问作者Crizzly
相关产品推荐
相关产品推荐

