ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

倍福TMC文件如何自动生成C#类,告别手动维护轴变量

倍福TMC文件如何自动生成C#类,告别手动维护轴变量 简介这份资源为倍福PLC开发人员提供了一整套TMC文件转C#类的自动化工具与运行环境解决了在C#应用程序中直接读写PLC结构体数据时需手工编写映射代码的痛点显著降低上位机与控制器之间的对接成本。开发者在.NET环境下运行AutoCode工具即可将TMC文件中的结构体定义、变量声明自动解析批量生成对应的C#类源码及可复用的DLL动态库再配合倍福官方的TwinCAT.Ads库快速实现与PLC的双向数据通信做到一次生成、多项目复用。资源包共19个文件、压缩后约2.1MB包含AutoCode主程序、TwinCAT.Ads与MySql.Data等核心依赖DLL、运行配置文件及示例应用程序清单结构精简可直接部署使用。目前已有226人下载学习适合正在用C#开发倍福上位机、需要提高PLC数据交互开发效率的自动化工程师参考。 做上位机的朋友应该都有过这种经历项目里几十个轴手动在C#代码里写结构体、定义变量名、维护地址偏移改一次PLC工程就要对着文档吐血核对一遍。倍福PLC的TMC文件就是为了解决这个痛点而存在的——它把轴的配置、参数、输入输出变量都结构化地保存在XML里我们完全可以把这个文件变成C#类让代码自动生成省去重复劳动。这篇文章我就从TMC文件的结构讲起带你走一遍从XML到C#类的完整流程包括生成器的核心实现以及生成之后如何接入ADS通信真正跑起来。适合刚接触倍福上位机开发或者已经被轴配置反复折磨的C#工程师参考。1. 为什么要把TMC文件变成C#类——先看透你手头的痛点1.1 手动维护轴变量的日子真的过不下去了先描述一个典型场景TwinCAT工程里有8个伺服轴每个轴需要读取实际位置、写入目标位置、读写控制字和状态字还可能有速度、加速度、跟随误差、扭矩等几十个变量。按传统方式你得在C#里做两件事在代码里定义一堆字符串常量比如Axis1.NcToPlc.ActPos、Axis1.PlcToNc.SetPos然后通过ADS库的CreateVariableHandle去绑定。一旦PLC工程里轴名改成Axis_01或者某个变量换了个名字C#这边不报错但运行时就抓瞎。或者用ReadSymbolInfo去动态查询符号表每次读变量前都要拼字符串、做类型转换代码冗余不说一个拼写错误就要调试半天。我见过最夸张的项目上位机里维护了一张700多行的“变量地址对照表”每次PLC更新版本就要人肉核对一遍。这种方案不叫开发叫维护石头。1.2 TMC文件解决的是“离线类型安全”问题TMC文件是TwinCAT工程导出的一份XML配置里面保存了NC轴模块的完整信息轴类型、周期时间、控制字状态字定义、参数列表、输入输出Symbol集合。它本身不是运行时动态变化的符号表而是工程编译前就定好的“蓝图”。这意味着我们可以在开发期间把它解析成C#类把这些信息变成编译期就能检查的类型。比如一个轴的实际位置TMC里定义的数据类型是LREAL我们生成C#类后就是double类型的ActPos属性控制字是WORD生成后就是ushort的ControlWord。写错类型、写错名字编译器直接给你报错根本轮不到运行时才发现。1.3 从TMC到C#类最直接的三个收益开发效率提升几十上百个轴变量不需要手写解析TMC一次生成后面复用。运行时稳定所有ADS读写的地址来源于生成的常量或属性不再依赖人手敲字符串杜绝拼写错。工程可追溯TMC文件和C#类一一对应PLC工程升级后重新生成一遍对照diff就能知道变量变了什么。2. TMC文件里藏着什么——关键节点与离线生成的理论基础2.1 从哪里拿到TMC文件在TwinCAT XAE环境中找到NC轴配置Motion或者NC-Task下方的轴右键轴的模块条目通常有“Export TMC File”之类的导出选项。导出的文件就是一个XML格式的.tmc文件。如果你手上只有别人给的一份TMC也可以直接作为输入解析。还有一点TwinCAT安装目录的Sample工程里也会附带一些TMC模板适合先拿来做格式研究。2.2 XML核心结构DataTypes、DataAreas、Module把TMC文件用文本编辑器打开别看它动辄几百KB核心结构其实非常清晰。TcModuleClass DataTypes !-- 定义结构体、枚举、别名等 -- /DataTypes DataAreas Area Symbols Symbol NameAxis1.NcToPlc.ActPos/Name TypeLREAL/Type BitSize64/BitSize IndexGroup16#00010220/IndexGroup IndexOffset16#00000000/IndexOffset CommentActual position/Comment SubItems.../SubItems /Symbol /Symbols /Area /DataAreas Module NameAXIS1/Name CLSID.../CLSID ParameterList.../ParameterList /Module /TcModuleClass几个节点各干各的DataTypes存放复杂类型定义比如MC_AXIS_REF结构体、MC_CONTROL_WORD枚举等里面有字段名和位宽。DataAreas/Area/Symbols这是重头戏每个Symbol节点对应一个ADS可访问的变量包含变量名、数据类型、位宽、IndexGroup和IndexOffset。我们生成C#类时主要处理和利用这一部分。Module模块级参数一般用来拿轴名称、CLSID等元信息。2.3 为什么TMC离线和ADS运行时能对上有人会问TMC是离线配置文件它和PLC运行时内存里的符号表是一回事吗答案是TMC描述的是NC模块的接口配置而ADS符号表是TwinCAT实时系统加载后暴露给上位机的运行时视图。两者在名字、数据类型、IndexGroup和IndexOffset上是严格对应的——因为TwinCAT在加载NC模块时就是按照TMC的描述去创建这些变量的。所以我们可以用TMC离线生成C#类等程序连上PLC后再通过ADS的ReadSymbolInfo或者Handle去访问地址完全对得上。这一点也是整个方案的基石离线生成类型安全代码 运行时ADS动态绑定。3. 两种主流落地路线开发期生成强类型类还是运行时解析3.1 方案A开发期生成C#类文件我推荐写一个生成器程序输入TMC文件输出一个或多个.cs文件。生成结果编译进上位机项目之后所有访问都走强类型代码。优点很明显编译期类型检查杜绝字符串拼错。属性和字段可以直接在IDE里智能提示写起来快。生成过程可以接入CI或构建脚本PLC工程更新后一键重新生成。缺点也有TMC一变需要重新生成并编译上位机。但话说回来PLC工程变化本来就该触发上位机联调这算不上负担。3.2 方案B运行时解析XML动态构建访问器程序启动时读取TMC文件用XDocument或者XmlSerializer解析反射生成属性访问器运行时调用ADS读取。好处是部署灵活PLC工程改了TMC文件上位机不用重编译坏处是把“类型安全”丢掉了属性访问基本靠反射或者字典维护起来反而麻烦。我见过有人用这个方案做通用监控面板但那属于泛用工具不适合做面向工艺的专用上位机。做项目我还是推荐方案A下面展开的也是方案A。3.3 还有一个变种开发期生成器 运行时校验折中方案是用生成器产出代码但程序启动时用ReadSymbolInfo批量校验一遍所有变量是否存在、类型是否匹配。这样既有开发期的强类型体验又保留运行时的快速反馈。我自己的项目基本都这么干上线前能提前暴露变量漂移问题。4. 生成器的完整拆解从XML节点到C#代码的转换过程4.1 生成器的整体流程写生成器不是让你把整个XML翻译一遍而是抽取有用的Symbol信息输出一个方便C#使用的访问层。我的实现分三步读取TMC文件遍历DataAreas/Area/Symbols下所有Symbol节点。对每个Symbol提取Name、Type、BitSize、IndexGroup、IndexOffset。按轴名分组为每个轴生成一个C#类同时生成一个静态符号常量类。下面这段代码是生成器的核心骨架用XDocument解析简单直接。using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Xml.Linq; namespace TmcCodeGenerator { public class SymbolInfo { public string Name { get; set; } public string Type { get; set; } public int BitSize { get; set; } public uint IndexGroup { get; set; } public uint IndexOffset { get; set; } public string Comment { get; set; } } public static class TmcParser { public static ListSymbolInfo ParseSymbols(string tmcPath) { var doc XDocument.Load(tmcPath); var symbols new ListSymbolInfo(); var symbolNodes doc.Descendants(Symbol); foreach (var node in symbolNodes) { var info new SymbolInfo { Name node.Element(Name)?.Value?.Trim(), Type node.Element(Type)?.Value?.Trim(), BitSize int.TryParse(node.Element(BitSize)?.Value, out var bits) ? bits : 0, Comment node.Element(Comment)?.Value?.Trim(), }; // IndexGroup 在 TMC 里写作 16#00010220 这种格式 info.IndexGroup ParseHexValue(node.Element(IndexGroup)?.Value); info.IndexOffset ParseHexValue(node.Element(IndexOffset)?.Value); if (!string.IsNullOrEmpty(info.Name)) symbols.Add(info); } return symbols; } private static uint ParseHexValue(string raw) { if (string.IsNullOrWhiteSpace(raw)) return 0; var cleaned raw.Replace(16#, ).Replace(0x, ); return Convert.ToUInt32(cleaned, 16); } } }4.2 TMC类型到C#类型的映射规则这一步是生成代码质量的关键。TMC里的类型名和C#原生类型不是一一对应需要做映射。我整理了一份常用的映射表基本覆盖NC轴里会出现的主要类型TMC类型C#类型说明BOOLbool布尔BYTEbyte无符号8位WORDushort无符号16位DWORDuint无符号32位SINTsbyte有符号8位INTshort有符号16位DINTint有符号32位REALfloat单精度浮点LREALdouble双精度浮点STRING(n)string字符串其他结构体对应生成的C# struct按DataTypes递归生成注意一点TMC里经常出现的是MC_AXIS_REF、ST_AxisParameter这类结构体它们在DataTypes节点里有字段级定义。如果轴参数全部绑定到某个结构体比如把PID参数打包成一个结构体你可以选择把它展开为扁平属性也可以生成对应的C#结构体。展开为扁平属性对ADS读写更友好因为ReadAny一次只能读连续内存生成结构体则更适合整块读写。我一般混合用常用状态变量展平参数结构体保留为C# class或struct。4.3 输出C#类文件的生成逻辑生成器输出分两部分一个McSymbols静态类保存全部符号的IndexGroup/IndexOffset一个Axis类封装每个轴的读写属性。下面是一个生成器输出代码的简化示意public static class McSymbols { public const string Axis1_NcToPlc_ActPos Axis1.NcToPlc.ActPos; public const string Axis1_PlcToNc_SetPos Axis1.PlcToNc.SetPos; public const string Axis1_PlcToNc_ControlWord Axis1.PlcToNc.ControlWord; public const string Axis1_NcToPlc_StatusWord Axis1.NcToPlc.StatusWord; public const string Axis2_NcToPlc_ActPos Axis2.NcToPlc.ActPos; }这里我只写常量但实际我还会把每个Symbol的IndexGroup/IndexOffset放进一个字典方便直接用索引地址访问。因为在某些高频率读取场景直接用IndexGroup/IndexOffset比用字符串Handle更快不用每次查句柄。public class Axis { private readonly AdsClient _ads; private readonly string _prefix; public Axis(AdsClient ads, string prefix) { _ads ads; _prefix prefix; } public double ActPos ReadDouble(${_prefix}.NcToPlc.ActPos); public double SetPos { set WriteDouble(${_prefix}.PlcToNc.SetPos, value); } public ushort ControlWord { set WriteUInt16(${_prefix}.PlcToNc.ControlWord, value); } private double ReadDouble(string symbolName) { var handle _ads.CreateVariableHandle(symbolName); var value (double)_ads.ReadAny(handle, typeof(double)); _ads.DeleteVariableHandle(handle); return value; } private void WriteDouble(string symbolName, double value) { var handle _ads.CreateVariableHandle(symbolName); _ads.WriteAny(handle, value); _ads.DeleteVariableHandle(handle); } }如果你的项目轴特别多遍历Symbols时按轴名分组再拼出这样一个类就行。这里的AdsClient来自TwinCAT.Ads包稍后第5章会讲怎么集成。4.4 把生成器做成一个可复用工具写生成器时不要目标定太窄。我建议把它做成命令行小工具支持三个参数TMC文件路径、输出目录、命名空间。这样每次PLC工程变更只要跑一条命令就能重新生成所有代码。TmcGen.exe --tmc D:\PlcProject\Axis.tmc --out D:\CsProject\Generated --ns MyPlc.NcAxes然后把这个命令挂到项目构建前事件里或者CI流水线里PLC工程师导出TMC文件后上位机项目一编译就自动更新从源头上杜绝两边不一致。5. 生成的类怎么和ADS通信接上——把代码放回真实项目5.1 引入TwinCAT.ADS库并建立连接生成出来的类只是“骨架”真正去PLC里读写数据还要靠ADS库。在NuGet里安装TwinCAT.Ads包然后建立连接。ADS通信走TCP 48898端口AmsNetId是PLC的AMS地址。using TwinCAT.Ads; var client new AdsClient(); client.Connect(new AmsNetId(192.168.0.1.1.1), 851);如果你的PLC和上位机在同一个局域网也可以直接用IP地址加端口连接TwinCAT.Ads库会处理好AMS路由。连接建立后把client传给生成的Axis类就可以用属性读写轴数据了。5.2 一个完整的轴读写示例比如我们要做的动作是读取1号轴当前位置然后给1号轴写一个目标位置。用生成的类就是下面这样干净、直白using (var ads new AdsClient()) { ads.Connect(new AmsNetId(192.168.0.1.1.1), 851); var axis1 new Axis(ads, Axis1); double currentPos axis1.ActPos; axis1.SetPos 1000.5; axis1.ControlWord 0x0006; // 使能 }你不需要在业务代码里再出现任何字符串类型的变量名。轴名变了、变量名变了生成代码会反映出来编译期就能发现。5.3 高频读写的性能优化上面示例里每次读写都创建和删除Handle这在高频读取场景比如1ms周期、多个轴同时读会造成不小的开销。实操建议是在Axis类的构造函数里把常用变量的Handle一次性建好然后整个生命周期复用。更进一步的优化是用indexGroup/indexOffset直接读不走符号名。低速场合无所谓但如果要做示波器、曲线绘制这类每秒几千次采样的功能直接把IndexGroup/IndexOffset交给AdsClient.ReadAny更稳。这也是我在4.3节强调要把地址存进字典的原因。var actPosAddr McSymbols.SymbolIndex[Axis1.NcToPlc.ActPos]; double pos ads.ReadAny(actPosAddr.IndexGroup, actPosAddr.IndexOffset, typeof(double));另外TwinCAT.Ads库支持AdsNotification或者更上层的Notification机制可以订阅变量变化事件避免频繁轮询。订阅时同样用生成的符号名或地址回调里拿到的数据就是强类型转换后的值。6. 往后踩坑的预警单位、多轴和版本更新的三个心法6.1 位置单位别被“double”迷惑TMC文件里的数据类型告诉你这个变量是LREAL所以C#里映射成double。但double不代表单位是毫米倍福NC轴的位置单位完全取决于TwinCAT工程里的缩放配置。有的工程内部用mm有的用µm有的用inc。我见过不止一次联调现场两边对不上位置值上位机显示1000PLC那边实际走了1米——最后发现是单位看错了。所以生成类时我建议在类注释或者属性上加一个单位标记。最省事的方法是在Axis类里放一个公开的UnitScale字段默认1.0按实际工程配置调整。代码生成时可以从TMC的ParameterList里自动识别有没有单位配置有就填上没有就建议工程师手动确认。6.2 多轴项目抽象一个公共基类几个轴还好说轴一多你会发现每个轴的代码高度相似。我把生成的Axis类拆成两层一个手写的AxisBase公共类负责ADS读写、Handle缓存、单位换算等通用逻辑一个由TMC生成的Axis1、Axis2等子类只包含这个轴自己的符号属性和地址。这样生成代码很短业务逻辑也很集中将来加一个轴不会把所有代码都复制一遍。我在实际项目中还加了一层简单的依赖注入每个轴通过构造器注入AdsClient这样调试时可以替换成模拟客户端不用连PLC也能先跑通界面逻辑。6.3 TwinCAT版本升级导致TMC结构漂移TwinCAT 3.1版本迭代过程中NC模块的Symbol命名和类型定义偶尔会调整尤其是4024.4之后的一些版本。线上遇到过一次老版本TMC里轴状态字叫StateWord升级后变成了StatusWord如果生成器写死变量名升级后就会生成一堆空的或错误的属性。我的应对方法是TMC生成器的映射规则不做死而是读取TMC里实际的Symbol名字和Comment来匹配用途比如通过Comment里包含“status”关键字来识别状态字。更稳妥一点维护一张“逻辑名到实际Symbol名”的映射配置表生成器从配置驱动而不是硬编码规则。这样PLC工程升级后改改映射表重新生成即可代码主体不用动。回到开头说的那个场景以前维护700行地址对照表的日子现在变成了一条命令重新生成。TMC文件生成的C#类并不神秘它就是把XML里已经存在的轴信息翻译成编译器能帮你把关的强类型代码。顺着这个思路不只是轴配置TwinCAT里其他模块的TMC文件也可以走同一套流程。你项目里若有这种机械重复的映射工作值得花一个下午把生成器写出来这笔时间投资不会亏。本文还有配套的精品资源点击获取
返回列表