1. 项目缘起为什么要在博途中处理系统时间字符串在工业自动化项目里尤其是涉及到数据记录、生产报表、设备状态追溯或者配方管理的场景时间戳是一个绕不开的核心要素。你可能需要把某个报警发生的时间、一批产品的生产开始/结束时间或者一个关键工艺参数的记录时间以清晰可读的格式保存下来或者显示在HMI画面上。西门子TIA Portal博途作为主流的PLC编程软件其强大的功能也体现在对时间和字符串的灵活处理上。直接使用PLC内部的系统时间比如SFC1读取的DATE_AND_TIME数据类型得到的是一个结构化的数据块对于机器和程序来说很友好但对于操作工、维护工程师或者需要导出到上位机数据库的场合就不那么直观了。我们更常见的需求是得到一个像2024-05-27 14:30:25或者20240527143025这样的字符串。这个需求看似简单但在博途的环境下尤其是面对S7-1200/1500系列PLC时需要一些特定的函数块和步骤来实现。很多刚接触博途的朋友或者习惯了其他品牌PLC编程思维的工程师第一次做这个功能时难免会走点弯路。今天我就结合在V16环境下的实际项目经验把这个过程的原理、步骤和踩过的坑掰开揉碎了讲清楚。2. 核心原理拆解从系统时钟到可读字符串的转换链路要理解整个转换过程我们得先看看博途TIA Portal里时间和字符串是怎么被PLC“认识”和“表达”的。这就像把一块瑞士机械表的时间翻译成电子表显示屏上的数字一样中间需要好几道工序。2.1 西门子PLC的系统时间DTL与DATE_AND_TIME在S7-1200/1500中获取系统时间最常用的方式是使用RD_SYS_T读取系统时间系统函数。这个函数返回的数据类型在博途V16及以后推荐使用DTLDate and Time Long。它是一个12字节的结构包含了从年份到纳秒的所有时间信息精度非常高。当然为了兼容老程序你可能还会见到DATE_AND_TIME简称DT类型它是8字节的。为了通用性和未来兼容性我们以DTL为例进行讲解。一个典型的DTL结构体包含以下成员Year(UInt)年份如2024Month(USInt)月份1-12Day(USInt)日1-31Weekday(USInt)星期几1周日2周一...7周六Hour(USInt)时0-23Minute(USInt)分0-59Second(USInt)秒0-59Nanosecond(UDInt)纳秒0-999,999,999我们的目标就是把这个结构体里的数字比如2024、5、27、14、30、25变成字符2、0、2、4、-... 然后按顺序拼在一起。2.2 字符串在博途中的本质字符数组在博途的SCL或LAD/FBD环境中字符串String并不是一个“原子”类型。它本质上是一个结构最大长度可达254个字符对于S7-1200/1500。这个结构包括一个最大长度字段和一个实际长度字段后面跟着字符数据数组。当我们说“转换成字符串”实际上是在构建这个字符数组。转换的核心思路是“分解”与“重组”分解从DTL结构体中提取出年、月、日、时、分、秒等各个独立的整数。格式化对每个整数进行格式化。例如月份“5”需要变成“05”这涉及到在个位数前补零。重组将格式化后的数字字符连同分隔符如-: 空格按照预定的顺序如YYYY-MM-DD HH:MM:SS逐个拼接到一个字符串变量中。这个过程无法通过一个指令完成需要我们组合使用多个函数块。理解了这个底层逻辑再看具体的程序块就不会觉得它们是一堆黑盒魔法了。3. 环境准备与关键函数块详解在开始动手写代码之前确保你的TIA Portal V16项目已经创建并且添加了正确的PLC设备如S7-1200或S7-1500。这个功能主要依靠SCL结构化控制语言来实现因为它的字符串处理能力比梯形图LAD要强大和直观得多。当然在LAD中通过调用SCL编写的函数块FC/FB也是完全可行的这是工程中常见的做法。3.1 核心武器库CONCATINSERT,DTL_TO_STRING很多人会首先去寻找一个叫DTL_TO_STRING或类似名字的现成函数块希望能一键转换。遗憾的是在博途的标准库中并没有这样一个直接的“万能”块。西门子提供了更基础的字符串操作指令我们需要像搭积木一样组合它们。最常用的几个是CONCAT连接这个块用于将多个字符串首尾相连。例如把2024-和05-连接成2024-05-。它的缺点是输入参数数量有限且每次调用都会生成一个新的临时字符串频繁使用可能影响性能。INSERT插入这个块功能更强大。它可以将一个字符串插入到另一个字符串的指定位置。这是我们本次实现的主力军。我们可以先创建一个足够长的、全是空格的初始字符串然后依次将年、月、日、分隔符等“插入”到正确的位置上。NUM_TO_STRING数值转字符串这个块负责把整数INT, DINT等转换成对应的数字字符串。比如输入整数5输出字符串5。但它有个问题它不会自动补零。对于月份5它输出的是5而不是我们想要的05。LEFT,RIGHT,MID字符串截取这些块用于处理字符串的局部在补零格式化时会用到。注意在SCL中你可以直接使用运算符来连接字符串这比调用CONCAT块更简洁底层效果是一样的。例如resultString : ‘2024-’ ‘05-’ ‘27’;。但在需要动态插入到固定位置时INSERT块依然不可替代。3.2 补零格式化一个必须自己动手的环节由于NUM_TO_STRING不补零我们需要自己实现一个“补零”函数。这是一个经典的编程小技巧在博途中可以封装成一个通用的函数FC。思路很简单判断数值是否小于10。如果是则在转换后的字符串前面拼接一个字符0如果不是则直接转换。 例如在SCL中编写一个FC_FormatTwoDigitsFUNCTION “FC_FormatTwoDigits” : String VAR_INPUT value : Int; END_VAR VAR_TEMP tempStr : String; END_VAR IF value 10 THEN “FC_FormatTwoDigits” : CONCAT(‘0’, NUM_TO_STRING(value)); ELSE “FC_FormatTwoDigits” : NUM_TO_STRING(value); END_IF;这个函数接收一个整数返回一个两位的字符串。我们将用它来处理月、日、时、分、秒。4. 实战步骤手把手构建时间转换函数块理论准备就绪现在我们开始创建一个实用的函数块。我建议创建一个函数FC因为它没有存储空间更纯粹。我们将其命名为FC_GetTimeString。4.1 第一步定义接口与局部变量打开FC首先定义输入输出参数和临时变量。输入iDTL_Time(DTL)输入的系统时间来自RD_SYS_T。iFormat(Int)可选用于指定输出格式。例如1代表YYYY-MM-DD HH:MM:SS2代表YYYYMMDDHHMMSS。为了简化我们先实现格式1。输出oTimeString(String[30])输出的时间字符串。长度给30个字符足够容纳各种格式。临时变量sYear,sMonth...sSecond(String[2])用于存放格式化后的两位字符串。sTemp(String[30])用于构建过程的临时字符串。4.2 第二步提取并格式化时间分量在SCL编辑器中我们开始编写主体逻辑。// 1. 提取DTL中的各个部分假设iDTL_Time是一个DTL类型的输入 VAR dtlData AT iDTL_Time: STRUCT Year: UINT; Month: USINT; Day: USINT; Hour: USINT; Minute: USINT; Second: USINT; // ... 其他字段本例暂不需要 END_STRUCT; END_VAR // 2. 格式化各分量为两位字符串 sYear : NUM_TO_STRING(dtlData.Year); // 年份是4位无需补零 sMonth : FC_FormatTwoDigits(dtlData.Month); sDay : FC_FormatTwoDigits(dtlData.Day); sHour : FC_FormatTwoDigits(dtlData.Hour); sMinute : FC_FormatTwoDigits(dtlData.Minute); sSecond : FC_FormatTwoDigits(dtlData.Second);这里用到了一个高级技巧AT覆盖。它允许我们用一个结构体STRUCT去“解释”一片内存区域这里是输入参数iDTL_Time。这样我们就可以直接用dtlData.Year来访问年份而不需要调用复杂的BLKMOV或指针操作。这是SCL处理结构化工控数据非常高效的方式。4.3 第三步使用INSERT指令组装字符串这是最关键的一步也是和简单连接CONCAT或不同的地方。我们先初始化一个全为空格的长字符串然后像填空格一样把内容插进去。// 3. 初始化结果字符串为全空格假设我们需要25个字符的固定长度 oTimeString : ‘ ‘; // 25个空格 // 4. 按位置插入各个部分 // 格式YYYY-MM-DD HH:MM:SS // 位置索引从1开始 oTimeString : INSERT(IN1 : oTimeString, IN2 : sYear, P : 1); // 插入年份位置1 oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘-‘, P : 5); // 插入‘-‘位置5 oTimeString : INSERT(IN1 : oTimeString, IN2 : sMonth, P : 6); // 插入月份位置6 oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘-‘, P : 8); // 插入‘-‘位置8 oTimeString : INSERT(IN1 : oTimeString, IN2 : sDay, P : 9); // 插入日位置9 oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘ ‘, P : 11); // 插入空格位置11 oTimeString : INSERT(IN1 : oTimeString, IN2 : sHour, P : 12); // 插入时位置12 oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘:‘, P : 14); // 插入‘:‘位置14 oTimeString : INSERT(IN1 : oTimeString, IN2 : sMinute, P : 15); // 插入分位置15 oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘:‘, P : 17); // 插入‘:‘位置17 oTimeString : INSERT(IN1 : oTimeString, IN2 : sSecond, P : 18); // 插入秒位置18通过这种方式我们精确地控制了每个字符在最终字符串中的位置输出的格式非常规整。无论年月日的数值是几位数它们都会出现在固定的列上这对于后续的文本对齐、比较或导入数据库都非常有利。5. 避坑指南与性能优化思考按照上面的步骤功能基本就实现了。但在实际项目应用中还有一些细节需要特别注意否则可能会遇到一些意想不到的问题。5.1 时区与夏令时问题RD_SYS_T读取的是PLC硬件时钟的时间。这个时间通常是本地时间并且PLC本身可能不会自动处理夏令时夏令时切换。如果你的项目对时间的绝对准确性要求极高或者需要跨时区对比这里就有坑。建议1在重要的数据记录或时间戳应用中明确记录时间是否已考虑夏令时。有时更稳妥的做法是让上位机或SCADA系统来负责时间的最终转换和显示PLC只提供原始的DTL结构。建议2如果必须由PLC处理可以考虑在PLC中维护一个“时区偏移量”参数比如TimeZoneOffset类型为Time在转换字符串前先用TIME加减运算调整DTL中的小时部分。但这会非常复杂不推荐。5.2 字符串长度与内存管理博途中的String类型有最大长度限制。如果你声明的oTimeString长度是String[20]但实际组装出来的字符串包括结束符超过了20个字符操作会失败可能导致字符串被截断或者输出空字符串。避坑方法务必给输出字符串分配足够的空间。对于YYYY-MM-DD HH:MM:SS格式需要19个字符。加上字符串类型自身的头部信息声明为String[25]或String[30]是安全且良好的习惯。在初始化全空格字符串时空格数量也要匹配这个长度。5.3 扫描周期与性能影响如果你在一个快速循环如OB1中频繁调用这个转换函数并且使用CONCAT进行大量字符串拼接可能会增加PLC的扫描周期时间。INSERT指令相对高效因为它是在原字符串上修改。优化建议不要在每个扫描周期都获取并转换时间。通常的做法是在需要的时候如发生报警、批次结束时才调用RD_SYS_T获取一次时间。或者在一个低速循环中断OB如OB35周期设为1秒或更长中定期获取和转换时间将结果存入一个全局DB的变量中供其他程序随时读取。这样时间字符串每秒更新一次对绝大多数应用来说精度足够且对系统性能影响微乎其微。5.4 DTL与DATE_AND_TIME (DT) 的混淆这是初学者常犯的错误。老式的SFC1读取的是DATE_AND_TIMEDT格式而新的RD_SYS_T默认使用DTL。两者字节结构和精度不同。关键区别DT是8字节BCD码格式而DTL是12字节的二进制结构。它们的存储方式天差地别。如果你把一个DTL变量错误地连接到期望DT类型输入的指令上或者反过来时间数据会完全错乱。如何选择对于新项目统一使用DTL和RD_SYS_T。如果遇到需要与旧程序或使用DT格式的第三方设备通讯再进行转换。博途库中有DTL_TO_DT和DT_TO_DTL转换函数块可用。6. 功能扩展更多格式与高级应用实现了基础格式后我们可以很容易地扩展这个函数块使其支持多种输出格式满足不同场景的需求。6.1 支持多种输出格式修改之前的FC_GetTimeString利用iFormat输入参数进行选择。CASE iFormat OF 1: // “YYYY-MM-DD HH:MM:SS” // ... 调用上述INSERT逻辑 2: // “YYYYMMDDHHMMSS” 紧凑格式常用于文件名 oTimeString : sYear sMonth sDay sHour sMinute sSecond; 3: // “DD/MM/YYYY HH:MM” oTimeString : INSERT(IN1 : oTimeString, IN2 : sDay, P:1); oTimeString : INSERT(IN1 : oTimeString, IN2 : ‘/‘, P:3); // ... 以此类推 ELSE // 默认格式1 // ... END_CASE;6.2 生成带时间戳的文件名这在数据记录功能中非常有用。你可以将时间字符串作为文件名的一部分确保文件不会重复且按时间排序一目了然。// 假设我们有一个基础文件名 “DataLog” baseName : ‘DataLog_’; timeStamp : FC_GetTimeString(iDTL_Time:#sysTime, iFormat:2); // 使用紧凑格式 fullFileName : baseName timeStamp ‘.csv’; // 结果可能是”DataLog_20240527143025.csv”然后你可以将这个fullFileName字符串用于TSEND_C发送文件或高级语言如C#通过Openness API操作时的文件创建。6.3 与HMI/WinCC的集成在WinCC (TIA Portal) 画面上显示这个时间字符串非常简单。在PLC中将转换好的时间字符串如DB_GlobalData.TimeString设置为一个String类型的变量。 在WinCC画面编辑器中添加一个“I/O域”或“文本域”。在其属性-属性-文本-文本中选择“动态对话框”。连接变量选择刚才的PLC字符串变量如DB_GlobalData.TimeString。显示格式选择“字符串”。这样HMI上就会实时显示PLC转换好的时间字符串。如果需要HMI直接处理时间WinCC本身有更丰富的时间脚本函数但那属于上位机范畴PLC提供原始或格式化好的时间字符串是更解耦、更清晰的做法。7. 调试技巧与常见问题排查即使逻辑正确第一次运行时也可能得不到预期结果。掌握以下调试技巧能帮你快速定位问题。7.1 使用监视表与强制表这是最直接的在线调试工具。监视RD_SYS_T的输出首先在线打开监视表添加RD_SYS_T的OUT输出DTL类型。查看其Year,Month等成员是否正确。如果这里就是错的说明PLC系统时间设置有问题需要去硬件配置里或通过WR_SYS_T校正PLC时钟。逐步监视转换FC在调用FC_GetTimeString的块如OB1或OB35里在线监视。查看输入iDTL_Time的值是否传递正确。然后关键一步在FC内部右键点击SCL代码编辑区选择“从块生成监视”。这会为FC创建一个独立的监视视图你可以看到dtlData.Year、sMonth等所有中间变量的值。逐行检查看是哪一步转换出了问题。强制输出如果怀疑是某个条件没触发可以临时在SCL代码里强制给某个变量赋值在线模式下修改值然后观察后续变化。7.2 典型问题与解决方案问题1输出字符串为空或乱码。检查1输出字符串oTimeString的声明长度是否足够尝试将其声明得更大如String[50]。检查2INSERT指令的位置参数P是否计算正确位置不能超过字符串长度也不能是0或负数。确保初始化的空格字符串长度大于所有插入操作结束后的位置。检查3用于格式化的FC_FormatTwoDigits函数是否正确返回了两位字符串用监视表查看其输出。问题2时间显示比实际快/慢8小时。原因这几乎可以肯定是时区问题。PLC的硬件时钟可能设置为UTC时间而你需要的是北京时间UTC8。解决进入设备视图选中CPU在属性-“常规”-“时间”中检查“时间同步”和“时区”设置。确保时区设置正确。或者在程序中手动加上8小时的偏移需谨慎考虑夏令时。问题3在HMI上显示的时间字符串末尾有奇怪的字符如方框或乱码。原因字符串实际内容后面包含了未初始化的内存数据。当你在PLC中用空格初始化字符串时可能空格数量不够INSERT操作没有覆盖到字符串的全部范围。解决确保初始化字符串时空格数量完全等于你声明的字符串长度。更稳妥的方法是在FC开始时使用S_MOVE指令或循环将字符串的每个字符都赋值为‘ ‘空格。或者在组装完字符串后使用LEFT指令截取到有效长度。通过以上从原理到实践从基础到扩展再到调试的完整梳理相信你已经掌握了在博途V16中将系统时间转换为字符串的核心技能。这个功能本身不复杂但体现了工控编程中“将机器数据转化为业务信息”的典型思路。在实际项目中灵活运用能让你的程序更加健壮和易维护。