1. 为什么说处理带数组的结构体数组是个“坎儿”?
如果你正在用C#和倍福的TwinCAT PLC打交道,肯定绕不开ADS通信。这东西好用是好用,但一旦数据复杂起来,比如PLC那边定义了一个结构体数组,而且结构体里面还嵌套着数组,很多朋友就开始头疼了。我刚开始做项目对接时,也在这里栽过跟头,明明变量名、类型都对,可读上来的数据要么是乱码,要么直接抛异常。
问题的核心在于“翻译”。C#运行在.NET的托管环境中,内存管理、数据类型都是“高级”的、安全的。而PLC那边的TwinCAT,底层是C/C++的世界,内存布局、数据对齐都是“原始”的、确定的。ADS通信就像在两个说不同语言的国家之间传递包裹,C#和PLC就是这两个国家。普通的整型、浮点型好比是标准件,大家约定俗成,传递起来没问题。但“结构体数组”尤其是“带数组成员的结构体数组”,就好比是一个内部结构复杂的定制包裹,如果打包(序列化)和解包(反序列化)的规则没对齐,送到对方手里肯定就散架了。
具体到我们的场景,难点集中在三处:第一,如何让C#准确地理解PLC结构体中那个数组的大小和类型?第二,当这种结构体本身又组成一个数组时,如何一次性高效地读写整个缓存区,而不是傻傻地循环读写?第三,如何规避因内存对齐(Pack)不一致导致的“错位”问题?这篇文章,我就结合自己踩过的坑和实战经验,带你一步步跨过这个坎儿,实现高效、稳定的数据交互。
2. 基石:在C#中正确“复刻”PLC的结构体
和PLC通信,第一步不是写连接代码,而是在C#里把PLC那边的数据结构“一模一样”地声明出来。这里差之毫厘,后续就会谬以千里。
2.1 理解并应用 StructLayout 和 MarshalAs
C#默认对类和结构的内存布局有优化,这不利于我们进行精确的二进制拷贝。所以,我们必须用 StructLayout 特性来告诉它:“别动,就按我写的顺序来排。”
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public class Feedbacks
{
// 成员定义...
}
LayoutKind.Sequential:这是关键,确保字段在内存中的顺序和代码中声明的顺序严格一致。Pack = 1:这指定了内存对齐的字节边界。Pack=1意味着按1字节对齐,也就是紧密排列,不留空隙。为什么常用1?因为很多PLC(包括倍福的TwinCAT)在默认或特定配置下,结构体就是紧密打包的。如果这里不统一,比如PLC按4字节对齐,C#按8字节对齐,一个bool后面可能就会多出3个字节的“空洞”,导致后续所有字段的偏移量全部错位。实测下来,对于大多数TwinCAT结构体,Pack=1是最稳妥的选择。
接下来是重头戏:处理结构体里的数组成员。在C#里,一个int[]引用本身只是一个指针。但我们需要的是把整个数组的内容都作为结构体的一部分连续地放在内存里。这时就要用到 MarshalAs(UnmanagedType.ByValArray)。
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4, ArraySubType = UnmanagedType.I1)]
public bool[] filter = new bool[4];
UnmanagedType.ByValArray:表示这是一个“内联”数组,数组元素直接存放在结构体的内存空间中,而不是一个单独的引用。SizeConst:必须指定! 这个值必须和PLC结构体里定义的数组大小完全一致。比如PLC里是ARRAY[1..4] OF BOOL,这里SizeConst就是4。这是通信双方对数组长度的“硬约定”。ArraySubType:指定数组中每个元素在非托管内存中的具体类型。这是最容易出错的地方,因为PLC的数据类型和C#/.NET、C语言类型并不总是一一对应。BOOL在TwinCAT中通常占1个字节(8位),对应UnmanagedType.I1(有符号8位整数)。虽然我们用它存true/false,但底层是按字节传输的。INT在TwinCAT中通常是16位(2字节),对应UnmanagedType.I2(有符号16位整数),也就是C#的short。如果你在PLC里用了DINT(32位),那这里就要对应UnmanagedType.I4(int)。REAL在TwinCAT中通常是32位单精度浮点数,对应UnmanagedType.R4,即C#的float。如果是LREAL,则对应UnmanagedType.R8(double)。
2.2 一个完整的结构体定义示例
假设PLC端定义了如下结构体,用于反馈一组传感器的
80



被折叠的 条评论
为什么被折叠?



