
简介面向工业自动化领域的C#开发人员这份资源围绕倍福PLC的TMC文件自动生成C#类解决PLC结构体数据与应用程序之间的快速映射与读写问题尤其适合需要在上位机软件中集成PLC数据交互的工程师。资源包共19个文件、约2.1MB包含10个DLL依赖库、2个EXE运行程序以及配套的配置文件、XML数据文件和PDB调试文件为直接运行、二次开发和排错提供完整环境。目前已有226人学习下载。包内可执行程序可独立运行同时集成了ADS通信、MySQL数据访问、压缩解压等常用库省去手动解析TMC文件与搭建依赖的麻烦还覆盖了结构体映射、类文件/DLL生成及与PLC通信等关键环节从解析到生成再到通信形成了一套闭环的工程实践方案。借助这套工具开发者能快速将TMC结构转换为C#对象结合ADS库实现变量实时读写与状态监控适合在倍福控制系统中构建上位机应用有效提升工业自动化项目的开发效率。 搞上位机的兄弟应该都遇到过这个场景倍福PLC的程序改了一版结构体里加了两个变量结果我这边C#写的对应类就得跟着手动改改完还得核对类型、偏移量一个不小心字符串或者数组的定义就对不上运行时读出来的数据全是乱的。后来我把目光放到了PLC编译后自动生成的tmc文件上研究了一段时间发现这东西完全可以把C#类“自动生成”出来省掉重复劳动。这篇文章就把我自己的做法和踩过的坑整理一下给同样被这件事折磨的人一个参考。1. 为什么要从tmc文件生成C#类1.1 上位机与倍福PLC通信的基础倍福PLC与上位机交互最常用的方式就是ADS通信。上位机作为ADS客户端通过TcAdsClient去连接PLC的Runtime端口然后按变量名读写数据。这个过程里你既要告诉ADS“我要读哪个变量”又要告诉ADS“这个变量是什么数据类型、占几个字节”后面这件事说白了就是靠你代码里的数据结构去和PLC里的实际布局一一对应。一旦PLC侧有一个结构体上位机这边通常也要定义一个一模一样的class或者struct。问题是PLC程序是TwinCAT XAE开发的里面定义结构体、数组、字符串长度都很随意而上位机这边你要手动维护一份C#定义两边一不一致全靠肉眼核对多了少了全靠自觉。项目规模小还好说规模一大改一次PLC变量上位机代码就要伤筋动骨一次。1.2 tmc文件到底是个什么东西tmc文件全称我一般叫它TwinCAT Measurement Configuration实际上它是TwinCAT 3在编译PLC项目时自动生成的一个XML格式描述文件。只要你激活了配置在PLC项目目录下就会生成对应的tmc文件。打开这个文件你会发现里面结构化地记录了PLC程序里的全部类型定义结构体的名字、包含哪些成员、每个成员是什么类型、数组有几个维度、字符串多长、每个符号的偏移地址是多少全部都在。这份文件本质上是PLC侧的“类型元数据”相当于把PLC程序的类型系统完整导出了一份。只要我们能把这份XML解析清楚然后在C#侧动态生成对应的类两边就能始终保持一致。1.3 手写类的痛点与自动生成的价值手写类最尴尬的时刻是PLC工程师突然丢过来一个嵌套了三层的结构体里面有数组、有字符串、还有枚举。你照着定义一个个敲C#字段敲完还要担心int和INT的区别、bool的对齐字节数来回调试半天。等你终于搞定PLC又改版了嵌套结构体里加了一个字段前面的偏移全变了你又要再核对一遍。生成类的价值就在于它让“PLC类型定义”成为唯一事实来源上位机代码只是它的翻译结果。你不需要苦思冥想这个字段应该映射成C#的什么类型机器会按规则自动处理。哪怕PLC侧每周改一次你重新生成一次代码编译几秒钟就完事手写根本不具备这种效率。2. 生成前的准备理解tmc文件结构2.1 tmc文件里的关键节点要解析tmc首先要能看明白里面的XML结构。下面是一个典型的tmc文件中的类型定义片段TcSmClass DataType NameST_PumpData/Name TypeGuid{8C3E5D0A-8A8B-4C7A-8BF1-7D2E2A3C4B5D}/TypeGuid SubItems SubItem Namech1Run/Name TypeBOOL/Type BitSize8/BitSize BitOffs0/BitOffs /SubItem SubItem NametargetTemp/Name TypeREAL/Type BitSize32/BitSize BitOffs8/BitOffs /SubItem SubItem Namehistory/Name TypeARRAY [0..9] OF REAL/Type BitSize320/BitSize BitOffs64/BitOffs /SubItem /SubItems /DataType /TcSmClass重点关注几个节点DataType定义了一个数据类型Name就是类型名SubItems下面挂的是这个类型的所有成员每一个SubItem都有Name、Type、BitSize、BitOffs。BitOffs是位偏移除以8就是字节偏移这个值在调试很有用。还有一点需要注意tmc文件的根节点不一定叫TcSmClass有时候是TcSmProject不同TwinCAT版本略有差异。不过DataType这个节点是通用存在的解析的时候按XDocument.Descendants(DataType)去找最稳妥。2.2 数据类型映射关系梳理tmc里的PLC类型和我们C#里的类型不完全一致需要建立一张映射表。下面是我在项目中用的一套映射规则PLC类型C#类型字节数备注BOOLbool1TC3里BOOL占1字节BYTEbyte1WORDushort2DWORDuint4SINTsbyte1USINTbyte1INTshort2UINTushort2DINTint4UDINTuint4REALfloat4LREALdouble8STRINGstring / byte[]按声明长度见后文WSTRINGstring按声明长度枚举enum 或 int按基础类型可生成enum结构体class递归生成ARRAY [a..b] OF TT[]长度 b-a1特别注意一点TwinCAT 3里REAL是4字节的floatLREAL是8字节的double好多从C转过来的人会在这里翻车把REAL当成了double。3. 生成C#类的几种方案对比3.1 方案一纯文本手工转换这个不用多说了就是把tmc里的结构体照着抄成C#类。优点是没有学习成本缺点是没有未来。只要PLC改动一次你就要手工同步一次项目里有几十个结构体的话这个工作量非常酸爽。3.2 方案二TwinCAT自带工具或第三方插件TwinCAT本身提供TcXaeShell的自动化接口可以通过Automation Interface读取PLC类型信息也有一些人写的T4模板插件可以干这事。这种方式好处是集成度高能在Visual Studio里直接一键生成但它也挑环境依赖TwinCAT安装、依赖VS版本换一台新电脑就可能跑不起来。3.3 方案三自己写XML解析生成器我个人最推荐自己写一个控制台小工具输入tmc文件路径输出一个cs文件。原因很实在不依赖TwinCAT环境逻辑完全可控哪个类型看不懂就加一条解析规则而且可以随时调整模板比如添加[JsonProperty]特性、添加序列化标签定制性极强。而且这个工具本质上就是一个“解析XML 拼接字符串”的程序非常适合放在上位机项目里作为构建步骤的一部分PLC程序更新后自动重新生成。4. 手把手实操用C#解析tmc并生成类文件4.1 环境准备和工程搭建我推荐创建一个.NET 6/8的控制台项目引用System.Xml.Linq不需要任何第三方包。这个工具可以和你的上位机项目放在同一个Solution里也可以单独放看你习惯。工具只做一件事输入tmc路径输出cs文件路径然后执行。核心思路是这样的先加载整个tmc到XDocument遍历所有DataType节点对每个节点生成一个C#类遇到嵌套结构体就递归处理。最后把所有类拼接成一个.cs文件输出。4.2 核心解析逻辑与关键代码下面是我这个工具里最核心的一段代码实现了对数据结构体的递归解析public static void ParseDataType(XElement dataTypeNode, StringBuilder sb, HashSetstring processedTypes) { string typeName dataTypeNode.Element(Name)?.Value ?? Unknown; if (!processedTypes.Add(typeName)) return; // 防止重复生成 sb.AppendLine($ public class {typeName}); sb.AppendLine( {); var subItems dataTypeNode.Descendants(SubItem); foreach (var item in subItems) { string fieldName item.Element(Name)?.Value ?? ; string fieldType item.Element(Type)?.Value ?? ; if (fieldName || fieldType ) continue; string csharpType MapType(fieldType, dataTypeNode, sb, processedTypes); // 数组类型处理 if (fieldType.StartsWith(ARRAY)) { int length ParseArrayLength(fieldType); csharpType ${csharpType}[]; sb.AppendLine($ public {csharpType} {fieldName} new {csharpType}[{length}];); } else if (csharpType string) { sb.AppendLine($ public string {fieldName} string.Empty;); } else { sb.AppendLine($ public {csharpType} {fieldName} {{ get; set; }}); } } sb.AppendLine( }); sb.AppendLine(); }对应的MapType函数就是前面提到的那张映射表的代码实现。注意嵌套结构体的处理如果fieldType不是基础类型而是tmc文件里另一个DataType的名字那么在生成当前类的字段时要递归去生成那个类型对应的类再把它作为字段类型使用。4.3 生成结果演示和使用方式工具跑完之后生成的C#类大致长这样public class ST_PumpData { public bool ch1Run { get; set; } public float targetTemp { get; set; } public float[] history new float[10]; } public class MAIN_DeviceStatus { public short errorCode { get; set; } public ST_PumpData pump { get; set; } new ST_PumpData(); }使用的时候你可以在上位机里通过ADS的CreateVariableHandle拿到变量句柄然后用ReadAny配合生成好的类型进行读取TcAdsClient client new TcAdsClient(); client.Connect(192.168.0.2.1.1, 851); int handle client.CreateVariableHandle(MAIN.deviceStatus); MAIN_DeviceStatus status client.ReadAny(handle, typeof(MAIN_DeviceStatus));这样读出来的数据字段布局完全由PLC侧的tmc决定只要工具解析正确C#这边几乎不会出现错位问题。5. 生成的类怎么用起来倍福PLC与电脑连接及ADS通信5.1 倍福PLC与电脑连接的常见方式有人可能会卡在第一步类生成出来了但电脑连不上PLC路由不通。所以这里必须把倍福PLC与电脑连接的方法梳理一遍最常见的是下面这几种本机直连一台工控机装了TwinCATPLC Runtime就跑在这台机器上上位机也在同一台机器直接使用TcAdsClient连接NetId通常就是127.0.0.1.1.1。网线直连用一根网线把电脑和嵌入式控制器比如CX系列连起来给电脑的有线网卡设置一个和控制器同一网段的IP然后在TwinCAT路由表里添加控制器。局域网连接通过交换机把多台设备和PLC连到同一局域网每台设备一个固定IP电脑上配置好静态路由即可。配置路由的方式很简单打开TwinCAT XAE在“TwinCAT”菜单里选择“Routes”点“Add”填入控制器的IP地址和AMS NetId系统会自动搜索并建立连接。之后上位机就可以通过ADS来通信了。5.2 最小可用的ADS通信示例路由配好之后上位机这边用起来就比较清爽了。下面这段代码演示了连接、读取结构体、写入变量的完整流程using TwinCAT.Ads; class Program { static void Main() { using var client new TcAdsClient(); // 连接远程PLCNetId和端口 client.Connect(192.168.0.2.1.1, 851); // 写入单个变量 int handle client.CreateVariableHandle(MAIN.setpoint); client.WriteAny(handle, 128.5f); client.DeleteVariableHandle(handle); // 读取结构体 int dataHandle client.CreateVariableHandle(MAIN.deviceStatus); var status client.ReadAny(dataHandle, typeof(MAIN_DeviceStatus)); client.DeleteVariableHandle(dataHandle); } }注意Connect的第一个参数是PLC的AMS NetId而不仅仅是IP地址NetId一般是IP地址末尾.1.1的格式所以192.168.0.2这台控制器的NetId大概率是192.168.0.2.1.1。第二个参数851是PLC Runtime的ADS端口号一般固定用851。如果连的是本机则连接127.0.0.1.1.1内容上差距不大。6. 常见问题与排查技巧6.1 类型映射对不上读出来数据全是乱的这是最常见的问题。之前有个朋友一直说生成的类跑不通我让他把tmc里的Type值发过来一看里面是T_ULINT无符号64位长整型我的映射表里没有默认转成了long。PLC侧是8字节无符号C#侧是8字节有符号数值一大就乱码。解决办法是解析时加一个MapType的兜底逻辑遇到未知类型弹一个警告日志并且默认映射为byte[]长度可以先用BitSize / 8去凑。这样至少不会错位后续再针对具体类型完善映射表。6.2 数组长度解析容易出错tmc里的数组定义格式是ARRAY [0..9] OF REAL注意下标不一定从0开始有时候PLC程序里写的是ARRAY [1..10]。生成C#数组时长度是10-1110不是10也不是11这里必须把起始值和结束值都解析出来再计算。我在这个坑上翻过车生成出来的数组长度比实际少了一个元素读结构体的时候错位错得莫名其妙。6.3 字符串的处理要格外小心TwinCAT里的STRING默认长度是80个字符但定义时可以写STRING(20)来指定长度。tmc文件里字符串类型有时候会写成STRING(20)有时候直接写成STRING解析时要统一处理对声明了长度的字符串C#侧生成的属性直接是string没问题但如果用Marshal做结构体指针转换建议还是用byte[]配合手动Encoding.ASCII.GetString去转。千万不要直接用string去做ReadAny的结构体映射C#的string是引用类型它不会按值类型那样直接放在结构体布局里会直接导致StructLayout错乱。我自己的做法是生成类时对STRING类型的字段统一生成byte[]长度按BitSize/8然后用的时候再自己转字符串。6.4 嵌套结构体或循环引用导致递归死循环PLC程序里偶尔会出现结构体互相引用的情况比如A里面有一个BB里面又有一个A这种在PLC侧是合法的通过引用实现但直接递归生成C#类会无限循环。解决方案是用一个HashSet记录已经生成的类型名如果递归到已经生成过的类型就不再继续展开直接使用类型名作为字段类型。就是代码里processedTypes.Add(typeName)那一步的作用。6.5 路由不通Connect报错如果你连接的时候报Client.Connect failed之类的错误优先检查三个事第一个PLC的网口IP和电脑是否在同一个网段第二个TwinCAT路由表里有没有正确配置AMS NetIdNetId的最后两位一般是.1.1第三个Windows防火墙是不是把ADS报文拦了建议把ADS相关进程或者TwinCAT的TcSystemService加入防火墙放行名单。6.6 枚举类型生成tmc文件里枚举类型也能解析出来它的DataType下面会有EnumInfo节点里面列了枚举值的名字和数值。这个我建议直接生成C#的enum不要在生成类的时候把它降级成int因为上位机要做状态判断时枚举名比魔法数字友好太多。查找枚举值的时候注意PLC枚举的底层类型常见的是BYTE、WORD、DINT生成enum时要指定对应的基础类型。7. 踩坑记录与个人经验在我自己把这个工具用起来的项目里最爽的一次是PLC工程师在一个大结构体里加了十几个变量我这边重新跑了一遍生成器整个上位机代码没有任何手改编译一次通过。从那以后我就坚定了一个习惯凡是和PLC结构体相关的DTO类一律自动生成严禁手写。但也不是说生成完就万事大吉我建议你保留生成工具的源码并把它纳入到版本管理里。以后不管还是单独维护TC3版本升级、tmc文件格式出现细微变化的时候都能快速调整解析规则。生成的.cs文件也可以提交到代码库方便其他同事直接引用。最后分享一个小技巧生成类的时候顺手把每个字段在PLC侧的BitOffs和BitSize以注释的形式写到生成代码里比如// BitOffs64, BitSize320。一旦将来上位机读出来的数据和PLC侧实际值对不上打开生成文件看一下偏移量就能立刻判断是结构体错位还是值本身就异常排查效率能提升一大截。这个习惯我保留到现在安利给每个做倍福上位机开发的朋友。本文还有配套的精品资源点击获取