You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 20:00:55