
1. 项目概述从设备时间到字符串的精准掌控在工业自动化项目里尤其是涉及到数据记录、生产批次追溯、设备运行日志或者与上位机系统如MES、SCADA进行数据交互时一个精准、格式统一的时间戳往往是不可或缺的。想象一下你正在调试一条包装线需要记录每一箱产品封箱完成的精确时刻或者你的设备报警信息需要附带发生时间以便后续分析。这时候直接从PLC里获取系统时间并将其转换成我们熟悉的“2024-05-27 14:30:15”这样的字符串格式就成了一个非常实际的需求。这个项目标题“博途V16 获取系统时间转换成字符串”直指西门子TIA Portal博途环境下S7-1200/1500系列PLC编程中的一个经典任务。它不仅仅是调用一个指令那么简单背后涉及对PLC系统时钟的理解、数据类型如DTL、字符串的转换、以及如何高效、可靠地组织代码。很多新手可能会在转换过程中遇到格式错乱、字节顺序不对、或者夏令时处理等问题。今天我就结合自己多年在项目中的实际应用把这个过程的每一步掰开揉碎不仅告诉你“怎么做”更重点解释“为什么这么做”以及那些容易踩坑的细节。2. 核心思路与系统时钟解析在动手写代码之前我们必须先搞清楚PLC里的“时间”是什么以及我们最终要的“字符串”又是什么。这决定了我们整个方案的选型。2.1 PLC中的时间数据类型DTL与DATE_AND_TIME西门子S7-1200/1500 PLC提供了几种用于处理时间的类型最常用的是DTLDate and Time Long和传统的DATE_AND_TIME简称DT。在博途V16及更高版本中DTL类型是更推荐使用的因为它精度更高纳秒级结构更清晰且与时区信息分离避免了DATE_AND_TIME类型中关于字节顺序和BCD码的一些历史遗留问题。一个DTL变量占用12个字节内部结构包含了年、月、日、时、分、秒、纳秒等独立字段。而我们要获取的系统时间通过指令RD_SYS_TRead System Time读取返回的正是DTL格式的数据。所以我们的起点是一个12字节的DTL结构体。2.2 目标字符串格式的考量我们需要把DTL转换成字符串。字符串在PLC里是String类型它有一个最大长度属性和实际长度。常见的需求格式有YYYY-MM-DD HH:MM:SS(例如2024-05-27 14:30:15)YYYYMMDDHHMMSS(紧凑格式用于文件名或报文)DD.MM.YYYY(欧洲常见日期格式)格式的选择直接影响后续转换逻辑的复杂度。第一种格式最通用可读性最好但包含空格和分隔符需要逐个字符拼接。第二种格式虽然字符数固定但需要处理年月日时分秒每个部分转换为2位数字。我们以第一种格式作为标准示例进行讲解掌握了它其他格式都能触类旁通。2.3 整体方案设计分而治之最直接、最稳定的转换思路是“分而治之”读取使用RD_SYS_T指令获取当前的系统时间DTL格式。分解将DTL结构体中的年、月、日、时、分、秒等字段分别提取出来通常存储为整数Int或DInt。转换将每个整数部分转换为对应的2位数字字符如月份5需要变成05。这里需要处理一位数前面补零的情况。拼接按照目标格式YYYY-MM-DD HH:MM:SS将转换好的数字字符以及分隔符-: 空格按顺序拼接成一个完整的String。这个方案的优势是步骤清晰每个环节都可控便于调试和排查问题。也有使用S_CONV数值转字符串结合字符串函数直接格式化的方法但对于固定格式且包含分隔符的场景分步拼接反而更直观可靠。注意PLC的系统时间需要准确。在设备初次上电或长时间断电后PLC的保持性实时时钟可能会不准。在重要的项目中通常会考虑通过NTP协议从网络同步时间或者由上位机定期写入时间。本项目默认PLC系统时间已校准。3. 关键指令与数据块详细配置接下来我们进入博途V16的实操环节。我会创建一个完整的函数块FB来封装这个功能。3.1 创建数据块与变量定义首先我们需要一个地方来存储中间变量和结果。我习惯创建一个全局数据块DB或者在一个FB的静态变量Static中定义。在FB的接口Interface或独立DB中定义以下变量VAR_INPUT // 通常可以留空或添加一个使能触发信号 Trig : Bool; END_VAR VAR_OUTPUT TimeString : String[30]; // 输出最终的时间字符串长度预留充足 Done : Bool; // 转换完成信号 Error : Bool; // 错误信号 ErrorID : Word; // 错误代码 END_VAR VAR // 临时变量建议放在FB的Static区域或Temp区域 sysTime : DTL; // 存储读取的系统时间 year : Int; month : Int; day : Int; hour : Int; minute : Int; second : Int; tempString1 : String[4]; tempString2 : String[2]; // ... 其他用于中间拼接的字符串 END_VAR变量定义解析TimeString输出长度设为30足以容纳2024-05-27 14:30:1519个字符以及字符串类型本身的开销。Done/Error这是好的编程习惯让调用方能清晰知道功能块执行状态。sysTimeDTL类型用于承接RD_SYS_T指令的结果。year, month...用于从DTL中提取出的各个时间分量。tempString1/2用于中间转换过程的临时字符串。例如tempString2可以用来存放转换后的两位月份或日期。3.2 核心指令 RD_SYS_T 详解RD_SYS_T指令是这一切的起点。在指令树中你可以在“基本指令” - “日期和时间”下找到它。在FB的主程序中添加RD_SYS_T指令#RD_SYS_T(OUT : #sysTime);这条指令没有输入参数执行后会将PLC当前的系统时间写入到#sysTime这个DTL变量中。重要细节RD_SYS_T读取的是PLC硬件时钟的时间。这个时间在PLC断电时由超级电容或电池保持。该指令的执行不需要放在周期性循环里每周期都调用。通常可以由一个上升沿脉冲触发或者每秒调用一次即可避免不必要的处理负担。在我们的FB里可以通过输入Trig的上升沿来触发一次读取和转换。3.3 时间分量的提取与处理读取到sysTime后我们需要从中提取出年、月、日等。DTL是结构体可以直接访问其成员。#year : #sysTime.YEAR; #month : #sysTime.MONTH; #day : #sysTime.DAY; #hour : #sysTime.HOUR; #minute : #sysTime.MINUTE; #second : #sysTime.SECOND; // 如果需要纳秒可以访问 #sysTime.NANOSECOND这一步非常简单直接。提取出来的变量都是整数。例如五月提取出来month就是5。关键处理补零操作我们的目标格式要求月份、日期、小时等必须是两位数字。所以当数字小于10时前面需要补一个字符‘0’。这是转换的核心技巧之一。我们不能直接对整数做补零需要先将其转换为字符串然后判断长度。博途提供了Int_To_StringI_S等转换指令但更灵活的方法是使用S_CONV转换值指令并配合格式字符串。以转换月份为例实现补零// 假设 month 5 IF #month 10 THEN #tempString2 : CONCAT(IN1 : 0, IN2 : INT_TO_STRING(#month)); ELSE #tempString2 : INT_TO_STRING(#month); END_IF;这样tempString2在月份为5时就是05为12时就是12。对day,hour,minute,second都需要进行同样的处理。年份通常直接转换为4位字符串即可。实操心得这里容易出错的地方是INT_TO_STRING转换后字符串里可能包含前导空格因为String是定长左对齐。虽然在我们拼接时影响不大但如果你需要单独使用这个字符串最好用LEFT或RIGHT指令配合LEN来修剪一下。更稳健的做法是使用S_CONV指令指定格式%02d来直接生成两位数字符串但博途的S_CONV对格式字符串支持不如高级语言灵活需要测试。4. 字符串拼接与完整功能块实现各个部分都准备好后就到了“组装”的时刻。我们将使用CONCAT连接指令把年份字符串、分隔符、月份字符串……按顺序拼接起来。4.1 逐步拼接法这是一种清晰易懂的方法逐步构建最终字符串。// 1. 拼接日期部分 YYYY-MM-DD #TimeString : INT_TO_STRING(#year); // 例如 “2024” #TimeString : CONCAT(IN1 : #TimeString, IN2 : -); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #monthStr); // #monthStr是已经补零的月份字符串 “05” #TimeString : CONCAT(IN1 : #TimeString, IN2 : -); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #dayStr); // 补零的日期字符串 // 2. 拼接空格 #TimeString : CONCAT(IN1 : #TimeString, IN2 : ); // 3. 拼接时间部分 HH:MM:SS #TimeString : CONCAT(IN1 : #TimeString, IN2 : #hourStr); #TimeString : CONCAT(IN1 : #TimeString, IN2 : :); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #minuteStr); #TimeString : CONCAT(IN1 : #TimeString, IN2 : :); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #secondStr);这种方法逻辑直白但会产生多条CONCAT指令。注意每次CONCAT都会产生一个新的临时字符串对于PLC来说只要不是在高频循环中调用性能完全足够。4.2 使用中间变量一次性拼接为了代码更紧凑也可以先组装好日期串和时间串最后再合并。VAR datePart : String[11]; // YYYY-MM-DD 固定10字符 timePart : String[9]; // HH:MM:SS 固定8字符 END_VAR // 组装日期部分 #datePart : CONCAT(IN1 : INT_TO_STRING(#year), IN2 : -); #datePart : CONCAT(IN1 : #datePart, IN2 : #monthStr); #datePart : CONCAT(IN1 : #datePart, IN2 : -); #datePart : CONCAT(IN1 : #datePart, IN2 : #dayStr); // 组装时间部分 #timePart : CONCAT(IN1 : #hourStr, IN2 : :); #timePart : CONCAT(IN1 : #timePart, IN2 : #minuteStr); #timePart : CONCAT(IN1 : #timePart, IN2 : :); #timePart : CONCAT(IN1 : #timePart, IN2 : #secondStr); // 合并 #TimeString : CONCAT(IN1 : #datePart, IN2 : ); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #timePart);4.3 完整功能块的执行逻辑与封装将以上所有步骤整合到一个FB中并添加合理的控制逻辑。FUNCTION_BLOCK FB_GetTimeString VAR_INPUT Trig : Bool; // 上升沿触发一次转换 END_VAR VAR_OUTPUT TimeString : String[30]; Done : Bool; Error : Bool; ErrorID : Word; END_VAR VAR sysTime : DTL; year, month, day, hour, minute, second : Int; yStr : String[4]; mStr, dStr, hStr, minStr, sStr : String[2]; trigRising : R_TRIG; // 上升沿检测 END_VAR // 主程序 #trigRising(CLK : #Trig); IF #trigRising.Q THEN // 1. 读取时间 #RD_SYS_T(OUT : #sysTime); // 2. 提取分量 #year : #sysTime.YEAR; #month : #sysTime.MONTH; #day : #sysTime.DAY; #hour : #sysTime.HOUR; #minute : #sysTime.MINUTE; #second : #sysTime.SECOND; // 3. 转换为2位字符串并补零 #yStr : INT_TO_STRING(#year); #mStr : INT_TO_STRING(#month); IF #month 10 THEN #mStr : CONCAT(0, #mStr); END_IF; // ... 对 dStr, hStr, minStr, sStr 进行同样处理 // 4. 拼接 #TimeString : CONCAT(IN1 : #yStr, IN2 : -); #TimeString : CONCAT(IN1 : #TimeString, IN2 : #mStr); // ... 继续拼接其他部分 #Done : TRUE; #Error : FALSE; #ErrorID : 0; END_IF; // 当Trig信号下降或为假时复位Done信号可选取决于你的应用需求 IF NOT #Trig THEN #Done : FALSE; END_IF;这样一个可复用的时间字符串转换功能块就完成了。在OB1中实例化并调用它即可。5. 高级应用、优化与问题排查掌握了基础方法后我们来看看如何让它更健壮、更高效并解决可能遇到的问题。5.1 性能优化与扫描周期考虑如果你的程序需要在每个扫描周期都获取时间例如用于高速时间戳频繁进行字符串转换和拼接会消耗可观的CPU资源。此时可以优化缓存策略每秒只执行一次完整的RD_SYS_T和转换将结果存储在全局变量中。其他需要时间的逻辑直接读取这个缓存字符串。这通过一个TON定时器或利用系统时钟的秒脉冲Clock_1Hz来实现。简化格式如果不需要可读性直接使用DTL或将其转换为DInt格式的Unix时间戳进行传输和存储在上位机侧进行格式化这是最高效的方式。使用S_CONV指令虽然格式字符串有限但可以尝试S_CONV(VAL : #sysTime, FORMAT : ‘%T’, STRG : #TimeString)。不过%T格式输出的字符串格式是固定的可能不符合你的自定义需求需要测试。5.2 时区与夏令时处理这是一个在跨国项目或需要与UTC时间对齐时才会遇到的深水区。RD_SYS_T读取的是PLC硬件设置的本地时间。PLC的硬件时钟本身不包含时区信息。需求如果你的系统需要UTC时间或者需要根据不同地区工厂自动调整就需要在程序里处理。方案硬件设置将PLC的硬件时钟直接设置为UTC时间。这是最推荐的做法从源头上统一。所有本地时间的转换由上位机HMI/SCADA根据操作员所在时区来完成。软件补偿如果PLC必须使用本地时间但又需要生成一个带时区标识的字符串如2024-05-27T14:30:1508:00你需要在程序里硬编码一个时区偏移量例如东八区为8小时并对读取的小时数进行加减计算。这非常不推荐因为无法处理夏令时自动切换。上位机负责PLC只提供原始的DTL时间或本地时间字符串时区转换和夏令时规则这种复杂逻辑交给更擅长处理的上位机或服务器。踩坑记录曾经有一个项目设备发往欧洲和美国PLC时间字符串用于生成本地报告。最初在PLC里做了时区补偿结果忘记考虑夏令时导致每年两次报告时间错乱。后来改为PLC始终使用UTC时间问题彻底解决。牢记在工业现场PLC的时间最好保持简单、一致、无需自动调整。5.3 常见问题与调试技巧即使逻辑正确第一次运行时也可能得不到预期的字符串。以下是常见问题及排查步骤问题现象可能原因排查方法输出TimeString为空1. FB未被正确调用或使能信号未触发。2.RD_SYS_T指令故障极罕见。3. 字符串拼接过程中出现非法操作。1. 在线监控FB的输入Trig和输出Done。2. 在线查看sysTime变量是否有值。3. 在拼接每一步后都监控中间字符串变量。字符串格式错乱如出现乱码1. 字符串长度定义不足导致拼接时被截断。2. 整数转字符串时包含了前导空格。3. 访问了未初始化的字符串变量。1. 确保所有临时String和最终输出的长度都足够大建议预留额外5-10字节。2. 使用RIGHT指令RIGHT(IN : INT_TO_STRING(num), L : 2)来获取右两位。3. 在FB初始化或第一次调用时给所有字符串变量赋初值。时间与实际相差整小时PLC硬件时钟的时区设置错误。进入PLC的硬件配置查看“时间同步”或“硬件时钟”设置确认时区是否为所在地区。转换出的月份/日期为0从DTL中提取字段错误或RD_SYS_T未成功执行。检查DTL变量sysTime的各个成员值。确保RD_SYS_T指令被成功执行无背景错误。程序运行后时间字符串不更新触发逻辑有问题Trig信号没有产生上升沿。将Trig信号改为常TRUE测试或使用每秒一次的时钟脉冲Clock_1Hz直接触发。调试技巧分段监控不要只盯着最终结果。在提取year,month后立刻监控这些整数值是否正确。强制与修改在线时可以尝试强制修改sysTime为一个已知值如DTL#2024-05-27-14:30:00然后单步执行看转换过程是否正确。使用“写入值”在监控表中对中间字符串变量使用“写入值”功能模拟拼接过程快速定位问题环节。6. 扩展应用时间戳在项目中的实战用法生成时间字符串不是终点而是起点。下面分享几个我在实际项目中应用的场景。6.1 生成带时间戳的报警与事件日志这是最经典的应用。当设备发生报警时除了记录报警编号、文本还要记录发生的时间。// 当报警触发时 IF #Alarm_Trigger THEN #GetTimeString_FB(Trig : TRUE); // 触发时间获取 IF #GetTimeString_FB.Done THEN // 将报警信息和时间字符串组合存入一个FIFO先入先出队列或发送给HMI #LogEntry : CONCAT(IN1 : #GetTimeString_FB.TimeString, IN2 : - Alarm ); #LogEntry : CONCAT(IN1 : #LogEntry, IN2 : INT_TO_STRING(#Alarm_ID)); #LogEntry : CONCAT(IN1 : #LogEntry, IN2 : : ); #LogEntry : CONCAT(IN1 : #LogEntry, IN2 : #Alarm_Text); // 调用队列写入函数 #WriteToLogQueue(Entry : #LogEntry); END_IF; END_IF;6.2 创建基于时间的唯一文件名当PLC需要将数据记录到插入的SD卡或通过网络传输文件时为了避免覆盖常用时间生成文件名。// 生成一个如 “DataLog_20240527_143015.csv” 的文件名 #GetTimeString_FB(Trig : #GenerateFileName); IF #GetTimeString_FB.Done THEN // 提取日期和时间部分并移除分隔符 #datePart : MID(IN : #GetTimeString_FB.TimeString, L : 10, P : 1); // “2024-05-27” #datePart : REPLACE(IN : #datePart, OLD : -, NEW : ); // “20240527” #timePart : MID(IN : #GetTimeString_FB.TimeString, L : 8, P : 12); // “14:30:15” #timePart : REPLACE(IN : #timePart, OLD : :, NEW : ); // “143015” #fileName : CONCAT(IN1 : DataLog_, IN2 : #datePart); #fileName : CONCAT(IN1 : #fileName, IN2 : _); #fileName : CONCAT(IN1 : #fileName, IN2 : #timePart); #fileName : CONCAT(IN1 : #fileName, IN2 : .csv); END_IF;6.3 与上位系统进行数据交互时的报文拼接在与MES或SCADA通信时协议报文里经常需要包含时间戳。// 假设要生成一个JSON-like的字符串: {time:2024-05-27 14:30:15,value:123.4} #GetTimeString_FB(Trig : #SendData); IF #GetTimeString_FB.Done THEN #telegram : {time:; #telegram : CONCAT(IN1 : #telegram, IN2 : #GetTimeString_FB.TimeString); #telegram : CONCAT(IN1 : #telegram, IN2 : ,value:); #telegram : CONCAT(IN1 : #telegram, IN2 : REAL_TO_STRING(#SensorValue)); #telegram : CONCAT(IN1 : #telegram, IN2 : }); // 通过TCP或UDP发送 #telegram END_IF;最后一点个人体会处理时间看似简单但在工业现场它关乎追溯的准确性和系统联调的顺畅度。我养成的习惯是在项目初期就统一规划好整个系统中所有设备PLC、HMI、服务器的时间源和同步方式并在PLC程序中将时间处理功能标准化、模块化。这个“获取系统时间并转换字符串”的FB就是我标准库里的一个常驻模块。把它做稳定、做清晰后续在需要时间戳的任何地方直接调用能省去大量重复调试和潜在的不一致问题。尤其是在调试跨国项目或者需要7x24小时连续运行的生产线时一个可靠的时间基准就像是给整个系统安上了一个精准的节拍器。