通过 Beckhoff ADS 读写 PLC 结构体时,上位机 C# 侧需要定义一个完全匹配的结构体。如果两边的内存布局(字段顺序、大小、对齐方式)有任何差异,读出来的数据就会错位——值不是 0 就是乱码,而且不会报任何异常。这是 ADS 通信中最难调试的 BUG 类型。
TwinCAT 3 默认使用 Pack Mode = 1(字节对齐),即结构体字段紧密排列,中间不插入 padding。这与 C# 默认的 StructLayout(LayoutKind.Sequential)(Pack = 8)不同。
// PLC 侧结构体 TYPE ST_Recipe : STRUCT sName : STRING(31); // 32 字节(含 \0 终止符) fTemp : LREAL; // 8 字节 nSpeed : INT; // 2 字节 bEnabled : BOOL; // 1 字节 END_STRUCT // 总计 = 32+8+2+1 = 43 字节 END_TYPE
// C# 侧 — 必须 Pack = 1 + 正确处理 STRING [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct ST_Recipe { // STRING(31) → 固定 32 字节的 byte 数组(不是 C# string!) [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public byte[] sName; public double fTemp; // LREAL → double (8 bytes) public short nSpeed; // INT → short (2 bytes) public bool bEnabled; // BOOL → bool (1 byte) // 辅助属性:将 byte[] 转为可读 string public string Name => Encoding.Default.GetString(sName) .TrimEnd('\0'); }
⚠️ 三个必须遵守的规则:
① Pack = 1 必须显式指定,否则 C# 默认 Pack=8 会插入 padding;
② STRING(n) 映射为 byte[n+1],不能用 C# 的 string 类型;
③ 字段顺序必须与 PLC 侧完全一致,不能调换。
TwinCAT 3 中 ARRAY[0..7] OF BOOL 实际占用 8 字节(每个 BOOL 占 1 字节)。C# 中 bool 也是 1 字节,但如果 BOOL 数组后面紧跟 LREAL,PLC 侧不会插 padding(因为 Pack=1),C# 侧也不能有 padding。
嵌套的 STRUCT 必须在 C# 侧也定义为独立结构体并标注 Pack = 1,然后在父结构体中以值类型引用。不能用 class 替代 struct。
TwinCAT 3 的 WSTRING 使用 UTF-16 LE 编码,每个字符占 2 字节。WSTRING(31) 占 64 字节(32 × 2)。C# 侧应映射为 byte[64],然后用 Encoding.Unicode.GetString() 解码。
// 在上位机启动时校验结构体大小 int csSize = Marshal.SizeOf<ST_Recipe>(); int plcSize = 43; // 从 PLC 侧 SIZEOF(ST_Recipe) 获取 if (csSize != plcSize) throw new InvalidOperationException( $"结构体大小不匹配:C#={csSize}, PLC={plcSize}");
完整映射工具类含自动校验、STRING/WSTRING 转换辅助方法,已上传至 github.com/tc3-engineer。