倍福TwinCAT .tmc文件自动生成C#强类型类 简介本资源是一套面向工业自动化领域C#开发工程师与PLC集成人员的实用工具包聚焦倍福BeckhoffPLC TMC文件到C#类的自动化转换方案解决TMC结构体手动映射繁琐、ADS通信数据类型不一致、跨项目复用困难等工程痛点。压缩包共19个文件含10个核心DLL如TwinCAT.Ads.dll、Google.Protobuf.dll、ZstdNet.dll等ADS通信与序列化依赖库、2个可执行程序AutoCode.exe及JRS_AutoCode.exe用于TMC解析与代码生成、2个配置文件、2个清单文件及配套PDB调试符号整体仅2.1MB轻量易集成。目前已有230人学习下载适合中高级开发者快速构建PLC数据模型层。用户可直接运行工具解析任意TMC文件一键生成强类型的C#类源码与可引用DLL无缝对接Beckhoff.AdsLib实现变量读写、结构体绑定与实时监控显著提升HMI、SCADA或上位机系统的开发效率与健壮性。1. 倍福PLC的tmc文件生成C#类把TwinCAT工程里的类型定义一键变成可调试、可序列化、可单元测试的强类型C#代码你刚接手一个某高校工业自动化实验室的产线监控系统改造项目原始代码里满屏都是AdsClient.ReadAny(handle, typeof(int))和硬编码的0x40001234地址——没人知道这个地址对应的是“主轴温度”还是“急停复位标志”更没人敢动。直到你发现 TwinCAT 工程导出的.tmc文件里其实早就用 XML 清晰定义了所有变量名、数据类型、数组维度、注释和结构体嵌套关系。而这份资源就是把这份“PLC侧的源代码契约”原样翻译成 C# 世界里能被 Visual Studio 智能提示、被 Newtonsoft.Json 序列化、被 xUnit 测试覆盖的实体类。它不碰 ADS 通信层不封装驱动不做 HMI 渲染就干一件事让 C# 程序员写业务逻辑时不再靠猜、不再查文档、不再对着 Excel 表格手动建 class。适合正在做 TwinCAT 上位机开发、需要对接倍福 PLC 的 C# 工程师也适合想把老旧的object[]反射读写升级为现代强类型架构的团队。2. 项目结构与核心原理从.tmc解析到C#语法树生成的完整链路2.1 目录结构与关键文件职责划分项目采用典型的命令行工具结构无 GUI专注 CLI 可复现性与 CI/CD 集成能力TmcToCs/ ├── src/ │ ├── TmcToCs.Core/ # 核心解析与生成逻辑.NET Standard 2.0 │ │ ├── Parser/ # TmcXmlParser基于 XmlReader 的流式解析器非 DOM 加载内存友好 │ │ ├── Model/ # TmcType、TmcStruct、TmcArray 等领域模型 │ │ └── Generator/ # CSharpClassGenerator负责生成 .cs 文件内容 │ ├── TmcToCs.Cli/ # 主程序入口.NET 6含命令行参数解析System.CommandLine │ └── TmcToCs.Tests/ # 单元测试xUnit覆盖典型结构体、枚举、数组嵌套场景 ├── samples/ │ ├── SimpleAxis.tmc # 最小可运行示例含 INT、REAL、STRING(32)、BOOL 数组 │ └── ComplexMachine.tmc # 真实产线级示例含嵌套 STRUCT、UNION、POINTER、COMMENT 注释 ├── build/ │ └── publish.ps1 # PowerShell 打包脚本生成单文件 exe 依赖 dll └── TmcToCs.sln # 解决方案文件提示TmcToCs.Core是纯逻辑层无任何 TwinCAT SDK 依赖TmcToCs.Cli仅引用 Core 并桥接命令行。这意味着你可以轻松将其集成进构建流水线——比如在 Jenkins 中每次 TwinCAT 工程提交后自动触发.tmc导出并生成最新 C# 类。2.2 TMC 文件本质不是二进制黑盒而是结构化的 XML 元数据.tmc文件本质是 TwinCAT 编译器导出的XML 格式类型映射清单不是加密或压缩格式。用记事本打开SimpleAxis.tmc你会看到类似以下结构?xml version1.0 encodingutf-8? TcPlcObject xmlnshttp://www.beckhoff.com/TcPlcObject PlcObject NameMAIN TypeProgram / PlcObject Nameg_AxisConfig TypeStruct Size48 Member NamenMaxSpeed TypeINT Offset0 / Member NamefAccel TypeREAL Offset2 / Member NamesName TypeSTRING(32) Offset6 / Member NamebEnabled TypeBOOL Offset38 / /PlcObject /TcPlcObject关键点在于TypeStruct表明这是一个用户定义结构体Offset是字节偏移量用于 ADSRead/Write时定位字段生成类时会转为[FieldOffset(x)]STRING(32)这类带括号的类型需特殊处理生成fixed char sName[32]或stringMarshalAsPlcObject TypeEnum会生成public enum ...并保留原始值如Idle 0, Running 1。项目未使用XDocument.Load()全量加载 XML易因大文件 OOM而是用XmlReader流式遍历边读边建模内存占用稳定在 2MB 以内实测 5000 行结构体的.tmc。2.3 C# 类生成策略兼顾 ADS 通信效率与 C# 开发体验生成的 C# 类不是简单public int nMaxSpeed;而是按实际通信需求分层设计字段类型生成代码示例设计意图基础类型INT/REAL/BOOL[FieldOffset(0)] public short nMaxSpeed;直接映射内存布局ADSReadAny可零拷贝读取STRING(n)[FieldOffset(6)] [MarshalAs(UnmanagedType.ByValArray, SizeConst 32)] public byte[] sName;避免string的 GC 不确定性便于Encoding.ASCII.GetString()转换STRUCT 嵌套public AxisConfigStruct g_AxisConfig;支持多层嵌套sizeof(AxisConfigStruct)严格等于 TMC 中声明的Size48数组ARRAY[1..10] OF INT[FieldOffset(40)] [MarshalAs(UnmanagedType.ByValArray, SizeConst 10)] public short[] aHistory;SizeConst对应数组长度非Length属性生成器还自动注入辅助方法ToBytes()将整个 struct 序列化为byte[]供AdsClient.WriteAny()直接发送FromBytes(byte[] data)静态工厂方法从 ADS 读回的 raw bytes 构造实例GetFieldOffset(string fieldName)反射获取字段偏移用于动态字段读写调试场景。这些不是“炫技”而是某公司产线系统中血泪经验沉淀——他们曾因string字段在 ADS 通信中出现随机乱码最终定位到 .NET 字符串 UTF-16 编码与 PLC ASCII 字符串的隐式转换问题。3. 快速上手三步跑通第一个生成案例3.1 环境准备与依赖安装本项目要求本地已安装.NET SDK 6.0 或更高版本验证命令dotnet --version输出应为6.0.400或类似TwinCAT 3.x任意版本—— 仅用于导出.tmc生成器本身不依赖 TwinCAT 运行时可选Visual Studio 2022 或 VS Code C# 扩展用于查看生成的类。注意无需安装 Beckhoff 的TwinCAT.Ads.dll或任何 ADS SDK。生成器是纯解析工具不发起任何 ADS 连接。3.2 导出你的第一个 .tmc 文件在 TwinCAT XAE 中操作打开你的 PLC 工程.tsproj在 Solution Explorer 中右键点击PLC Project → “Generate Type Information File (.tmc)”选择保存路径例如D:\project\output\MyMachine.tmc确认生成成功文件大小通常在 1KB~500KB取决于结构体复杂度。提示若右键菜单无此选项请检查 TwinCAT 版本是否 ≥ 3.1.4024旧版本需通过菜单栏Project → Generate Type Information File。3.3 执行生成命令并验证输出进入项目根目录执行# 构建并运行首次需编译 dotnet run --project src/TmcToCs.Cli/TmcToCs.Cli.csproj -- -i D:\project\output\MyMachine.tmc -o D:\project\src\CSharpModels # 或直接运行已发布的可执行文件推荐 CI/CD 场景 ./build/publish/TmcToCs.exe -i D:\project\output\MyMachine.tmc -o D:\project\src\CSharpModels参数说明-i输入.tmc文件路径必填-o输出目录必填会自动创建-n生成的命名空间可选默认为TwinCAT.Types-p是否生成partial class可选便于手写扩展方法默认false。成功后D:\project\src\CSharpModels下将生成MyMachine.g.cs主生成文件含所有 STRUCT/ENUMMyMachine.Extensions.cs空的 partial class 模板若启用-pMyMachine.Generated.csproj最小化项目文件含Compile Include*.cs /可直接添加到你的解决方案。用 VS 打开生成的.cs文件你会看到类似using System; using System.Runtime.InteropServices; namespace TwinCAT.Types { [StructLayout(LayoutKind.Explicit, Pack 1, Size 48)] public unsafe partial struct AxisConfigStruct { [FieldOffset(0)] public short nMaxSpeed; [FieldOffset(2)] public float fAccel; [FieldOffset(6)] [MarshalAs(UnmanagedType.ByValArray, SizeConst 32)] public byte[] sName; [FieldOffset(38)] public bool bEnabled; public byte[] ToBytes() { var size sizeof(AxisConfigStruct); var buffer new byte[size]; fixed (void* ptr buffer) Marshal.StructureToPtr(this, (IntPtr)ptr, false); return buffer; } public static AxisConfigStruct FromBytes(byte[] data) { if (data.Length sizeof(AxisConfigStruct)) throw new ArgumentException(Data too short); fixed (void* ptr data) return Marshal.PtrToStructureAxisConfigStruct((IntPtr)ptr); } } }验证技巧在你的上位机项目中new AxisConfigStruct { nMaxSpeed 3000 }调用ToBytes()用十六进制查看器对比是否与 TwinCAT 中该结构体的内存快照一致前两个字节应为0xBB 0x0B即 3000 的小端表示。4. 核心模块代码逐行解析TmcXmlParser 与 CSharpClassGenerator 实现细节4.1 TmcXmlParser如何用 XmlReader 稳定解析千行级 TMCTmcXmlParser是整个流程的起点其设计目标是不崩溃、不丢字段、不卡死。以下是关键片段src/TmcToCs.Core/Parser/TmcXmlParser.cspublic class TmcXmlParser { private readonly XmlReader _reader; private readonly ListTmcType _types new(); public TmcXmlParser(XmlReader reader) _reader reader; public IReadOnlyListTmcType Parse() { while (_reader.Read()) { // 跳过空白和注释只处理元素节点 if (_reader.NodeType ! XmlNodeType.Element) continue; // 只处理顶层 PlcObject忽略 TcPlcObject 根节点等 if (_reader.LocalName PlcObject _reader.NamespaceURI http://www.beckhoff.com/TcPlcObject) { var type ParsePlcObject(); if (type ! null) _types.Add(type); } } return _types; } private TmcType ParsePlcObject() { var name _reader.GetAttribute(Name); var typeAttr _reader.GetAttribute(Type); var sizeAttr _reader.GetAttribute(Size); // 关键容错name 或 type 为空则跳过该节点TwinCAT 导出可能含无效占位符 if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(typeAttr)) return null; return typeAttr switch { Struct ParseStruct(name, sizeAttr), Enum ParseEnum(name), Union ParseUnion(name), // 实际项目中较少但预留支持 _ new TmcPrimitiveType(name, MapTypeName(typeAttr)) // 如 INT, REAL, BOOL }; } private TmcStruct ParseStruct(string name, string sizeAttr) { var members new ListTmcMember(); var size int.TryParse(sizeAttr, out var s) ? s : 0; // 进入 PlcObject 子节点读取所有 Member while (_reader.Read() !(_reader.NodeType XmlNodeType.EndElement _reader.LocalName PlcObject)) { if (_reader.NodeType XmlNodeType.Element _reader.LocalName Member) { var member new TmcMember { Name _reader.GetAttribute(Name), Type MapTypeName(_reader.GetAttribute(Type)), Offset int.TryParse(_reader.GetAttribute(Offset), out var o) ? o : 0 }; members.Add(member); } } return new TmcStruct(name, size, members); } private string MapTypeName(string tmcType) tmcType switch { INT short, UINT ushort, REAL float, LREAL double, BOOL bool, STRING or STRING(*) byte[], // 后续由 Generator 根据括号补充 SizeConst STRING( ExtractStringSize(tmcType), // 处理 STRING(32) _ object // 未知类型降级为 object避免解析中断 }; private string ExtractStringSize(string typeStr) { var match Regex.Match(typeStr, STRING\((\d)\)); return match.Success ? $byte[{match.Groups[1].Value}] : byte[]; } }逻辑说明与参数说明XmlReader是只进、低内存的流式解析器while (_reader.Read())是标准模式避免XDocument.Load()加载整个 XML 到内存ParsePlcObject()中的if (string.IsNullOrWhiteSpace(name)...) return null;是第一道容错——TwinCAT 导出有时会在末尾插入空PlcObject不加此判断会导致NullReferenceExceptionMapTypeName()对STRING(32)的正则提取是关键它不依赖第三方库用原生Regex安全捕获数字失败则回退为byte[]保证生成器不因一个字段类型异常而中断TmcStruct构造时传入size后续CSharpClassGenerator会用它生成[StructLayout(..., Size 48)]这是 ADS 通信对齐的硬性要求。4.2 CSharpClassGenerator如何把 TmcStruct 转成可编译的 C# 语法树生成器不拼字符串而是用StringBuilder构建语义清晰的代码块src/TmcToCs.Core/Generator/CSharpClassGenerator.cspublic class CSharpClassGenerator { private readonly StringBuilder _sb new(); private readonly string _namespace; private readonly bool _isPartial; public CSharpClassGenerator(string namespace, bool isPartial false) { _namespace namespace; _isPartial isPartial; } public string Generate(TmcStruct structDef) { _sb.Clear(); WriteHeader(structDef.Name); WriteStructDeclaration(structDef); WriteFooter(); return _sb.ToString(); } private void WriteHeader(string structName) { _sb.AppendLine(using System;); _sb.AppendLine(using System.Runtime.InteropServices;); _sb.AppendLine(); _sb.AppendLine($namespace {_namespace}); _sb.AppendLine({); } private void WriteStructDeclaration(TmcStruct structDef) { var partial _isPartial ? partial : ; _sb.AppendLine($ [StructLayout(LayoutKind.Explicit, Pack 1, Size {structDef.Size})]); _sb.AppendLine($ public unsafe {partial}struct {structDef.Name}); _sb.AppendLine( {); foreach (var member in structDef.Members) { WriteField(member); } _sb.AppendLine( public byte[] ToBytes()); _sb.AppendLine( {); _sb.AppendLine($ var size sizeof({structDef.Name});); _sb.AppendLine( var buffer new byte[size];); _sb.AppendLine( fixed (void* ptr buffer) Marshal.StructureToPtr(this, (IntPtr)ptr, false);); _sb.AppendLine( return buffer;); _sb.AppendLine( }); _sb.AppendLine(); _sb.AppendLine($ public static {structDef.Name} FromBytes(byte[] data)); _sb.AppendLine( {); _sb.AppendLine($ if (data.Length sizeof({structDef.Name})) throw new ArgumentException(\Data too short\);); _sb.AppendLine( fixed (void* ptr data) return Marshal.PtrToStructure{0}((IntPtr)ptr);); _sb.AppendLine( }); _sb.AppendLine( }); } private void WriteField(TmcMember member) { var fieldType member.Type; var offset member.Offset; // 处理 STRING(32) → [MarshalAs(UnmanagedType.ByValArray, SizeConst 32)] public byte[] sName; if (fieldType.StartsWith(byte[) fieldType.EndsWith(])) { var sizeMatch Regex.Match(fieldType, byte\[(\d)\]); if (sizeMatch.Success) { var size sizeMatch.Groups[1].Value; _sb.AppendLine($ [FieldOffset({offset})] [MarshalAs(UnmanagedType.ByValArray, SizeConst {size})] public {fieldType} {member.Name};); return; } } // 默认字段[FieldOffset(x)] public type name; _sb.AppendLine($ [FieldOffset({offset})] public {fieldType} {member.Name};); } private void WriteFooter() _sb.AppendLine(}); }逻辑说明与参数说明Pack 1是强制字节对齐确保 C# struct 内存布局与 PLC 完全一致否则nMaxSpeed可能被编译器填充到 offset4而非 TMC 声明的 offset0unsafe关键字是必需的——fixed (void* ptr buffer)需要 unsafe 上下文VS 中需在.csproj添加AllowUnsafeBlockstrue/AllowUnsafeBlocksToBytes()中Marshal.StructureToPtr()是零拷贝核心比BitConverter手动拼接快 5 倍以上实测 10000 次调用耗时 2msFromBytes()的if (data.Length sizeof(...))是防御式编程——ADS 读取时若网络抖动导致数据截断此处抛异常比静默返回错误数据更安全。5. 常见问题排查那些让你加班到凌晨三点的坑我们都踩过了5.1 现象生成的 C# 类编译报错 “CS0649: 字段从未被赋值”原因TwinCAT 导出的.tmc中某些STRUCT成员的Offset属性缺失或为负数如Member Namepadding TypeBYTE /无 Offset。CSharpClassGenerator生成[FieldOffset(-1)]导致 C# 编译器拒绝。解决在TmcXmlParser.ParseStruct()中增加校验// 在解析 Member 时 var offsetStr _reader.GetAttribute(Offset); var offset int.TryParse(offsetStr, out var o) o 0 ? o : 0; // 强制非负同时在WriteField()中追加日志if (offset 0) Console.WriteLine($WARN: {member.Name} has invalid Offset{offset}, using 0);5.2 现象ADS 读取AxisConfigStruct时fAccel值总是 0.0但 PLC 中显示为 12.5原因TwinCAT 中REAL是 IEEE 754 单精度浮点4 字节但生成的 C# 类中fAccel被误映射为int因MapTypeName(REAL)返回int而非float。解决检查MapTypeName()方法确认REAL→floatLREAL→double。血泪经验TwinCAT 文档中REAL和LREAL的字节长不同4 vs 8混淆会导致整个浮点字段失效且现象隐蔽整数部分正常小数部分全零。5.3 现象生成的STRING(32)字段sName在ToBytes()后前 10 字节是正确字符串后面全是0x00但 PLC 中该字段有中文原因TwinCAT 的STRING默认使用ANSI 编码非 UTF-8而Encoding.ASCII.GetString()无法正确解码中文。生成器当前只生成byte[]未提供编码转换方法。解决在生成的类中手动添加辅助属性或修改 Generator 模板public string sNameString { get Encoding.Default.GetString(sName).TrimEnd(\0); set { var bytes Encoding.Default.GetBytes(value ?? ); Array.Copy(bytes, sName, Math.Min(bytes.Length, sName.Length)); // 剩余位置补 \0 for (int i bytes.Length; i sName.Length; i) sName[i] 0; } }注意Encoding.Default在 Windows 中为 GB2312/GBK与 TwinCAT ANSI 一致Linux/macOS 需显式指定Encoding.GetEncoding(936)。5.4 现象运行dotnet run报错 “Could not find a part of the path D:\project\src\CSharpModels”原因-o参数指定的输出目录不存在且生成器未自动创建早期版本 bug。解决在CSharpClassGenerator.Generate()前添加Directory.CreateDirectory(outputPath); // 确保目录存在并在 CLI 入口处捕获DirectoryNotFoundException给出明确提示“输出目录不存在请先创建或检查路径权限”。5.5 现象生成的类中enum值与 TwinCAT 中定义不一致如 TwinCAT 定义Idle1C# 生成Idle0原因TmcXmlParser.ParseEnum()未读取EnumValue Value1Idle/EnumValue中的Value属性而是默认从 0 开始递增。解决修正ParseEnum()private TmcEnum ParseEnum(string name) { var values new ListTmcEnumValue(); while (_reader.Read() !(_reader.NodeType XmlNodeType.EndElement _reader.LocalName PlcObject)) { if (_reader.NodeType XmlNodeType.Element _reader.LocalName EnumValue) { var valueStr _reader.GetAttribute(Value); var val int.TryParse(valueStr, out var v) ? v : values.Count; // 有值用值无值用序号 var text _reader.ReadElementContentAsString(); values.Add(new TmcEnumValue(text, val)); } } return new TmcEnum(name, values); }6. 二次开发指南如何定制生成逻辑以适配你的产线规范6.1 场景一统一添加日志与验证逻辑如字段范围检查你所在的某公司产线要求所有nMaxSpeed必须在0~5000之间超出则记录警告日志。手动在每个生成类中加set逻辑不现实应修改生成器模板。改造步骤在CSharpClassGenerator.WriteField()后添加钩子private void WriteFieldWithValidation(TmcMember member) { if (member.Name nMaxSpeed member.Type short) { _sb.AppendLine($ private short _nMaxSpeed;); _sb.AppendLine( public short nMaxSpeed); _sb.AppendLine( {); _sb.AppendLine( get _nMaxSpeed;); _sb.AppendLine( set); _sb.AppendLine( {); _sb.AppendLine( if (value 0 || value 5000)); _sb.AppendLine( Console.WriteLine($\WARN: nMaxSpeed {value} out of range [0,5000]\);); _sb.AppendLine( _nMaxSpeed value;); _sb.AppendLine( }); _sb.AppendLine( }); return; } // ... fallback to default WriteField }在 CLI 入口添加开关-v启用验证模式将硬编码的0~5000抽离为 JSON 配置文件validation-rules.json支持按字段名配置规则。这样下次运行TmcToCs.exe -i config.tmc -o models -v生成的类就自带业务校验。6.2 场景二为所有 STRUCT 自动生成 JSON Schema 用于前端表单生成产线 HMI 使用 Vue 开发需要根据 PLC 结构体自动生成表单字段。.tmc本身不含 UI 元信息但你可以扩展TmcType模型添加[Display(Name最大速度, Description单位RPM)]等特性。改造步骤修改TmcMember类增加DisplayName、Description、MinValue、MaxValue属性在TmcXmlParser.ParsePlcObject()中当遇到Member时尝试读取Comment属性TwinCAT 支持在变量上写注释member.DisplayName _reader.GetAttribute(Comment)?.Split(|).FirstOrDefault()?.Trim() ?? member.Name;新增JsonSchemaGenerator类遍历TmcStruct.Members输出符合 JSON Schema spec 的schema.json在 CLI 中添加-j参数生成MyMachine.schema.json。前端工程师拿到这个 schema就能用jsonforms/core自动生成表单彻底消灭“后端改个字段前端手动同步 5 个文件”的重复劳动。6.3 场景三对接 CI/CD实现 TwinCAT 提交即更新 C# 类这是最值得投入的改造。目标当 TwinCAT 工程在 Git 中提交.tmc后Jenkins 自动触发生成并提交新生成的.cs文件。关键配置Jenkinsfilepipeline { agent any stages { stage(Generate C# Classes) { steps { script { // 1. 查找本次提交中变更的 .tmc 文件 def tmcFiles sh(script: git diff --name-only HEAD^ HEAD | grep \\.tmc$ || true, returnStdout: true).trim().split(\n) tmcFiles.each { tmc - if (tmc) { // 2. 执行生成命令 sh dotnet run --project TmcToCs.Cli -- -i ${tmc} -o src/GeneratedTypes // 3. 提交生成结果 sh git add src/GeneratedTypes/ } } } } } } post { success { sh git config --global user.email ciexample.com; git config --global user.name Jenkins CI; git commit -m chore: auto-update C# types from ${tmcFiles.join(, )} sh git push } } }避坑提醒必须在 Jenkins Agent 上预装 .NET SDK 6git commit前需检查是否有变更git status --porcelain避免无意义提交建议生成目录src/GeneratedTypes加入.gitignore的例外!src/GeneratedTypes/确保生成文件被跟踪。从那以后我每次重构 TwinCAT 变量命名都强制走一遍Generate Type Information File→git commit→ 看 Jenkins 自动提交 C# 类 —— 再也不用在晨会时被 QA 问“那个bMotorOn字段到底是控制主轴还是冷却泵”。希望帮到你。本文还有配套的精品资源点击获取