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及以后,推荐使用DTL(Date 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 核心武器库:CONCAT,INSERT,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_FormatTwoDigits:
FUNCTION “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:SS,2代表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_TIME(DT)格式,而新的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])。 - 检查2:
INSERT指令的位置参数P是否计算正确?位置不能超过字符串长度,也不能是0或负数。确保初始化的空格字符串长度大于所有插入操作结束后的位置。 - 检查3:用于格式化的
FC_FormatTwoDigits函数是否正确返回了两位字符串?用监视表查看其输出。
- 检查1:输出字符串
问题2:时间显示比实际快/慢8小时。
- 原因:这几乎可以肯定是时区问题。PLC的硬件时钟可能设置为UTC时间,而你需要的是北京时间(UTC+8)。
- 解决:进入设备视图,选中CPU,在属性->“常规”->“时间”中,检查“时间同步”和“时区”设置。确保时区设置正确。或者,在程序中手动加上8小时的偏移(需谨慎,考虑夏令时)。
问题3:在HMI上显示的时间字符串末尾有奇怪的字符(如方框或乱码)。
- 原因:字符串实际内容后面包含了未初始化的内存数据。当你在PLC中用空格初始化字符串时,可能空格数量不够,
INSERT操作没有覆盖到字符串的全部范围。 - 解决:确保初始化字符串时,空格数量完全等于你声明的字符串长度。更稳妥的方法是,在FC开始时,使用
S_MOVE指令或循环将字符串的每个字符都赋值为‘ ‘(空格)。或者,在组装完字符串后,使用LEFT指令截取到有效长度。
- 原因:字符串实际内容后面包含了未初始化的内存数据。当你在PLC中用空格初始化字符串时,可能空格数量不够,
通过以上从原理到实践,从基础到扩展,再到调试的完整梳理,相信你已经掌握了在博途V16中将系统时间转换为字符串的核心技能。这个功能本身不复杂,但体现了工控编程中“将机器数据转化为业务信息”的典型思路。在实际项目中灵活运用,能让你的程序更加健壮和易维护。