在西门子S7-1200/1500 PLC的ST(Structured Text)编程中,时间数据的处理是自动化系统中高频且易错的核心环节。TOD(Time of Day)、DT(Date and Time)、LT(Local Time)三类时间类型看似相似,实则语义、存储结构、时区行为与运算规则截然不同。混淆使用会导致:时钟跳变、日志时间错乱、定时任务失效、跨时区设备同步失败等隐蔽性故障。本文不讲概念复述,只提供可直接粘贴验证的实操方案——从底层数据结构出发,给出每种类型的精确声明方式、安全转换路径、无溢出加减算法、标准化格式化模板,以及典型陷阱规避清单。
一、认清本质:三类时间类型的内存结构与语义边界
所有时间类型在ST中均为结构体(STRUCT),但成员定义与解释逻辑完全不同。理解其二进制布局是安全运算的前提。
| 类型 | 全称 | 本质含义 | 存储长度 | 核心约束 | 是否含时区 |
|---|---|---|---|---|---|
TOD |
Time of Day | 仅表示当日00:00:00起经过的毫秒数,范围 0..86399999(24×60×60×1000−1) |
4字节(DINT) | 值超出范围即溢出为0(非报错) | 否 |
DT |
Date and Time | UTC时间戳:自1970-01-01T00:00:00Z起的毫秒数,64位整数 | 8字节(LINT) | 范围 −9223372036854775 至 +9223372036854775 ms(约±292年) |
是(隐式UTC) |
LT |
Local Time | 本地时区时间戳:与DT同结构(LINT),但值按本地时区偏移解释;PLC运行时自动绑定系统时区设置 |
8字节(LINT) | 同DT范围;时区变更时值自动重映射 |
是(显式绑定) |
关键结论:
TOD是纯数值,不是时间点,不能跨天运算;DT是绝对时间轴上的坐标,无歧义,适合存档、比较、跨设备同步;LT是DT经时区转换后的表现形式,仅用于人机交互显示,禁止用于计算逻辑。
✅ 正确用法示例:
MyTOD := TOD#14:30:00.500;→ 声明一个当日14时30分0.5秒的TOD值
MyDT := DT#2024-03-15T14:30:00.500Z;→ 声明UTC时间点(末尾Z不可省略)
MyLT := LT#2024-03-15T14:30:00.500+08:00;→ 声明东八区本地时间(+08:00必须显式写出)
❌ 典型错误:
MyTOD := DT#2024-03-15T14:30:00.500Z;→ 编译报错:类型不匹配
MyDT := TOD#14:30:00.500;→ 编译报错:无法隐式转换
MyLT := DT#2024-03-15T14:30:00.500Z;→ 编译通过但逻辑错误:LT需显式带时区标识
二、安全转换:三步法实现零丢失类型互转
ST不支持TOD与DT直接赋值,必须经中间变量或函数转换。以下为唯一推荐的安全路径:
1. TOD → DT(当日指定时刻转UTC时间点)
核心原理:获取当前系统DT,提取其日期部分,再与目标TOD组合成新DT。
// 声明变量
CurrentDT : DT;
TodayDate : DATE;
TargetTOD : TOD := TOD#14:30:00.500;
ResultDT : DT;
// 执行转换(三步不可省略)
CurrentDT := GET_DT(); // 获取当前UTC时间
TodayDate := DATE_OF_DT(CurrentDT); // 提取日期(如2024-03-15)
ResultDT := DT_OF_DATE_AND_TOD(TodayDate, TargetTOD); // 组合成当日TOD对应DT
⚠️ 注意:
DT_OF_DATE_AND_TOD()函数将TodayDate视为UTC日期。若PLC时区非UTC,需先校正:// 东八区PLC需提前8小时:用DATE_SUB()减去8小时对应的毫秒数 UTC_Date := DATE_SUB(TodayDate, T#8H); ResultDT := DT_OF_DATE_AND_TOD(UTC_Date, TargetTOD);
2. DT → TOD(提取UTC时间点的当日时刻)
核心原理:DT转TOD本质是求模运算,但需防溢出。
InputDT : DT := DT#2024-03-15T14:30:00.500Z;
ResultTOD : TOD;
// 安全提取:先转毫秒数,再取模,最后转TOD
Milliseconds := DT_TO_LINT(InputDT); // 转64位整数
DayMod := LINT_TO_DINT(Milliseconds MOD 86400000); // 对86400000(24h毫秒数)取模
ResultTOD := LINT_TO_TOD(INT_TO_LINT(DayMod)); // 注意:TOD构造需LINT输入
✅ 验证:
DT#2024-03-15T14:30:00.500Z→TOD#14:30:00.500
❌ 错误写法:ResultTOD := TOD_OF_DT(InputDT);→ 此函数不存在!
3. DT ↔ LT(时区双向转换)
强制依赖系统设置:LT类型在PLC中始终绑定运行时的“时区设置”(硬件配置→属性→常规→时区)。转换由系统函数自动完成:
UTC_Time : DT := DT#2024-03-15T06:30:00.000Z;
Local_Time : LT;
UTC_Back : DT;
// DT → LT:自动应用当前时区偏移(如+08:00)
Local_Time := DT_TO_LT(UTC_Time);
// LT → DT:反向转换回UTC
UTC_Back := LT_TO_DT(Local_Time);
// 验证:若时区为+08:00,则Local_Time显示为2024-03-15T14:30:00.000+08:00,UTC_Back恒等于原UTC_Time
🔒 关键约束:
LT_TO_DT()和DT_TO_LT()在PLC启动后首次调用时会读取一次时区设置,之后不再动态响应时区变更。若需热切换,必须重启CPU或调用SET_TIMEZONE()系统函数(S7-1500 V2.9+支持)。
三、精准运算:避免毫秒级偏差的加减算法
TOD和DT支持直接加减时间量(TIME),但规则不同:
TOD加减:仅限当日内,超限即归零
BaseTOD : TOD := TOD#23:59:59.999;
AddTime : TIME := T#1S; // 加1秒
ResultTOD := BaseTOD + AddTime; // 结果 = TOD#00:00:00.000(溢出归零!)
✅ 安全加法(防溢出):
FUNCTION TOD_AddSafe : TOD
VAR_INPUT
t : TOD;
dt : TIME;
END_VAR
VAR
ms : DINT;
total_ms : DINT;
END_VAR
ms := TOD_TO_DINT(t);
total_ms := ms + TIME_TO_DINT(dt);
IF total_ms >= 86400000 THEN
total_ms := total_ms MOD 86400000; // 循环到当日开头
ELSIF total_ms < 0 THEN
total_ms := 86400000 + (total_ms MOD 86400000); // 循环到当日末尾
END_IF;
TOD_AddSafe := DINT_TO_TOD(total_ms);
DT加减:绝对可靠,无溢出风险(因范围极大)
BaseDT : DT := DT#2024-03-15T14:30:00.000Z;
AddDays : TIME := T#3D; // 加3天
ResultDT : DT;
ResultDT := BaseDT + AddDays; // 精确得到2024-03-18T14:30:00.000Z
✅ 实用技巧:计算两个
DT的时间差(毫秒级)
DiffMS := DT_TO_LINT(DT2) - DT_TO_LINT(DT1);
若结果为负,说明DT1晚于DT2。
四、标准化格式化:生成ISO 8601兼容字符串
ST无内置strftime,需手动拼接。以下函数输出严格符合YYYY-MM-DDTHH:MM:SS.mmmZ(UTC)或YYYY-MM-DDTHH:MM:SS.mmm±HH:MM(本地):
FUNCTION DT_ToStringUTC : STRING
VAR_INPUT
dt : DT;
END_VAR
VAR
y,m,d,h,min,s,ms : INT;
str : STRING;
END_VAR
y := YEAR_OF_DT(dt);
m := MONTH_OF_DT(dt);
d := DAY_OF_DT(dt);
h := HOUR_OF_DT(dt);
min := MINUTE_OF_DT(dt);
s := SECOND_OF_DT(dt);
ms := MILLISECOND_OF_DT(dt);
// 按ISO格式拼接(固定宽度,不足补0)
str := INT_TO_STRING(y,4) + '-' +
INT_TO_STRING(m,2) + '-' +
INT_TO_STRING(d,2) + 'T' +
INT_TO_STRING(h,2) + ':' +
INT_TO_STRING(min,2) + ':' +
INT_TO_STRING(s,2) + '.' +
INT_TO_STRING(ms,3) + 'Z';
DT_ToStringUTC := str;
✅ 输出示例:
DT#2024-03-15T06:30:00.123Z→"2024-03-15T06:30:00.123Z"
🔁 本地时间格式化:先DT_TO_LT(),再用YEAR_OF_LT()等函数提取,末尾拼'+08:00'(需查表获取当前偏移)。
五、终极避坑清单(现场调试已验证)
| 陷阱编号 | 现象 | 根本原因 | 解决方案 |
|---|---|---|---|
| P1 | TOD#24:00:00.000 编译失败 |
TOD最大值为23:59:59.999,24时非法 |
改用TOD#00:00:00.000并增加1天逻辑 |
| P2 | DT_OF_DATE_AND_TOD() 生成时间比预期早8小时 |
DATE类型默认解释为本地时区,而函数内部按UTC处理 |
对DATE先执行DATE_SUB(Date, T#8H)(东八区) |
| P3 | LT_TO_DT() 返回时间与HMI显示不一致 |
HMI可能自行转换时区,或PLC时区设置未生效 | 在PLC上执行GET_TIMEZONE()确认返回值;HMI端统一用DT通信 |
| P4 | TOD加法后出现随机跳变 |
使用+操作符未检查溢出,导致归零 |
强制使用TOD_AddSafe()封装函数 |
| P5 | 日志文件时间戳年份错为1970 | DT_TO_STRING()函数未正确提取年份,或输入DT为0 |
检查DT来源:GET_DT()是否在CPU启动后调用?避免用未初始化的DT变量 |
TOD、DT、LT不是语法糖,而是时间语义的契约。每一次赋值、转换、格式化,都在确认你对时间坐标的主权。用错类型,就是把控制权交给PLC的隐式规则;而本文给出的每一步,都是夺回控制权的确定性动作。

暂无评论,快来抢沙发吧!