ADS · C# · 避坑

PLC 结构体映射到 C# 的内存对齐陷阱:Pack=1 · STRING 特殊处理完整指南

StructLayout Pack=1 ADS 通信 C# 2026-02-15 · 尚工

一、为什么结构体映射会出 BUG

通过 Beckhoff ADS 读写 PLC 结构体时,上位机 C# 侧需要定义一个完全匹配的结构体。如果两边的内存布局(字段顺序、大小、对齐方式)有任何差异,读出来的数据就会错位——值不是 0 就是乱码,而且不会报任何异常。这是 ADS 通信中最难调试的 BUG 类型。

二、TwinCAT 3 的默认对齐规则

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# 侧的正确映射

// 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 侧完全一致,不能调换。

四、常见踩坑场景

1. BOOL 数组的 padding

TwinCAT 3 中 ARRAY[0..7] OF BOOL 实际占用 8 字节(每个 BOOL 占 1 字节)。C# 中 bool 也是 1 字节,但如果 BOOL 数组后面紧跟 LREAL,PLC 侧不会插 padding(因为 Pack=1),C# 侧也不能有 padding。

2. 嵌套结构体

嵌套的 STRUCT 必须在 C# 侧也定义为独立结构体并标注 Pack = 1,然后在父结构体中以值类型引用。不能用 class 替代 struct

3. WSTRING 与 UTF-16

TwinCAT 3 的 WSTRING 使用 UTF-16 LE 编码,每个字符占 2 字节。WSTRING(31) 占 64 字节(32 × 2)。C# 侧应映射为 byte[64],然后用 Encoding.Unicode.GetString() 解码。

五、验证工具:Marshal.SizeOf

// 在上位机启动时校验结构体大小
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